在 Rust 日趋流行的今日,我这个老顽固在部门同事的推动下,艰难地学习着 Rust。

本文主要总结自 Rust 圣经官方教程 Rust Book 中文翻译

基本语法

Rust 支持类型推理。用 let 指定不可变变量,let mut 指定可变变量,const 指定(全局)常量,static mut 指定(全局)变量/静态变量。静态变量的内存地址保持不变,只能通过 unsafe 语句进行访问和修改。

1
2
3
4
5
6
7
8
let x = 5;
let mut y = 5;
y = 6;
let z = {
let x = 3;
x + 1
};
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Rust 支持变量遮蔽(shadow),遮蔽前后类型可以不同,作用域结束后遮蔽自动结束。

1
2
3
4
5
6
let x = 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {}", x); // 2
}
println!("The value of x is: {}", x); // 1

函数参数和返回值(如果有)必须指定类型。函数定义的位置不作要求。如果没有返回值,默认返回 ()。特殊地,如果返回值表示为 !,表示这是一个永不返回的函数(diverge function),常用于输出 panic。

1
2
3
fn five() -> i32{
5
}

if 判断的表达式必须返回布尔类型,表达式无须强制套小括号。if 表达式返回的结果必须同类型。

1
let number = if condition { 5 } else { 6 };

loopwhilefor 来表示循环。支持 continuebreakbreak 后可添加返回值来构建表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
while number != 0 {
println!("{}!", number);
number -= 1;
}
for element in (1..4).rev() {
println!("the value is: {}", element);
}

基本数据类型

Rust 是 强类型语言。基本类型(Scalar Type)如下:

  • 整型:i8, i16, i32, i64, i128, isize; u8, u16, u32, u64, u128, usize。默认是 i32。整型溢出在 debug 模式下会报 panic 错误,release 模式下会用补码包裹。
  • 浮点类型:f32, f64。默认是 f64。与 Nan 交互都会变成 NaNNaN 参与比较会 panic。
  • 布尔类型:bool。大小为 1 字节。
  • 字符类型:char。大小为 4 字节,Unicode 编码。用单引号表示。

序列(Range)用来生成连续的数值,默认前闭后开。如 1..51..=5。常用于 for...in...

元组(Tuple)可以将多个不同类型的值复合在一起,也可用 struct 来定义。用模式匹配或 . 和下标来索引。

单元类型(unit type)是一种特殊的元组,只有一个值 (),该值被称为单元值(unit value)。如果表达式或函数不返回任何其他值,就隐式地返回单元值。如果 map 不关心 value,也可以用 () 占位,不占用任何内存。

1
2
3
4
5
let tuple = (500, 6.4, 1);
let (x, y, z) = tup;
let six_point_four = tuple.1;
struct Point(i32, i32, i32);
let origin = Point(0, 0, 0);

字符串(String)不定长,存储在堆里,本质上是 Vec<u8> 的封装。注意字面量会被保存在栈中。

字符串的元素用 UTF-8 编码。因为每个元素占用的字节数可能不同,字符串不支持下标索引以防止歧义发生。

  • .chars() 返回字符串的实际字符集合。用 .chars().count() 统计实际字符个数,复杂度线性。
  • .bytes() 返回每一个原始字节信息。用 .len() 返回实际字节数,复杂度 O(1)O(1)
1
2
3
4
5
6
7
8
let mut s1 = String::from("hello");
s1.push_str(", world!"); // push_str 的参数是 slice,即不获取其所有权
s1.push('2'); // push 的参数是单个字符
let end_char = s1.pop(); // 删除最后一个字符并返回
s1.remove(0); // 删除指定位置的字符,索引必须是该字符开头
let mut s2 = s1 + "233"; // '+' 会调用 add,会抢走左边变量的所有权
let s3 = String::from("tic");
let s4 = format!("{}{}", s2, s3); // format!

切片(Slice)可以部分或全部地引用字符串(str),前闭后开。若切片错误地切断了某个 UTF-8 的元素会报错。

1
2
3
4
5
6
7
8
9
10
11
let s = String::from("hello");
let slice = &s[0..2]/&s[..2]/&s[1..]/&s[..];
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}

数组(Array)只能组合同类型的元素,长度不可变。数组内容会被分配至栈空间中。数组也可以进行切片操作,且我们往往用引用的形式去获取切片,因为切片本身大小不确定,而引用的大小是确定的。

1
2
3
4
5
let months = ["January", "February", "March"];
let a: [i32; 5] = [1, 2, 3, 4, 5]
let a: [3; 5]; // [3, 3, 3, 3, 3]
let first = a[0];
let slice: &[i32] = &a[1..3];

类型转换

FromInto 是 Rust 里两个相关联的 trait。From 语义是“怎么根据另一种类型生成自己”,Into 反之。如果实现了 From,也就免费获得了 Into。使用 into 时如果编译器不能推断,就要指明具体转换到的类型。

1
2
3
4
5
6
7
8
9
10
11
12
struct Number {value: i32}
impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}

fn main() {
let int = 5;
let num1 = Number::from(30);
let num2: Number = int.into();
}

TryFromTryInto 也是类似的类型转换 trait,通常用于易出错的转换,返回值是 Result 型。

若要把任何类型转换成 String,需实现 ToString trait。建议直接实现 fmt::Display trait,还能用来打印。

如果目标类型实现了 FromStr trait,就可以用 parse() 把字符串转化成该类型。

1
2
3
4
5
6
7
impl fmt::Display for Number {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Number of radius {}", self.value)
}
}
let parsed: i32 = "5".parse().unwrap();
let turbo_parsed = "10".parse::<i32>().unwrap();

结构体

结构体用 struct 定义,用 . 索引内容。结构体必须整个可变或整个不可变。

  • 变量和字段同名时可以简化初始化。
  • 把结构体里某个字段的所有权转移出去后,就无法访问该字段,但仍能正常访问其他字段。
  • .. 来根据另一个实例创建新实例。注意是简单的浅拷贝,堆元素的拷贝会使原实例对应字段失效。
  • impl 来定义结构体的专属方法。如果作用于某个实例,第一个参数用 &self 表示。调用结构体方法时,会根据方法的定义进行自动引用和解引用(automatic referencing and dereferencing)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct User {
username: String,
sign_in_count: u64,
}
let user1 = User {
username: String::from("someusername123"),
sign_in_count: 1,
};
let user2 = User {
sign_in_count: 2,
..user1
};
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}

在结构体前加上 #[derive(Debug)],可以用 {:?}{:#?} 来格式化输出结构体的内容。

元组结构体是一种特殊的结构体,字段不需要命名,如 struct Color(i32, i32, i32)

枚举类型和匹配

枚举(Enum)可以为每个成员绑定一个具体的数据类型。可以像结构体一样用 impl 定义方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
...
}
}
let m = Message::Write(String::from("hello"));
m.call();
enum Option<T> {
Some(T),
None,
}

Match 匹配的模式执行相应代码。Match 必须穷尽所有情况,或者使用 Other/_ 表示其他情况。

| 表示并列情况的等同处理,../..= 列举一个区间。如果是结构体的匹配,用 .. 表示忽略剩余字段。

匹配守卫(match guard)是位于某个分支后的额外 if 条件,只有当这个 if 也成立才会执行对应语句。

1
2
3
4
5
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}

If let 是 Match 的语法糖。else 相当于 Match 里的 _,如果没有 else 也是允许的。

1
2
3
4
5
6
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

错误处理

panic 指的是不可恢复的错误。panic 出现时 Rust 的默认行为是展开(unwinding),回溯栈并清理数据。可以在配置项里增加 panic = 'abort',这样在出现 panic 后直接终止(abort),由操作系统清理内存。

Result<T, E> 处理可恢复的错误,它是个枚举类型,Ok(T)Err(E)

  • .unwrap() 可以解包 Result 直接获得其返回值,若产生 Error 会转化成 panic。
  • .expect() 与上述类似,只是会把 panic 错误信息替换成给出的字符串参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}

调用函数的末尾加上 ? 后可以简单地起到错误传播的作用。

1
2
3
4
5
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}

作用域和生命周期

Rust 中的每一个值都有一个对应的所有者(owner)的变量。当变量离开其作用域时,值会被丢弃。

  • 对未实现 copy trait 的类型浅拷贝时会转移(move),前者不能再被使用;用 .clone() 做到深拷贝。
  • 未实现 copy trait 的值在函数传参时如果不用 & 标记,会把所有者转移至函数内部,原变量不再能使用。
  • & 表示引用(borrow),不发生所有者的转移;用 &mut 表示可变引用,可以改变值但是不发生所有者的转移。对于一个值,要么只能有一个可变引用,要么只能有多个不可变引用。
  • Rust2018 引入非词法作用域生命周期(Non-Lexical Lifetimes,简称 NLL)特性。如果某个引用在以后的代码中不再被使用,它会在最后一句相关的语句执行之后提前被消除。即以下代码能正确运行。

实现 copy trait 的类型有:整型、布尔型、浮点型、字符型、元素都实现 copy trait 的元组、不可变引用。

1
2
3
4
5
6
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
let r3 = &mut s;
println!("{}", r3);

Rust 可以标注引用的生命周期(lifetime),以 ' 开头,通常是较短的小写字母,如 'a。生命周期标注会告诉 Rust,多个引用的泛型生命周期参数如何相互联系。

函数参数如果带有引用,需要对其进行生命周期标注。泛型生命周期参数声明在函数名和参数列表间的尖括号中。下面的例子暗示了 longest 返回的引用的生命周期与传入两个引用的较小者保持一致(不标注就会编译失败)。

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

注意,在函数中使用生命周期标注时,这些标注仅出现在函数签名中,而不存在于函数体中的任何代码中。

结构体的每个引用也要标注。下例暗示 ImportantExcerpt 实例不能比其 part 字段引用的字符串存在得更久。

1
2
3
4
5
6
7
8
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt { part: first_sentence };
}

结构体方法里的引用也要标注。下例展示了两组(执行生命周期省略规则后)合法的方法标注示例。

1
2
3
4
5
6
7
8
9
10
11
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

早期 Rust 要求每个引用都标注明确的生命周期,后来编译器会执行生命周期省略规则(lifetime elision rules)。Rust 有一条输入生命周期(input lifetimes)省略规则和两条输出生命周期(output lifetimes)省略规则。它会根据这三条规则对当前代码进行不完整推断,如果推断完后仍然残留未知生命周期的参数就会报错。

  1. 默认每一个是引用的参数都有它自己默认的生命周期参数。
  2. 如果输入生命周期参数只有一种,那么它会被赋予给所有输出生命周期参数。
  3. 如果方法有多个输入生命周期参数且其中一个参数是 &self&mut self(说明是对象的方法),那么所有输出生命周期参数会被赋予 self 的生命周期。

静态生命周期用 'static 标注,其生命周期能够存活于整个程序期间。字符串字面量默认是静态生命周期。

1
let s: &'static str = "I have a static lifetime.";

泛型和 trait

函数、结构体和结构体方法都支持泛型。编译时进行单态化(monomorphization),即泛型不影响效率。

1
2
3
4
5
6
7
8
9
10
11
12
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}

trait 类似于其他语言的接口(interfaces)。使用 trait bounds 指定泛型是任何拥有特定行为的类型。

定义 trait 时可以只有声明,或者带上默认的方法。注意,无法从相同方法的重载实现中调用默认方法。

1
2
3
4
5
6
7
8
9
10
11
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}

可以在某个函数里定义 trait。

  • : 指定参数要求实现的 trait,多个 trait 之间用 + 连接,不同泛型之间用 , 连接。
  • 也可以用 impl 简化声明,不过默认对应参数是任意类型。
  • 还可以用 where 语法把参数里的 trait bound 更清晰地提取出来。
1
2
3
4
5
6
pub fn notify(item1: impl Summary, item2: impl Summary)
pub fn notify<T: Summary, U: Summary>(item1: T, item2: U)

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone, U: Clone + Debug

可以使用 trait bound 为泛型有条件地实现方法。

1
2
3
4
5
6
7
8
9
struct Pair<T> {
x: T,
y: T,
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
println!("x = {}, y = {}", self.x, self.y);
}
}

泛型类型参数、trait bounds 和生命周期可以混合在一起使用,如:

1
2
3
4
5
6
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {x} else {y}
}

Derive 关键词可以作用于 struct 定义前,等价于在实际代码里增加对应的 trait。

1
2
3
4
5
#[derive(PartialEq, Debug)]
struct Foo<T> {
a: i32,
b: T,
}

例如, Debug 实现了 {:?} 后的格式化输出,PartialEq 等效于增加了以下代码:

1
2
3
4
5
6
7
8
impl<T: PartialEq> PartialEq for Foo<T> {
fn eq(&self, other: &Foo<T>) -> bool {
self.a == other.a && self.b == other.b
}
fn ne(&self, other: &Foo<T>) -> bool {
self.a != other.a || self.b != other.b
}
}

Vector 和 Map

vec<T> 是可变长数组,只能存放相同类型的元素。可以用 vec! 宏直接构造 vector,自动进行类型推理。

1
2
3
let v: Vec<i32> = Vec::new();
let mut v = vec![1, 2, 3];
v.push(4);

可以用索引或者 .get() 的方式访问 vector 内的元素。前者在越界时会报 panic 错误,后者是 Optional<&T> 类型。注意,如果直接使用下标访问且 vector 中的元素没有实现 copy trait,所有权会 move 到使用者那里。

1
2
3
4
5
let third: &i32 = &v[2];
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}

获得某个下标的引用后禁止对 vector 进行 push,因为 push 要获取整个 vector 的可变引用。

可以用 for...in... 简化对 vector 的遍历。可以选择不可变引用遍历和可变引用遍历。

1
2
3
4
5
6
7
8
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}

可以用枚举类型来变相让 vector 支持不同类型。vector 会为每个元素开 max(diff_types) 的空间。

1
2
3
4
5
6
7
8
9
10
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];

HashMap 用 use std::collections::HashMap 引用。键和值的类型必须各自相同。

  • .insert(key, value):插入一组元素,若存在则会覆盖。
  • .entry(key).or_insert(value):如果 key 不存在,就插入指定 value。返回 value 的可变索引。

对于基本类型这种实现了 copy trait 的类型,其值会直接拷贝至 map,否则会转移所有权至 map。

1
2
3
4
5
6
7
8
9
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
let score = scores.get(&team_name);
for (key, value) in &scores {
println!("{}: {}", key, value);
}

闭包和迭代器

定义闭包时,|| 里定义参数列表,后接 {} 定义函数体。函数参数和返回值的类型可以省略,按第一次使用时的情况自动推理。闭包和函数最大的不同时它可以自动捕捉作用域里的变量。

1
2
3
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;

可以用结构体去承接一个闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Cacher<T> where T: Fn(u32) -> u32
{
calculation: T,
value: Option<u32>,
}
impl<T> Cacher<T> where T: Fn(u32) -> u32
{
fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, }}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
},
}
}
}

闭包可以通过三种方式捕获其环境,对应函数的三种获取参数的方法:获得所有权、可变借用和不可变借用。

方式 原理
FnOnce(self) 闭包结束后捕获的变量会被释放。只能调用一次。所有闭包都默认实现该 trait。
FnMut(&mut self) 获得作用域变量的可变引用。可以被调用多次。
Fn(&self) 获得作用域变量的不可变引用。可以被调用多次。

在闭包参数前加 move 关键词后,可以强制闭包获取作用域里变量的所有权。

迭代器都实现了一个叫做 Iterator 的标准 trait,其定义如下:

1
2
3
4
5
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 此处省略了方法的默认实现
}

type ItemSelf::Item 定义了 trait 的关联类型(associated type),表示 next 返回的是元素的类型。

v.iter()/iter_mul()/into_iter() 获取 vector 对应的迭代器。后者会获取 vector 的所有权。

map 能遍历(消费)迭代器并返回一个新迭代器,sum/collect/filter 等能获得迭代器所有权并消费它。

1
2
3
4
5
6
7
8
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
}

智能指针

最简单的智能指针是 Box<T>,实际数据会被转移到堆上存储,栈上仅保留指针。

Rust 不允许直接递归定义结构体,因为这样就无法计算所需空间,但我们可以利用 Box 间接做到这一点。

1
2
3
4
5
6
7
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

智能指针通常需要实现 Deref trait 和 Drop trait 来做到引用和清理。

Deref Trait 用来重载解引用运算符(dereference operator)。调用 *mb 后等价于调用 *(mb.deref())

1
2
3
4
5
6
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}

Rust 支持以下三种解引用的强制转换(deref coercions)。如 &String 能够强转为 &str

  • T: Deref<Target=U> 时从 &T&U
  • T: DerefMut<Target=U> 时从 &mut T&mut U
  • T: Deref<Target=U> 时从 &mut T&U

Drop Trait 用于离开作用域时自动清理空间。注意不能在作用域结束前显式调用 c.drop() 提早清理,会收到编译错误。不过可以用 std::mem::drop(c) 来做到提早清理。

1
2
3
4
5
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}

引用计数指针 Rc<T> 支持多所有权机制。用 Rc::clone 克隆时计数器会加一,离开作用域计数器就减一,当计数值为 0 时数据会被清理。 注意 Rc<T> 仅支持多个部分之间只读地共享数据,否则会违反所有权规则。

Rc<T> 可能会引起引用循环导致内存泄漏,因为只有当 strong_count=0 才会清理。可以调用 Rc::downgrade 创建 Weak<T> 。弱引用也会进行计数,区别在于 weak_count 无须计数为 0 就能使 Rc<T> 实例被清理。

内部可变性(interior mutability)是 Rust 的一个设计模式,需要用 unsafe 标记,以模糊可变性和借用规则。

RefCell<T> 就是遵从内部可变性的指针。它是唯一所有权模式,但是能修改其指向的原本不可变的数据。它有 borrowborrow_mul 两个方法,分别创建一个不可变借用和可变借用。任何时候仅能有多个不可变借用或者一个可变借用,只是这个规则从编译时检查变成了运行时检查(违反会 panic)。

结合 RcRefCell 来拥有多个可变数据所有者,即 Rc<RefCell<T>>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a); // a after = Cons(RefCell { value: 15 }, Nil)
println!("b after = {:?}", b); // b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
println!("c after = {:?}", c); // c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
}

unsafe

unsafe 有以下特殊的“超能力”,但是依然受到 Rust 编译器对于借用规则的制约。

解引用裸指针。除了引用和智能指针外,Rust 还支持裸指针。裸指针可以绕开借用规则同时拥有一个数据的多个可变/不可变指针。裸指针和 C 语言的指针很像,可以是 null,不保证指向的内存合法,没有实现自动 drop。

1
2
3
4
5
6
7
8
let a: Box<i32> = Box::new(10);
let b: *const i32 = &*a;
let c: *const i32 = Box::into_raw(a);
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32; // 这里的强制转化是安全的,只有解引用时才是不安全的。
unsafe {println!("r1 is: {]", *r1);}
let address = 0x012345usize;
let r = address as *const i32; // 基于内存地址创建的裸指针显然是不安全的。

调用 unsafe 函数或者 FFI 的函数FFI(Foreign Function Interface)可以用来与其它语言进行交互。

1
2
3
4
5
6
7
8
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}

访问或修改一个可变的静态变量。实现一个 unsafe 的 trait。访问 union 中的字段(常用于与 C 交互)。

宏(macro)分为 声明宏(Declarative Macro)和三种 过程宏(Procedural Macro)。

声明宏用于编写一些类 match 的表达式来生成代码。以下是 vec! 简化后的声明宏。

  • #[macro_export] 标注说明,只要将定义了宏的 crate 引入作用域,宏就应当是可用的。
  • macro_rules! 和宏名称完成对宏的定义。
1
2
3
4
5
6
7
8
9
10
11
12
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}

过程宏(procedural macros)更像是函数,接收 Rust 代码作为输入,操作后产生另一些代码作为输出。过程宏分为自定义派生(derive)、类属性和类函数三种。

举个例子,我们希望实现一个自定义 derive 宏,使得只要在结构体定义前加上 derive(HelloMacro) 的标记,就能为结构体自动定义一个能够打印结构体名字的 trait 方法。

  • 当用户在一个结构体前指定了 derive(HelloMacro) 标记后,hello_macro_derive 会被调用。
  • syn::parse 负责解析把 rust 代码解析成可操作的数据结构。若失败,会直接 panic。
  • impl_hello_macro() 读入可操作的数据结构,转化成新代码的 TokenStream。quote! 宏可以让我们编写需要返回的 Rust 代码。最后的 into() 方法会将其从原来的中间表示(IR)转化成 TokenStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}

类属性宏不仅可以为属性生成代码,还可以创建新的属性。类函数宏能比声明宏更灵活地操控函数。

包管理

crate 是一个二进制项(binary crate)或者库(library crate)。

包(package)是提供一系列功能的一个或多个 crate。Cargo.toml 描述了包的具体信息,src/main.rs 是默认的二进制项,src/lib.rs 是默认的库。可以在 src/bin 里添加任意多的二进制项。

模块(mod)可以对一个 crate 里的代码进行分组和嵌套。模块、函数、结构体、结构体成员等默认私有(子模块可以调用父模块内容,父模块无法调用子模块内容,模块之间可以调用),枚举成员默认公有。可以用 pub 关键词修饰公有,修饰后上一层父模块可调用子模块内容(如果想在更上层调用,需要继续添加 pub 关键词)。

调用模块树里的内容时,可以使用绝对路径(crate::...)或相对路径(用 self:: 表示当前模块,用 super:: 表示父模块,也可以直接以同级的某个模块名开始)。

use 导入(创建路径的软链接),用 as 重命名,用 .* 引入全部公有项。一般函数的导入会保留最后一层模块,而结构体直接引用到具体内容。 use 前也可以用 pub 修饰,称为重导出(re-exporting)。

可以用嵌套路径来消除大量的 use 行。如果涉及和子路径合并,可以使用 self 关键词。

1
2
3
4
5
6
use std::cmp::Ordering;
use std::io;
use std::{cmp::Ordering, io};
use std::io;
use std::io::Write;
use std::io::{self, Write};