【Rust】闭包
很多语言中都有闭包,有的叫匿名函数,有的叫 lambda
函数,用一个最简单的例子演示闭包,例如 sort_by_key
传入的就是一个闭包函数:
1 | struct City { |
很多语言中都有闭包,有的叫匿名函数,有的叫 lambda
函数,用一个最简单的例子演示闭包,例如 sort_by_key
传入的就是一个闭包函数:
1 | struct City { |
Rust
中的 Trait
可以分为三类:
语言扩展 Trait
:主要用于运算符重载,我们可以将常用的运算符使用在自己的类型之中,只要相应的 Trait
即可,例如 Eq
,AddAssign
,Dere
,Drop
以及 From
和 Into
等;
标记类型 Trait
:这些 Trait
主要用于绑定泛型类型变量,以表达无法以其他方式捕获的约束,这些包括 Sized
和 Copy
;
剩下的主要是一些为解决常遇到的问题,例如:Default
,AsRef
,AsMut
,Borrow
,BorrowMut
,TryFrom
和 TryInto
;
Drop
Rust
中当一个值离开作用域时就会对它的内存进行清理,但是所有权转移不会,这类似于 C++
中的析构函数。在 Rust
中我们也可以对析构的过程进行自定义,只要实现 std::ops::Drop
即可,在值需要清理的时候会自动调用 drop
函数,不能显示调用:
1 | pub trait Drop { |
通常不需要实现 std::ops::Drop
,除非定义了一个拥有 Rust
不知道的资源的类型。 例如,在 Unix
系统上,Rust
的标准库在内部使用以下类型来表示操作系统文件描述符:
1 | struct FileDesc { |
FileDesc
的 fd
字段只是程序完成时应该关闭的文件描述符的编号,c_int
是 i32
的别名。标准库为 FileDesc
实现 Drop
如下:
1 | impl Drop for FileDesc { |
这里,libc::close
是 C
库关闭函数的 Rust
名称,Rust
仅能在 unsafe
块中调用 C
函数。
如果一个类型实现了 Drop
,它就不能实现 Copy
,如果类型可 Copy
,则意味着简单的逐字节复制足以生成该值的独立副本,但是在相同的数据上多次调用相同的 drop
方法通常是错误的。
标准库预包含的 drop
函数可以显示删除一个值:
1 | let v = vec![1, 2, 3]; |
我们可以为自定义的类型实现加减乘除操作,只要实现标准库的一些 Trait
,这称之为运算符重载。下图是可以重载的运算符和需要对应实现的 Trait
列表:
编程中可能经常遇到要用相同的逻辑处理不同的类型,即使这个类型是还没出世的自定义类型。这种能力对于 Rust
来说并不新鲜,它被称为多态性,诞生于 1970
年代的编程语言技术,到现在为止仍然普遍。Rust
支持具有两个相关特性的多态性:Trait
和 泛型。
Trait
是 Rust
对接口或抽象基类的对照实现,它们看起来就像 Java
或 C#
中的接口:
1 | trait Write { |
File
,TcpStream
以及 Vec<u8>
都实现了 std::io::Write
,这3
个类型都提供了 .write()
,.flush()
等等方法,我们可以使用 write
方法而不用关心它的实际类型:
1 | use std::io::Write; |
&mut dyn Write
的意思是任何实现了 Write
的可变引用,我们可以调用 say_hello
并且给他传递这样一个引用:
1 | use std::fs::File; |
泛型函数就像 C++
中模板函数,一个泛型函数或者类型可以用于许多不同类型的值:
1 | /// Given two values, pick whichever one is less. |
<T: Ord>
意思是 T
类型必须实现 Ord
,这称为边界,因为它设置了 T
可能是哪些类型,编译器为实际使用的每种类型 T
生成自定义机器代码。
在 Rust
中,枚举也可以包含数据,甚至是不同类型的数据。例如,Rust
的 Result<String, io::Error>
类型是一个枚举,这样的值要么是包含字符串的 Ok
值,要么是包含 io::Error
的 Err
值。
只要 value
只有一种可能,枚举就很有用。使用它们的代价是你必须安全地访问数据,使用模式匹配就可以完成。Rust
模式有点像正则表达式,它们用于检测一个值是否是想要的,他们也可以将结构体或tuple
中的多个字段提取到局部变量中。
来看一个标准库中枚举示例 std::cmp::Ordering
,它有三种可能的值:Ordering::Less
, Ordering::Equal
和 Ordering::Greater
,称为变量或者构造函数:
1 | #[repr(i8)] |
我们在使用的时候可以直接导入:
1 | use std::cmp::Ordering; |
如果导入当前模块的枚举的构造函数可以使用 self
:
1 | enum Pet { |
Rust
中也有结构体,类似 C/C++
中的结构体,python
中的 class
以及 javascript
中的对象。Rust
中除了常规的结构体之外,还有 tuple
结构体,单元结构体。
Rust
中约定包括结构体在内的所有类型都采用驼峰法命名,并且首字母大写,而方法和字段采用蛇形命名,即 _
连接小写单词。例如:
1 | /// A rectangle of eight-bit grayscale pixels. |
结构体初始化:
1 | let width = 1024; |
如果局部变量或者函数参数和字段名称同名,还可以省略字段名称,例如:
1 | fn new_map(size: (usize, usize), pixels: Vec<u8>) -> GrayscaleMap { |
字段访问采用 .
运算符:
1 | assert_eq!(image.size, (1024, 576)); |
结构体默认只能在当前模块和子模块中使用,如果想要导出结构体需要使用 pub
标识,字段也是同样的道理,如果字段都是私有的,那么只能使用类似 Vec::new
的构造方法来初始化字段:
1 | /// A rectangle of eight-bit grayscale pixels. |
我们还可以使用相同类型的结构变量去初始化另外一个,使用 ..
运算符,自动填充未显示赋值的字段:
1 | #[derive(Debug)] |
p2
除了 name
字段是显示赋值的,其他两个字段都是来源于 p1
,这段代码运行之后将输出:
p1: Person { name: "michael", age: 28, sex: '男' }, pw: Person { name: "skye", age: 28, sex: '男' }
Crates
Rust
程序是由 crate
组成的,每个 crate
都是一个完整的的单元:单个库或可执行文件的所有源代码,以及任何相关的测试、示例、工具、配置和其他东西。可以使用 cargo build --verbose
查看项目中使用了哪些 crates
。
通常项目的依赖都是配置在 Cargo.toml
文件中,例如:
1 | [dependencies] |
可以通过 cargo build
,cargo install
或者 cargo add
下载依赖代码。一旦有了源代码,Cargo
就会编译所有的 crate
。它为项目依赖图中的每个 crate
运行一次 rustc
(Rust
编译器)。编译库时,Cargo
使用 --crate-type lib
选项。这告诉 rustc
不要寻找 main()
函数,而是生成一个 .rlib
文件,其中包含可用于创建二进制文件和其他 .rlib
文件的编译代码。例如:
1 | rustc --crate-name num --edition=2018 /Users/fudenglong/.cargo/registry/src/mirrors.ustc.edu.cn-61ef6e0cd06fb9b8/num-0.4.0/src/lib.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type lib --emit=dep-info,metadata,link -C embed-bitcode=no -C split-debuginfo=unpacked -C debuginfo=2 --cfg 'feature="default"' --cfg 'feature="num-bigint"' --cfg 'feature="std"' -C metadata=b84820de50dc7f78 -C extra-filename=-b84820de50dc7f78 --out-dir /Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps -L dependency=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps --extern num_bigint=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_bigint-bd772250e89d4bb9.rmeta --extern num_complex=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_complex-d3fd80f953e1ac52.rmeta --extern num_integer=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_integer-7ff0466209086397.rmeta --extern num_iter=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_iter-2b149e71dbad2afc.rmeta --extern num_rational=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_rational-1686ad6eb82c18d4.rmeta --extern num_traits=/Users/fudenglong/WORKDIR/rust/mandelbrot/target/debug/deps/libnum_traits-deaceb32c41a04f1.rmeta --cap-lints allow |
对于每个 rustc
命令,Cargo
都会传递 --extern
选项,给出 crate
将使用的每个库的文件名。这样,当 rustc
看到像 use num::bigint::BigInt;
这样的代码行时,它可以确定 num
是另一个 crate
的名称,并且通过 Cargo
,可以在磁盘上找到已编译的 crate
。Rust
编译器需要访问这些 .rlib
文件,因为它们包含库的编译代码, Rust
会将该代码静态链接到最终的可执行文件中。 .rlib
还包含类型信息,因此 Rust
可以检查我们在代码中使用的库功能是否确实存在,以及我们是否正确使用它们,它还包含 crate
的公共内联函数、泛型和宏的副本等。
如果编译程序时,Cargo
使用 --crate-type bin
,结果将会生成目标平台的二进制可执行文件。
Rust
的错误处理方法非常不同寻常,本节介绍了 Rust 中两种不同类型的错误处理:panic
和 Result
。
Panic
当程序遇到,数组越界,除0
,这样很严重的bug
时就会panic
,在 Result
上调用 .expect()
遇到错误以及断言失败都会发生panic
。还有宏 panic!()
,用于在代码发现它出错是,想要直接退出。panic!()
接受可选的 println!()
样式参数,用于构建错误消息。
这些都是程序员的错,但我们都会犯错,当这些不该发生的错误发生时,Rust
可以终止进程。来看一个除0
的示例:
1 | fn main() { |
运行这段代码,程序会奔溃的并且打印出调用栈,还提示我们可以设置 RUST_BACKTRACE=full
获得更多的信息:
/Users/fudenglong/.cargo/bin/cargo run --color=always --package mandelbrot --bin mandelbrot
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/mandelbrot`
thread 'main' panicked at 'attempt to divide by zero', src/main.rs:7:5
stack backtrace:
0: rust_begin_unwind
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/std/src/panicking.rs:584:5
1: core::panicking::panic_fmt
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/core/src/panicking.rs:142:14
2: core::panicking::panic
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/core/src/panicking.rs:48:5
3: mandelbrot::pirate_share
at ./src/main.rs:7:5
4: mandelbrot::main
at ./src/main.rs:2:5
5: core::ops::function::FnOnce::call_once
at /rustc/4ca19e09d302a4cbde14f9cb1bc109179dc824cd/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Process finished with exit code 101
线程之间的 panic
是相互独立的,也可以调用 std::panic::catch_unwind()
捕获异常,并且让程序执行。默认发生 panic
时会展开调用栈,此外有两种情况 Rust
不会尝试展开调用栈:
如果 .drop()
方法触发了第二次恐慌,而 Rust
在第一次之后仍在尝试清理,这被认为是致命的,Rust
停止展开并中止整个进程;
Rust
的恐慌行为是可定制的。如果使用 -C panic=abort
编译,程序中的第一个 panic
会立即中止进程。(使用这个选项,Rust
不需要知道如何展开调用栈,因此这可以减少编译代码的大小。)
Rust
被称作表达式语言,在C
中,if
和switch
是语句,它们不会产生值,也不能在表达式中间使用。在Rust
中,if
和 match
可以产生值。例如:
1 | let status = if cpu.temperature <= MAX_TEMP { |
这解释了为什么Rust
没有C
的三元运算符(expr1: Expr2: expr3)
,在 C
中,它类似 if
语句,而在 Rust
中,if
完全可以代替。另外大多数控制流在 C
中是语句,而在 Rust
中是表达式(语句都会以 ;
结束,而表达式没有)。
在 Rust
中,指针按是否有所有权属性可以分为两类,例如 Box<T>
,String
,或者 Vec
具有所有权属性的指针(owning pointers
),可以说它们拥有指向的内存,当它们被删除时,指向的内存也会被被释放掉。但是,也有一种非所有权指针,叫做引用(references)
,它们的存在不会影响指向值的生命周期,在 Rust
中创建引用的行为称之为对值的借用。
要注意的是,引用决不能超过其引用的值的生命周期。必须在代码中明确指出,任何引用都不可能超过它所指向的值的寿命。为了强调这一点,Rust
将创建对某个值的引用称为借用:你所借的东西,最终必须归还给它的所有者。
在《【Rust】所有权》章节中,我们说到函数传值会转移值得所有权,for
循环也会,例如,对下面的代码,我们在将 table
传递给 show
函数之后,table
就处于未初始化状态:
1 | use std::collections::HashMap; |
如果在 show
函数之后,我们再想使用 table
变量就会报错,例如:
1 | ... |
Rust
编译器提示变量 table
已经不可用,show
函数的调用已经转移 table
的所有权:
error[E0382]: borrow of moved value: `table`
--> src/main.rs:24:16
|
13 | let mut table = Table::new();
| --------- move occurs because `table` has type `HashMap<String, Vec<String>>`, which does not implement the `Copy` trait
...
23 | show(table);
| ----- value moved here
24 | assert_eq!(table["Gesualdo"][0], "many madrigals");
| ^^^^^ value borrowed here after move