Rust之路(1)
【未经书面许可,严禁转载】-- 2020-10-09 --
正式开始Rust学习之路了!
思而不学则罔,学而不思则殆。边学边练才能快速上手,让我们先来个Hello World!
但前提是有Rust环境啊!
Rust是跨平台的语言,而且无论在Windows还是在Linux、macOS上安装都比较简单。打开官网的安装指导页面:
https://www.rust-lang.org/tools/install
网站会根据当前使用的系统给予响应的安装指导。Windows系统就是下载exe安装程序,下载后安装即可;macOS和Linux使用一道curl命令安装。
安装过程是在命令行模式下进行,当有如下提示时输入1,即可:
Current installation options: default host triple: x86_64-apple-darwin default toolchain: stable (default) profile: default modify PATH variable: yes 1) Proceed with installation (default) 2) Customize installation 3) Cancel installation >
[题内话]
- Rust安装目录都是默认在用户的家目录。我在Windows上安装的时候按照说明改了系统变量也没修改成功安装目录,但是安装后可以把系统盘的用户目录里安装后的文件夹(有两个文件夹:.cargo和.rustup,注意文件夹的第一个字符是一个点)拷贝到别的盘,然后修改系统变量指向新的位置(必要的步骤)。更改安装位置是因为后期随着Rust安装的库增多,.cargo文件夹会变的很大,有系统盘吃紧的危险。
- Windows系统需要同时安装Visual Studio C++ Build tools,如果安装过visual Studio2013以上可以忽略,否则请至https://visualstudio.microsoft.com/visual-cpp-build-tools/下载安装。
- macOS安装后编译Rust程序出现linking with cc failed错误,并且有xcrun error字样的话,安装苹果家的开发环境Xcode即可(APP Store搜索安装)。
- 由于访问Rust官网下载和安装较慢,可以使用国内镜像服务器(以清华大学TUNA镜像站为例,也可自行选择其他镜像站):在安装前,Windows系统在系统变量里增加RUSTUP_DIST_SERVER,变量值为https://mirrors.tuna.tsinghua.edu.cn/rustup;macOS和Linux执行命令:echo 'export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup' >> ~/.bash_profile,执行此命令后可能需要重新打开终端输入安装命令才能生效。
[题外话]
- 当今时代,跨平台已成为硬通货。Rust是跨平台的,这意味着,使用率最高的三大系统Windows、macOS、Linux都可以安装使用,源码不用改动即可以从Windows上编写,然后把源码拷贝到Linux上,重新编译即可使用(当然涉及到依赖系统API的除外)。
- 最近的Windows 10更新版本内置了Linux系统,而且不是用虚拟化技术实现,而是直接嵌入式。微软积极拥抱Linux内核,说明可能十年八年以后,桌面操作系统的格局会有很大变化,所以,学习跨平台的技术,能使用更持久。
因为安装很简单,不多赘述。
关于用什么IDE(写代码、编译、调试的环境),其实有很多选择。国内更喜欢微软的Visual Studio Code,国外用Atom的也很多,还有jetbrains公司的IntelliJ Rust + Clion(Clion是IDE,Intellij Rust是插件,貌似免费的社区版不带调试功能)。因为配置环境的教程很多,我也不多说了,各个电脑有各个电脑的情况,出现了不能调试或其他问题的解决方法也不能一概而论。
安装完毕,先跑个分?(Oh, NO!我们不是雷总)
按照国际编程惯例,先Hello World一下?
Emmm,我们应该更有追求一点,写个稍微有意义点的Demo。
[题内话]
如果需要Rust安装或IDE安装详细过程,可以留言,我再考虑完善。
[题外话]
某些方面真的能体现出Rust是年轻的编程语言:一方面:没有统领性的IDE,几大IDE厂商还在完善和竞争的阶段,各IDE都有优缺点,配置也是各种野路子;另一方面,Rust主要还是开发黑窗口应用,GUI编程还不成熟。不过好消息是据说Rust2021版会集成有官方GUI框架。
开始进入Rust的世界!
Rust源码文件的后缀名是rs,主文件一般命名为main.rs。下面我们新建一个Rust源码文件main.rs,用IDE的编辑器,甚至可以用记事本打开此文件,输入:
1 //Rust程序1.1 2 //程序入口函数 3 fn main() { 4 let a = 4; 5 let b = 6; 6 let num = gcd(a,b); 7 println!("{}和{}的最大公约数是{}", a, b, num); 8 } 9 //函数,输入两个数字,输出二者的最大公约数 10 fn gcd(mut n: i32, mut m: i32) -> i32 { 11 assert!(n != 0 && m != 0); 12 while m != 0 { 13 if m < n { 14 let t = m; 15 m = n; 16 n = t; 17 } 18 m = m % n; 19 } 20 n 21 }
这个小程序使用欧几里得算法计算两个数字的最大公约数。将main.rs文件保存,打开命令提示符(windows系统)或终端窗口(macOS、Linux),使用cd命令切换到文件保存目录,然后输入编译命令:
rustc main.rs
回车,main.rs就会进行编译,编译完成在同一目录就会有编译成功的程序main.exe(Windows系统)或main(macOS、Linux)。
然后输入:
main
回车,命令就会执行。执行结果输出为:
4和6的最大公约数是2
然后程序自动退出。
此段程序代码的信息量很大,不要着急,我们一句一句来解释,解释通了这个小程序,就相当于望见了Rust大门的门楣了!
程序1.1的第3行(正式代码的第1行):
fn main() {
这是Rust的函数定义,关键字fn是function(函数)的缩写,音标读作[fʌn]。用fn来表示此处正在定义一个函数。main是函数名称,可以使用字母、数字、下划线,但不能用数字开头。前面说过,Rust的入口函数必须是main,即,一个Rust可执行程序的源代码必须有且只有一个main函数,程序就是从main函数开始的。函数名后跟一对小括号,如果函数有参数,可以放到小括号中间(此处main函数是没有参数的,所以小括号内是空的)。此行最后是左大括号,预示着后面是main函数的函数体了。
第4、5行:
let a = 4;
let b = 6;
各声明了一个变量并给变量赋值,let是变量声明的关键字,后面跟的是变量名、赋值操作符=以及变量的值(数字4和6)。
变量名的规则和函数名一样,使用字母、数字和下划线。所赋的值4和6在Rust中默认是i32型,即32位带符号整数型,带符号的意思是有正值也有负值,下一节讲数据类型。
Rust的变量声明规则有:
- 声明变量并同时赋值语句中,如果能从所赋的值推断出类型,那么就不需要写变量的类型。如果无法推断,或者是只有变量声明,没有赋值(形如 let a;),则必须加变量的类型,语法是let 变量名:类型(写作let a:i32);
- 赋值可以用字面量,也可以用表达式(例如后面学习的match、if else语句);
- 关于推断,Rust是非常高阶的。会分析整个代码,将没标明类型的变量做出推断。如发现无法推断又没标明类型的,就会发出异常编译错误。
- 默认情况下,一旦一个变量被初始化,它的值就不能改变,但是在变量名之前加上mut关键字(发音为[mjuːt],是mutable的缩写)可以声明可变变量。在实践中,大多数变量都没有声明为可变。使用mut强调变量是否可变在阅读代码时非常有用。
[题内话]
Rust的类型推断不限于当前的赋值语句,而是对所有代码综合分析,这一点不像其他语言。例如:
将程序1.1中的gcd函数签名修改为:fn gcd(mut n: u64, mut m:u64) -> u64
即函数的输入参数和返回值都修改为64位无符号整型,其他代码都不变。那a、b应该推断为什么类型呢?
按常规理论,编译器首先读取了let a=4;let b=6;代码以后,应该把a和b推断成最常规的、数字4和6的类型:i32型。然而事实并非如此!
事实是:对整段程序代码进行分析,后面会将a、b当做实参传入gcd函数,而gcd的形参类型我们修改为u64型了,所以,a和b最好的安排就是使用u64型,这样传入gcd函数的时候,值类型才正确。这一点是可以证明的。
Rust的绝大多数类型的值都有一个type_id()方法,用于返回类型标识,其值是std::any::TypeId类型,这个类型用一串数字标识各种类型,每种类型都不一样。另外std::any::TypeId类型还有一个泛型静态函数TypeId::of::<T>(),尖括号<>内的T是某种类型名,例如i32,String等等,返回值是这种类型的类型标识。
我们就用这个函数验证一下上面的结论:在main函数的最后部分:
println!("{}和{}的最大公约数是{}", a, b, num);
//加上如下一段代码
println!("a的TypeId是:{:?}", a.type_id());
println!("b的TypeId是:{:?}", b.type_id());
println!("num的TypeId是:{:?}", num.type_id());
println!("i32类型的TypeId是:{:?}", TypeId::of::<i32>());
println!("u64类型的TypeId是:{:?}", TypeId::of::<u64>());
//以上为加上的代码段
}
运行后会打印出a、b、num、i32类型、u64类型的TypeId,可以看出a、b、num都和u64类型的TypeId相同,而不是i32的TypeId。
上面有些概念不理解没问题,后面会陆续讲到。
第6行:
let num = gcd(a,b);
前半部分let num=,和上面讲的一样,是变量声明和赋值。后半部分gcd(a,b),是函数调用的格式,即程序运行到此处,会进入到函数gcd内执行,并把a、b两个变量分别赋给gcd函数的两个形参。gcd函数执行完毕,获取执行的结果,然后赋值给num变量。
第7行:
println!("{}和{}的最大公约数是{}", a, b, num);
Rust中,一个标识符加一个叹号,是宏的使用格式,println是宏名。在编译的时候,宏会被一段代码替换,代码是println宏定义来定义的。println宏接收一个模板字符串,将第2个及以后的参数的格式化版本填充到模板字符串。在本例中,a的值替换掉字符串中第一个{},b替换第二个{},num替换第三个{}。形成了“4和6的最大公约数是2”字符串。然后输出到标准输出,也就是屏幕。
第10行:
fn gcd(mut n: i32, mut m: i32) -> i32 {
声明了gcd函数,与上面的main函数不同,gcd函数的小括号内有两个参数。因为函数体内需要改变m和n的值,所以参数用mut标记为可变,而且函数的形参需要声明类型,格式为:变量名:类型。i32表示为32位带符号整型,而u32表示32位无符号整型,u是unsigned的简写。
函数声明的后半部分 ->i32,表示的是函数的返回值类型也是i32。
第11-21行:
assert!(n != 0 && m != 0); while m != 0 { if m < n { let t = m; m = n; n = t; } m = m % n; } n }
这一段是函数gcd的函数体和结尾大括号。函数体以对assert宏的调用开始,验证两个参数都不是零。叹号!将其标记为宏调用,而不是函数调用。像C和C++中的断言宏一样,Rust的断言检查它的参数是否为真,如果不是,则输出一条提示消息提示失败及失败的代码位置,并终止程序。Rust的这种突然终止称为panic,大多数中文译文中直译为恐慌,我觉得翻译成异常更好,虽然这样很不Rust。C和C++程序可以跳过断言,但Rust总是检查断言,而不管程序是以什么方式编译的。另外还有一个debug_assert!宏,只有在debug模式下才检查断言,而已优化代码的方式进行release编译时不检查。
函数的核心是包含if语句和赋值的while循环,逻辑很简单,可以了解一下最大公约数的欧几里得计算法。先确认m大于或等于n,如果m比n小,则交换值,然后求m除以n的余数,赋值给m,直到m=0,则此时n就是原来两个数的最大公约数。
所以,函数gcd应该返回n的值,注意函数体最后一行,只有一个n,而且n后面没有分号作为结束。这是Rust的函数返回值的通常写法,函数最后一行,写一个表达式,表达式的值就是函数的返回值。但是如果在函数体代码中间返回时,需要写return xxx;并且需要有分号结束。例如:
fn xxx(a:i32, b:i32)->i32{ if a>b{ return a; } else{ b } }
[题外话]
表达式和语句:在Rust中,一句代码后面没有分号结束,就是表达式,是有值的;有分号结束,就是语句,没有返回值(实际上返回值是个(),()是一种特殊的元组类型,意为空,所以认为是没有返回值)。
Rust不需要在if后的条件表达式用括号括起来,但函数体需要用大括号括起来。后面会用到的whle循环和match匹配,也遵循同样的规定。
程序1.1由两个函数组成:main()和gcd(),main是程序入口。在大多数语言中,main函数体内调用gcd函数,那必须在main函数之前定义gcd函数,最起码像C++那样有个前向声明。但是Rust不在意两个函数的顺序,只要有,编译器就能知道。
Cargo包管理器
我们再看一下在Rust中的大杀器Cargo工具,cargo是Rust的编译管理器、包管理器和通用工具。可以使用Cargo启动一个新项目,构建和运行程序,以及管理代码所依赖的任何外部库。换句话说,用Cargo命令可以创建一个项目,然后可以用IDE编写代码,代码编写完成后,Cargo可以执行编译、运行、测试(当然是Cargo调用了其他的命令来执行这些动作)。而且程序中使用到的依赖包也能靠Cargo在编译阶段自动在线下载和编译,你只需要在配置文件中输入依赖包的名称和版本需求。
例如一个项目开发的过程:
事项 | 操作/命令 | 备注 |
创建一个项目文件夹 |
创建一个可执行程序: cargo new --bin project01 创建一个库项目: cargo new --lib project01 |
project01是项目名称,bin或lib选项前是两个减号--。命令执行完毕,会创建一个项目文件夹,里面包括配置文件(toml后缀名),src文件夹(文件夹内有main.rs或lib.rs) |
编写程序代码 | 在IDE中编写代码,部署多个rs文件 | 第三方依赖包需要在toml配置文件的[dependencies]段添加 |
编译 | cargo build | 下载、编译依赖包,编译程序 |
测试 | cargo test | 此操作会测试代码中使用#[test]标记的函数,其他函数都会忽略 |
运行 | cargo run | 如果想直接运行,可忽略cargo build操作,直接run |
这些是cargo的基本使用,此命令还有很多用法,可运行cargo –h查看帮助。
让我们把上面的程序改造成用cargo来管理。
首先在适当的位置新建一个项目,比如在D:\Programs(Windows)或~/Programs(macOS、Linux)。
Windows |
|
macOS、Linux |
用IDE或记事本打开prog_gcd目录下的main.rs文件,你会发现里面有个HelloWorld程序,所以本例没有展示HelloWorld的写法,因为Rust自带了!把main.rs的内容清空,然后输程序1.1的代码,保存。
再回到命令提示符/终端窗口,运行cargo run,cargo就会自动编译,然后运行编译后的程序,最终得到想要的结果!
是不是So easy!
在Rust开发中,一定要用cargo,它做了很多的幕后工作,为我们节省时间和精力。在包含多文件的项目中,cargo的文件组织能力才是最有用的。
cargo新建项目的时候,新建了项目文件夹prog_gcd,进入项目文件夹,新建了配置文件(项目名).toml,打开后内容为:
1 [package] 2 name = "prog_gcd" #修改此处,可重命名生成的程序名 3 version = "0.1.0" #版本号 4 authors = ["sumyuan"] #作者和版权信息 5 edition = "2018" #Rust版本,目前只能写2015和2018两个版本之一 6 #下面是一段自动生成的说明信息 7 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 9 [dependencies] #这下面可以添加程序的依赖包
toml文件中注释写在#后。我们这个简单程序暂时没有依赖第三方包,所以[dependencies]节的内容为空。
src文件夹内存放rs源文件。在经过cargo build编译,或cargo run编译并运行后,项目文件夹内会产生target文件夹,在taget文件夹内,有debug文件夹,它用于存放编译后的程序,如果在编译时加了release参数,命令如:
cargo build –release 或 cargo run –release
target文件夹内会产生release文件夹,它用于存放release模式编译后的程序。
文件夹内其他的文件我们暂且不管。
题内话:
debug模式和release模式,是绝大多数编译器的两种编译方式。debug模式不对代码做任何优化,并且可以设置断点,附加了很多调试所用到的状态,用于自行调试;release模式对代码会做优化,优化程度也可用参数控制,使得生成的程序或库体积小、运行速度快,但是不能断点调试,用于最终发布。
需要说的内容还有个坑要填。cargo test用于测试程序,前提是代码中设置了测试函数。
我们在程序1.1的最后,加入一个函数,代码如下:
//Rust程序1.1 //前面内容省略...... #[test] fn test_gcd() { let num = gcd(120, 90); println!("{}和{}的最大公约数是{}", 120, 90, num); }
在函数test_gcd()的上方,加上#[test],cargo进行测试的时候会知道这个函数是测试函数。在执行cargo test时,不是去找main函数,反而会查找代码中所有有此标记的函数,依次执行。执行结果:
D:\Programs\prog_gcd>cargo test Compiling prog_gcd v0.1.0 (D:\Programs\prog_gcd) Finished test [unoptimized + debuginfo] target(s) in 1.59s Running target\debug\deps\prog_gcd-d88e6d06203d2866.exe running 1 test test test_gcd ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
结果显示测试通过,说明我们写的test_gcd以及调用的gcd函数是没问题的。
【总结】
在我们写的第一个小程序中,包含了Rust的诸多语法:
变量的声明和赋值,以及可变性(let关键字,mut的用法);
简单的数字类型i32,u64;
函数的声明和定义的语法(fn关键字,返回值类型用->表明);
初步认识宏的使用(叹号!的使用);
类型推断(整体推断);
函数传参(变量或值传入函数);
另外,讨论了cargo的常用命令(new\build\run\test)和程序测试(#[test])。
知道了这些,就算跨入了正式的Rust学习之路了,后面我们陆续进行各种语法的学习和演练。