UMBRELLA

未雨绸缪,举重若轻

Rust 中,指针按是否有所有权属性可以分为两类,例如 Box<T>String,或者 Vec 具有所有权属性的指针(owning pointers),可以说它们拥有指向的内存,当它们被删除时,指向的内存也会被被释放掉。但是,也有一种非所有权指针,叫做引用(references),它们的存在不会影响指向值的生命周期,在 Rust 中创建引用的行为称之为对值的借用。

要注意的是,引用决不能超过其引用的值的生命周期。必须在代码中明确指出,任何引用都不可能超过它所指向的值的寿命。为了强调这一点,Rust 将创建对某个值的引用称为借用:你所借的东西,最终必须归还给它的所有者。

引用值

在《【Rust】所有权》章节中,我们说到函数传值会转移值得所有权,for 循环也会,例如,对下面的代码,我们在将 table 传递给 show 函数之后,table 就处于未初始化状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::collections::HashMap;
type Table = HashMap<String, Vec<String>>;

fn show(table: Table) {
for (artist, works) in table {
println!("works by {}:", artist); for work in works {
println!(" {}", work);
}
}
}


fn main() {
let mut table = Table::new();
table.insert("Gesualdo".to_string(),
vec!["many madrigals".to_string(),
"Tenebrae Responsoria".to_string()]);
table.insert("Caravaggio".to_string(),
vec!["The Musicians".to_string(),
"The Calling of St. Matthew".to_string()]);
table.insert("Cellini".to_string(),
vec!["Perseus with the head of Medusa".to_string(),
"a salt cellar".to_string()]);
show(table);
}

如果在 show 函数之后,我们再想使用 table 变量就会报错,例如:

1
2
3
...
show(table);
assert_eq!(table["Gesualdo"][0], "many madrigals");

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
阅读全文 »

Rust 被称作表达式语言,在C中,ifswitch 是语句,它们不会产生值,也不能在表达式中间使用。在Rust中,ifmatch可以产生值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
let status = if cpu.temperature <= MAX_TEMP {
HttpStatus::Ok
} else {
HttpStatus::ServerError
};

println!(
"Inside the vat, you see {}.",
match vat.contents {
Some(brain) => brain.desc(),
None => "nothing of interest",
}
);

这解释了为什么Rust没有C的三元运算符(expr1: Expr2: expr3),在 C 中,它类似 if 语句,而在 Rust 中,if 完全可以代替。另外大多数控制流在 C 中是语句,而在 Rust 中是表达式(语句都会以 ; 结束,而表达式没有)。

阅读全文 »

Rust 的错误处理方法非常不同寻常,本节介绍了 Rust 中两种不同类型的错误处理:panicResult

Panic

当程序遇到,数组越界,除0,这样很严重的bug时就会panic,在 Result 上调用 .expect() 遇到错误以及断言失败都会发生panic。还有宏 panic!(),用于在代码发现它出错是,想要直接退出。panic!() 接受可选的 println!() 样式参数,用于构建错误消息。

这些都是程序员的错,但我们都会犯错,当这些不该发生的错误发生时,Rust 可以终止进程。来看一个除0的示例:

1
2
3
4
5
6
7
8
fn main() {
pirate_share(100, 0);
}

fn pirate_share(total: u64, crew_size: usize) -> u64 {
let half = total / 2;
half / crew_size as u64
}

运行这段代码,程序会奔溃的并且打印出调用栈,还提示我们可以设置 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 不需要知道如何展开调用栈,因此这可以减少编译代码的大小。)

阅读全文 »

Crates

Rust 程序是由 crate 组成的,每个 crate 都是一个完整的的单元:单个库或可执行文件的所有源代码,以及任何相关的测试、示例、工具、配置和其他东西。可以使用 cargo build --verbose 查看项目中使用了哪些 crates

通常项目的依赖都是配置在 Cargo.toml 文件中,例如:

1
2
3
4
[dependencies]
num = "0.4"
image = "0.13"
crossbeam = "0.8"

可以通过 cargo buildcargo install 或者 cargo add 下载依赖代码。一旦有了源代码,Cargo 就会编译所有的 crate。它为项目依赖图中的每个 crate 运行一次 rustcRust 编译器)。编译库时,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,可以在磁盘上找到已编译的 crateRust 编译器需要访问这些 .rlib 文件,因为它们包含库的编译代码, Rust 会将该代码静态链接到最终的可执行文件中。 .rlib 还包含类型信息,因此 Rust 可以检查我们在代码中使用的库功能是否确实存在,以及我们是否正确使用它们,它还包含 crate 的公共内联函数、泛型和宏的副本等。

如果编译程序时,Cargo 使用 --crate-type bin,结果将会生成目标平台的二进制可执行文件。

阅读全文 »

Rust 中也有结构体,类似 C/C++ 中的结构体,python 中的 class 以及 javascript 中的对象。Rust 中除了常规的结构体之外,还有 tuple 结构体,单元结构体。

结构体

Rust 中约定包括结构体在内的所有类型都采用驼峰法命名,并且首字母大写,而方法和字段采用蛇形命名,即 _ 连接小写单词。例如:

1
2
3
4
5
/// A rectangle of eight-bit grayscale pixels.
struct GrayscaleMap {
pixels: Vec<u8>,
size: (usize, usize)
}

结构体初始化:

1
2
3
4
5
6
let width = 1024;
let height = 576;
let image = GrayscaleMap {
pixels: vec![0; width * height],
size: (width, height)
};

如果局部变量或者函数参数和字段名称同名,还可以省略字段名称,例如:

1
2
3
4
fn new_map(size: (usize, usize), pixels: Vec<u8>) -> GrayscaleMap {
assert_eq!(pixels.len(), size.0 * size.1);
GrayscaleMap { pixels, size }
}

字段访问采用 . 运算符:

1
2
assert_eq!(image.size, (1024, 576));
assert_eq!(image.pixels.len(), 1024 * 576);

结构体默认只能在当前模块和子模块中使用,如果想要导出结构体需要使用 pub 标识,字段也是同样的道理,如果字段都是私有的,那么只能使用类似 Vec::new 的构造方法来初始化字段:

1
2
3
4
5
/// A rectangle of eight-bit grayscale pixels.
pub struct GrayscaleMap {
pub pixels: Vec<u8>,
pub size: (usize, usize)
}

我们还可以使用相同类型的结构变量去初始化另外一个,使用 .. 运算符,自动填充未显示赋值的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#[derive(Debug)]
struct Person {
name: String,
age: i32,
sex: char,
}

fn main() {
let p1 = Person {
name: "michael".to_string(),
age: 28,
sex: '男',
};

let p2 = Person {
name: "skye".to_string(),
..p1
};

println!("p1: {:?}, pw: {:?}", p1, p2);
}

p2 除了 name 字段是显示赋值的,其他两个字段都是来源于 p1,这段代码运行之后将输出:

p1: Person { name: "michael", age: 28, sex: '男' }, pw: Person { name: "skye", age: 28, sex: '男' }
阅读全文 »

Rust 中,枚举也可以包含数据,甚至是不同类型的数据。例如,RustResult<String, io::Error> 类型是一个枚举,这样的值要么是包含字符串的 Ok值,要么是包含 io::ErrorErr 值。

只要 value 只有一种可能,枚举就很有用。使用它们的代价是你必须安全地访问数据,使用模式匹配就可以完成。Rust 模式有点像正则表达式,它们用于检测一个值是否是想要的,他们也可以将结构体或tuple中的多个字段提取到局部变量中。

枚举

来看一个标准库中枚举示例 std::cmp::Ordering,它有三种可能的值:Ordering::LessOrdering::EqualOrdering::Greater,称为变量或者构造函数:

1
2
3
4
5
6
#[repr(i8)]
pub enum Ordering {
Less,
Equal,
Greater,
}

我们在使用的时候可以直接导入:

1
2
3
4
use std::cmp::Ordering;

//或者
use std::cmp::Ordering::{self, *};

如果导入当前模块的枚举的构造函数可以使用 self

1
2
3
4
5
6
7
enum Pet {
Orca,
Giraffe,
...
}

use self::Pet::*;
阅读全文 »

编程中可能经常遇到要用相同的逻辑处理不同的类型,即使这个类型是还没出世的自定义类型。这种能力对于 Rust 来说并不新鲜,它被称为多态性,诞生于 1970 年代的编程语言技术,到现在为止仍然普遍。Rust 支持具有两个相关特性的多态性:Trait 和 泛型。

TraitRust 对接口或抽象基类的对照实现,它们看起来就像 JavaC# 中的接口:

1
2
3
4
5
6
7
8
trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;

fn flush(&mut self) -> Result<()>;

fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
...
}

FileTcpStream 以及 Vec<u8> 都实现了 std::io::Write,这3个类型都提供了 .write().flush() 等等方法,我们可以使用 write 方法而不用关心它的实际类型:

1
2
3
4
5
6
use std::io::Write;

fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
out.write_all(b"hello world\n")?;
out.flush()
}

&mut dyn Write 的意思是任何实现了 Write 的可变引用,我们可以调用 say_hello 并且给他传递这样一个引用:

1
2
3
4
5
6
7
use std::fs::File;
let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?; // works

let mut bytes = vec![];
say_hello(&mut bytes)?; // also works
assert_eq!(bytes, b"hello world\n");

泛型函数就像 C++ 中模板函数,一个泛型函数或者类型可以用于许多不同类型的值:

1
2
3
4
5
6
7
8
9
/// Given two values, pick whichever one is less.
fn min<T: Ord>(value1: T, value2: T) -> T {

if value1 <= value2 {
value1
} else {
value2
}
}

<T: Ord> 意思是 T 类型必须实现 Ord,这称为边界,因为它设置了 T 可能是哪些类型,编译器为实际使用的每种类型 T 生成自定义机器代码。

阅读全文 »

我们可以为自定义的类型实现加减乘除操作,只要实现标准库的一些 Trait,这称之为运算符重载。下图是可以重载的运算符和需要对应实现的 Trait 列表:

阅读全文 »

Rust 中的 Trait 可以分为三类:

  1. 语言扩展 Trait:主要用于运算符重载,我们可以将常用的运算符使用在自己的类型之中,只要相应的 Trait 即可,例如 EqAddAssignDereDrop 以及 FromInto 等;

  2. 标记类型 Trait:这些 Trait 主要用于绑定泛型类型变量,以表达无法以其他方式捕获的约束,这些包括 SizedCopy

  3. 剩下的主要是一些为解决常遇到的问题,例如:DefaultAsRefAsMutBorrowBorrowMutTryFromTryInto

Drop

Rust 中当一个值离开作用域时就会对它的内存进行清理,但是所有权转移不会,这类似于 C++ 中的析构函数。在 Rust 中我们也可以对析构的过程进行自定义,只要实现 std::ops::Drop 即可,在值需要清理的时候会自动调用 drop 函数,不能显示调用:

1
2
3
pub trait Drop {
fn drop(&mut self);
}

通常不需要实现 std::ops::Drop,除非定义了一个拥有 Rust 不知道的资源的类型。 例如,在 Unix 系统上,Rust 的标准库在内部使用以下类型来表示操作系统文件描述符:

1
2
3
struct FileDesc {
fd: c_int,
}

FileDescfd 字段只是程序完成时应该关闭的文件描述符的编号,c_inti32 的别名。标准库为 FileDesc 实现 Drop 如下:

1
2
3
4
5
6
7
impl Drop for FileDesc {
fn drop(&mut self) {
if self.close_on_drop {
unsafe { ::libc::close(self.fd); }
}
}
}

这里,libc::closeC 库关闭函数的 Rust 名称,Rust 仅能在 unsafe 块中调用 C 函数。

如果一个类型实现了 Drop,它就不能实现 Copy,如果类型可 Copy,则意味着简单的逐字节复制足以生成该值的独立副本,但是在相同的数据上多次调用相同的 drop 方法通常是错误的。

标准库预包含的 drop 函数可以显示删除一个值:

1
2
3
let v = vec![1, 2, 3];

drop(v); // explicitly drop the vector
阅读全文 »

迭代器是产生一系列值的值,通常用于循环操作。Rust 的标准库提供了遍历vector、字符串、哈希表和其他集合的迭代器,还提供了从输入流生成文本行、网络连接、用于多线程之间值传递的迭代器,Rust 的迭代器灵活、富有表现力且高效。

Rust 中,std::iter::Iteratorstd::iter::IntoIterator 是实现迭代器的基础。

1
2
3
4
5
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
...
}

迭代器可以是任何实现了 Iterator 的值,Item 是每次迭代产生的值,next 要么返回 Some(v)v 是下一个值,要么返回 None 表示迭代结束。

想要被迭代的类型也可以实现 std::iter::IntoIterator,它的 into_iter 返回一个迭代器:

1
2
3
4
5
pub trait IntoIterator {
type Item;
type IntoIter: Iterator;
fn into_iter(self) -> Self::IntoIter;
}

我们常用的 for 循环仅仅是先调用 into_iter 生成一个迭代器,然后重复调用迭代器的 next 方法,直到 next 返回 None,预示着迭代结束。

阅读全文 »
0%