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

 Rust之路(1)

macOS、Linux

 Rust之路(1)

 

用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学习之路了,后面我们陆续进行各种语法的学习和演练。

 

上一篇:理解内存对齐


下一篇:Rust中的函数调用