Learning Rust - 1
学习微软Rust教程的笔记。
Rust的特点
Rust 是现有系统软件语言(如 C 和 C++)的一种安全替代语言。 与 C 和 C++ 一样,Rust 没有大型运行时或垃圾回收器,这几乎与所有其他现代语言形成了鲜明对比。 但是,与 C 和 C++ 不同的是,Rust 保证了内存安全。 Rust 可以避免很多与在 C 和 C++ 中遇到的内存使用错误相关的 bug。
Rust 有以下优点,非常适合各种应用程序:
- 类型安全:编译器可确保不会将任何操作应用于错误类型的变量。
- 内存安全:Rust 指针(称为“引用”)始终引用有效的内存。
- 无数据争用:Rust 的 borrow 检查器通过确保程序的多个部分不能同时更改同一值来保证线程安全。
- 零成本抽象:Rust 允许使用高级别概念,例如迭代、接口和函数编程,将性能成本控制在最低,甚至不会产生成本。 这些抽象的性能与手工编写的底层代码一样出色。
- 最小运行时:Rust 具有非常小的可选运行时。 为了有效地管理内存,此语言也不具有垃圾回收器。 在这一点上,Rust 非常类似于 C 和 C++ 之类的语言。
- 面向裸机:Rust 可以用于嵌入式和“裸机”编程,因此适合用于编写操作系统内核或设备驱动程序。
了解Cargo
Cargo是Rust语言的生成工具和依赖管理器,Cargo为管理Rust程序带来了很多便利。
Cargo 可以为你做许多事情,包括:
- 使用
cargo new
命令创建新的项目模板。 - 使用
cargo build
编译项目。 - 使用
cargo run
命令编译并运行项目。 - 使用
cargo test
命令测试项目。 - 使用
cargo check
命令检查项目类型。 - 使用
cargo doc
命令编译项目的文档。 - 使用
cargo publish
命令将库发布到 crates.io。
尝试Cargo
首先使用Cargo创建一个新的Rust工程:
|
|
创建完工程之后,使用VSCode打开刚才创建的文件夹,可以看到cargo已经自动为你生成了cargo.toml
和src/main.rs
两个文件。
- Cargo.toml 是 Rust 代码库的配置文件,用于管理依赖版本等。如果你有其他语言的经验,可以类比
package.json
或者go.mod
等 - src 子目录中的 main.rs 文件为当前工程的主入口文件,里面的
fn main()
即为主入口函数
cargo init
生成的工程是一个可运行的Rust的HelloWorld工程。下面还有一些命令可以尝试:
运行当前工程:
|
|
编译当前工程:
|
|
编译当前工程(发布使用):
|
|
编译完成之后,你可以在target/debug
和target/release
目录下看到编译出的可执行文件。
语言基础
变量
声明变量
使用关键字let
|
|
变量的可变性
和一般的语言不一样,默认情况下,Rust的变量是不可变的:
|
|
如果想要变量可变,那么需要使用关键字mut
:
|
|
为什么要默认变量是不可变的呢?《The Rust Programming Language》有如下解释:
如果一部分代码假设一个值永远也不会改变,而另一部分代码改变了这个值,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 的起因难以跟踪,尤其是第二部分代码只是 有时 会改变值。
Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要追踪一个值如何和在哪可能会被改变,从而使得代码易于推导。
不过可变性也是非常有用的。变量只是默认不可变;正如在第二章所做的那样,你可以在变量名之前加
mut
来使其可变。除了允许改变值之外,mut
向读者表明了其他代码将会改变这个变量值的意图。
不可变变量和常量的区别:
总结一下:
- 不允许对常量使用
mut
。常量不光默认不能变,它总是不能变。 - 声明常量使用
const
关键字而不是let
,并且 必须 注明值的类型 - 常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值
- 常量可以在任何作用域中声明,包括全局作用域
变量隐藏
可以重复使用let声明同名的变量,这样的话变量名会被绑定在新的值上面,旧的变量就被“隐藏”了。需要注意的是,旧变量仍然存在。
|
|
数据类型
Rust是静态类型语言。在声明变量时,编译器会自动推断变量类型,但是也可以使用:
(类似typescript)手动指定变量类型:
|
|
数字类型
分为整数、浮点数。具体表见:数据类型 - Rust 程序设计语言
默认的整型和浮点数类型为:i32
和f64
布尔类型
true
or false
字符
字符类型为char
,使用单引号括住:
|
|
字符串
Rust中,有好几种字符串类型:&str
(字符串引用), String
(堆上字符串)等。具体使用还是有区别的。可以参考:字符串 - Rust 程序设计语言。现在,可以先简单地认为 String
是可随程序运行而更改的文本数据。 &str
引用是文本数据的不可变视图,不会随着程序运行而改变。
元组tuple
元组是固定长度的分组,使用(<value1>, <value2>, ...)
表示。每个value的类型可以不一样。获取元组中的元素,可使用tuple.index
|
|
控制流
if else
语法很简单:
|
|
用的时候,ifelse
块还可以充当表达式:
|
|
复杂数据结构
结构体
使用关键字struct
定义,结构类型名称采用大写形式。
Rust 支持三种结构类型:经典结构、元组结构和单元结构。 这些结构类型支持使用各种方式对数据进行分组和处理。
- “经典 C 结构”最为常用。 结构中的每个字段都具有名称和数据类型。 定义经典结构后,可以使用语法
<struct>.<field>
访问结构中的字段。- 元组结构类似于经典结构,但字段没有名称。 要访问元组结构中的字段,请使用索引元组时所用的语法:
<tuple>.<index>
。 与元组一样,元组结构中的索引值从 0 开始。- “单元结构”最常用作标记。 我们将在了解 Rust 的特征功能时,将深入了解单元结构之所以实用的原因。
以下代码显示三种结构类型变体的示例定义:
1 2 3 4 5 6 7 8
// Classic struct with named fields struct Student { name: String, level: u8, pass: bool } // Tuple struct with data types only struct Grades(char, char, char, char, f32); // Unit struct struct Unit;
结构体实例化:
|
|
当然,我们也可以为结构体定义成员函数,使用impl
关键字即可
|
|
枚举
关键字enum
。需要注意的是,Rust的枚举中,每个值可以有不同的类型。这样的话,在使用某个枚举类型时,必须接受其下面所有值的类型:
|
|
一般来说,我们不直接在枚举里面定义一个复杂的结构,而是在外面定义好相应的结构体之后,在枚举里面使用:
|
|
在使用枚举时,采用运算符::
来指定具体的枚举值:
|
|
Rust函数
Rust的函数使用关键字fn
声明:
|
|
函数的返回值由->
确定,参数填在()
里面,使用:
指定类型:
|
|
在函数体中,大多数的语句是分号;
结尾的。如果不是分号结尾的语句,则有可能是函数的返回值!
|
|
###集合类型
Rust中自带了一些常见的集合类型:数组、向量、HashMap等
数组
Rust中的数组是具有相同数据类型和固定长度的对象集合。定义和索引:
|
|
向量
Rust中的向量是长度可变的相同数据类型的对象集合。向量声明:
|
|
注意,代码中的vec!
是一个宏,而Vec::new()
为调用Vec
中的new()
方法
索引、添加和删除值:
|
|
HashMap
Rust中的HashMap定义在标准库中,因此在使用前需要使用
|
|
引入。use
关键字和其他语言中的import
类似,用于导入。
初始化,添加、获取、删除元素:
|
|
循环
Rust中,提供了三种循环:loop
, while
, for
loop
Rust中的loop
为无限循环,只能使用break
跳出。使用break
时,还能顺带返回一个值:
|
|
如果loop
中有多个break
,那么每处返回的类型需要一致。
while
和其他语言的while
没什么区别:
|
|
for
对于数组之类的数据结构,可以用for <value> in <list>
|
|
另外一种常见的用法是for idx in a...b
,其中,a...b
表示从a
开始,步长为1迭代到b
(不包含b):
|
|
错误处理
panic
panic 是 Rust 中最简单的错误处理机制。发生panic时,Rust会输出一条错误消息、清理资源,然后退出程序。可以调用panic!
宏来使当前进程panic。一般来说,只有在程序遇到无论如何都恢复不了的错误时使用:
|
|
Option
在Rust中,使用Option<T>
处理可能为空的值。其他语言中,会有null
, nil
, None
之类的值表示空值,在Rust中,除了与其他语言(比如C)交互时,其他情况下基本都不会使用null
。
Option<T>
是一个带泛型的枚举:
|
|
那什么时候会用到Option呢?下面就是一个例子:
在前面的单元中,我们提到尝试访问矢量的不存在的索引会导致程序
panic
,但你可以通过使用Vec::get
方法(该方法返回Option
类型,而不是 panic)来避免这种情况。 如果该值存在于指定的索引处,系统会将其包装在Option::Some(value)
变体中。 如果索引超出界限,则它会改为返回Option::None
值。
1 2 3 4 5 6 7 8 9
let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"]; // pick the first item: let first = fruits.get(0); println!("{:?}", first); // Some("banana") // pick the 99th item, which is non-existent: let non_existent = fruits.get(99); println!("{:?}", non_existent); // None
Rust提供了多种方法来处理Option值:
-
match
:类似其他语言的switch,针对Option中的每种情况分别处理1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
match Option<T> { Some(value) => { ... } None => {... } } // 下面是一个实例 let fruits = vec!["banana", "apple", "coconut", "orange", "strawberry"]; for &index in [0, 2, 99].iter() { // fruits.get(index)返回一个Option<String> match fruits.get(index) { Some(&"coconut") => println!("Coconuts are awesome!!!"), Some(fruit_name) => println!("It's a delicious {}!", fruit_name), None => println!("There is no fruit! :("), } }
-
if let
:如果只关心Option中的某一个特定值1 2 3 4 5 6
let a_number: Option<u8> = Some(7); // 如果我只关心这个数字为7的情况,此时适合使用if let if let Some(7) = a_number { println!("That's my lucky number!"); }
-
unwrap()
/expect()
:直接获取Option中的Some值,但是Option为None,会直接panic。区别在于expect()
可以自定义panic的报错信息1 2 3 4 5 6
let a: i32 = Some(1).unwrap(); let empty_gift: Option<&str> = None; empty_gift.unwrap(); // panic! empty_gift.expect("the gift is none!"); // panic with given message! // thread 'main' panicked at 'the gift is none!'
-
unwrap_or(<default_value>)
:如果Option为None,则使用默认值
Result
Rust的Option
提供了对空值的处理,而对于可能出现的程序的错误,Rust提供了Result<T, E>
枚举来处理:
|
|
Result
枚举也非常好理解:要么程序运行OK,返回一个T类型的值;要么程序运行Err,返回一个E类型的错误。和Option
类似,Result
也提供了unwrap()
和expect()
方法直接获取OK()
包的值。如果返回的是Err
,则会panic。
能够用于Option
的match
和if let
,也可以用于Result