Rust 基础

Misc

  • let 声明的是不可变变量,let mut 声明的是可变变量。同时 Rust 中还有常量,必须用 const 声明且指定类型。因为常量必须在编译期被估值

  • 与 Java,Go 不同,使用 let 声明的变量不会被赋予零值,必须手动对其进行初始化才能使用(未被使用的变量可以不用初始化)。未被使用的变量编译器会给 warning,在这个变量名前加一个 _ 以显式告诉编译器这个变量就是用不到的(常见情形:只接收返回值但不使用;只传递参数但不使用)

  • let a: i32 = 3;,这其中的类型声明不是必须的

  • println!() 其实不是一个函数,而是一个宏(名字后面是 !

  • 在格式化输出语句中,{} 可以当做任何类型的占位符,它输出的内容由是变量的 std::fmt::Display 这个 trait 决定;{:?} 输出的内容由变量的 std::fmt::Debug 这个 trait 决定

可以简单地认为 Rust 中的 trait 就是其他语言中的 Interface

  • 加了 ; 就是语句,不加就是表达式。结合编译原理的知识可知,表达式可以看做有返回值。这在 Rust 的函数中尤为重要,函数体内最后一条表达式的结果将会被当成函数的返回值,因此无需显式的 return

  • Rust 中存在元组 tuple类型,元组中的元素可以不是一个类型的。可以通过将函数的返回值设置成一个元组,来实现 Go 中类似的函数多返回值特性

  • 与 Java/C++/C 中的 for 不同,Rust 中的 for 语句的格式为 for <element> in <iterator>。事实上,Rust 的风格是基于迭代器(更具体的,是 into_iter)而不是下标,来对集合(包含数组、切片等)的元素进行访问

  • Rust 中的 moduse,十分类似 Java 中的 package 与 import,但更灵活一些,例如在同一个文件中可以定义多个 mod

  • size_of::<String> 在一台 64 位的机器上运行的结果为何是 24?

    首先一个 String 对象的字符是存储在堆中的,这部分的大小是无法获知的。使用 size_of 得到的其实只是这个对象在上管理的元数据的大小:一个指向堆中数据的指针、length、capacity。这三个字段的长度都是 usize,在 64 位机器上,usize 就是 u64,占据 8 个字节,因此这三个栈上的字段总共占据 24 字节。结合后面的内容我们可以知道,String 类型也可以看作是一种智能指针

  • Rust 中的宏比 C/C++ 中的宏更强大,它的本质上是代码生成器,类似于 Java 中的 lombok 插件。C/C++ 中的宏完全基于文本层面的替换,而 Rust 中的宏是带有语义特征的,直接在编译期在 AST 层面上进行替换

  • char 类型是 32 位的,因为存储的是 Unicode。与 Java 和 Go 不同,Rust 中的 char 类型必须显式转为整数类型才能相减

  • if 是表达式而不是语句,因此 if 块可以返回值(并且每个分支的返回值类型必须一致)。或者从另一个角度理解,{} 都是有返回值的。同理,loop 也是一个表达式,它的功能等于其他语言的 while (TRUE)。此外,if 的条件表达式也可以写在一个局部作用域中

    初学 Rust 时可能会觉得一些代码中的局部作用域完全是多余的,但是这其实方便了对象生命周期的管理。例如在一个局部作用域中使用锁,那么局部作用域结束后,锁就被自动释放了

  • Rust 中的闭包由|| -> () {} 的形式实现,可以捕获外部的变量(此时要格外小心所有权的移动);而在函数内部定义的函数(使用 fn 关键字)是不能捕获外部的变量的(这点与 Go 不同)。并且 Rust 中的闭包不能进行递归调用(使用函数指针也许可以做到,不过太丑了)

  • 在涉及多态的编程中,有这样一条经验:「参数最好是引用(&dyn TraitA),返回值最好是 owner(Box<dyn TraitA>)」

所有权与借用

下文中的「引用」和「借用」可认为是同义词

引用本身是一种类型,并且是尺寸固定的,即 usize

  • Rust 采取的内存回收机制更像是 C++ 中的 RAII 思想

    回忆:C++ 中的 RAII 思想:

    RAII = Resource Acquisition Is Initialization

    一句话总结:堆内存资源随着其关联的栈上局部变量一起被回收

    我们知道,C++ 中,一个作用域结束后,会自动清理在这个作用域中声明的全部局部变量。对于在这个函数中声明的局部对象(是的,C++ 中对象可以创建在栈上,此即局部对象),它会自动调用该对象的析构函数,释放其占有的资源。于是使用局部对象来实现 RAII 是简单的。但是,将对象全部创建成局部对象也许不太合适(例如想要创建的对象太大了,不适合放在栈上)。如果我们想对创建在堆上的对象也使用 RAII 的思想进行管理呢?在 C++ 11 以后,提供了智能指针的概念。智能指针本身是创建在栈上的,它所指的对象创建在堆上。但是,当智能指针离开其作用域后,其析构函数被调用时,不仅会释放自己所占的内存,也会自动调用它所指的堆上对象的析构函数,从而实现 RAII

  • 基本类型可以不考虑所有权与借用,因为变量之间的赋值其实自动执行了拷贝

    哪些类型在赋值时会自动拷贝,而不转移所有权?

    • 所有的整数类型、浮点类型、boolchar
    • 由以上类型组成的 tuple
    • 由以上类型组成的 array
    • 可变引用 &T

    那么由以上类型组成的 struct 是否会自动拷贝?这取决于是否实现了 Copy 这个 trait(事实上,对于上面能够自动拷贝的类型,其实是编译器自动为它们实现了 Copy

  • 可以认为只有上的值(也许称为对象更能与已有的认知联系在一起)才要考虑所有权与借用的关系。当我们把一个值赋给一个变量时,此时这个变量就称为这个值的拥有者,一个值的拥有者是唯一的,这种拥有关系可以进行转移,转移过后,原有的变量不再有效;在离开一个作用域时,Rust 会自动调用 drop 函数,它将清理所有的有效的所有者所引用的堆内存。下面是一段示例代码,它可能与常见的面向对象语言(如 Java)的行为不一致

    1
    2
    3
    4
    5
    fn main() {
    let x: String = String::from("Hello");
    let y = x;
    println!("{}", x)
    }
    RUST

    这段代码将不能通过编译,原因是第 3 行代码使得值的所有权被转移到 yx 不再有效;

    编译器告诉我们,‘move occurs because x has type String, which does not implement the Copy trait’。意思是说,如果 x 变量绑定的值具有 Copy 这个 trait,那么将发生值的拷贝,执行第 3 行代码后,xy 均有效

    另外,编译器提示我们可以使用 clone() 方法,以下是修改后的代码,这个代码可以顺利通过编译:

    1
    2
    3
    4
    5
    fn main() {
    let x: String = String::from("Hello");
    let y = x.clone();
    println!("{}", x)
    }
    RUST

    这很像其他编程语言中浅拷贝和深拷贝的关系。在 Rust 中,除非显式地使用 clone(),否则变量间的赋值永远都是浅拷贝(即发生所有权的转移)

    除了上述的变量间的赋值语句,函数调用也会转移值的所有权:

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let x: String = String::from("Hello");
    take_ownership(x);
    println!("{}", x)
    }
    fn take_ownership(s: String) {
    println!("{}", s)
    }
    RUST

    执行第 3 行代码调用函数后,x 所绑定的值的所有权被转移到函数的形参 s 中,第 7 行代码执行结束后,由于 s 离开其作用域,将导致其绑定的值被回收。值得注意的是,尽管第 7 行代码可以顺利执行到,但是运行这段代码并不能看到这个输出。因为上述关于所有权与借用的讨论,编译器在编译期进行检查。上述代码未能通过编译,因此不会执行

    所以函数参数的类型基本都是引用类型或者是智能指针类型

    使用 for in 迭代集合对象时也会发生所有权的转移,因此要尽量使用引用

    此外,函数的返回值也可以转移其所有权到函数的调用者中,可以将它形象地理解成被外部接收的返回值幸运地逃脱了函数调用结束后的回收

  • 如果将一个变量的引用赋给另一个变量,此时值的所有权发生了借用。借用只允许获得值的使用权,但不能获得值的所有权(ownership)。因为引用并不拥有这个值,所以当引用离开作用域后,其引用的值也不会被drop

  • 通过引用修改原始的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let mut s = String::from("Hello");
    let s1: &mut String = &mut s;
    foo(s1);
    println!("{}", s)
    }

    fn foo(s: &mut String) {
    s.push_str(" World");
    }
    RUST

    正如可变变量的声明一样,可变引用必须显示使用 mut 关键字进行声明

    只能通过可变引用修改借用的值,并且这个值必须也是可变的!并且一旦存在一个可变引用,相同作用域下不能存在同一个值的任何其他引用(包括不可变引用)。这与读写指针的思想一致

    在一个不可变引用的作用域内,可以存在其他的不可变引用,但不能存在对于同一个值的可变引用!

    Rust 的编译器检查可以保证不可能出现 C/C++ 中的悬垂指针(尝试访问一块已经被释放的区域)

    let mut a = &c;

    a = &d;

    这里 a 前面的 mut 指的是允许 a 引用其他的值,而不是允许修改 a 指向的值本身

  • 一些函数调用也会转移值的所有权,原因是这个函数虽然是通过 . 操作符调用,但是隐含了一个 self 参数,这个参数就是这个对象的引用(联想 Python 类的方法定义中自带的 self),而且这个引用往往是可变引用。因此,函数调用可能会十分隐晦地违反上述引用的冲突规则,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let mut s = String::from("Hello");
    let first = first_char(&s); // immutable borrow here
    s.clear(); // mutable borrow here
    println!("{}", first) // immutable borrow here
    }
    fn first_char(s: &String) -> &str {
    &s[..1]
    }
    RUST

    在第 4 行,由于 first 仍旧活跃(因为第 5 行将被使用),并且,这个切片的底层与 s 的底层是一致的,因此可以看做是一个活跃的对于 s 的不可变引用。但是在调用 clear 方法时,实际上是为这个方法提供了一个 s 的可变引用。这就违反了上述的引用冲突的规则。尽管 first(即 &str 切片)只是指向 s 的部分数据,它仍然是基于 s 的不可变引用。在 s 的一部分(通过 first指向)仍然存在不可变引用时,不能存在 s 的可变引用

  • 与函数调用类似的,使用宏(macro)时也可能隐晦地违反上面的规则

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let mut a = 10u32;
    let b = &mut a;
    *b = 20;
    println!("{a}"); // 这里发生了对a的不可变借用
    println!("{b}"); // 可变借用b的生命周期在这里终止
    }
    RUST
  • 所有型变量和引用型变量生命周期的不同

    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let mut a = 10u32;
    let c = &a;
    let b = &mut a;
    *b = 20; // b的生命周期这里结束
    println!("{c}"); // c的生命周期这里结束
    } // a的生命周期这里结束
    RUST

    NLL(Non Lexical Lifetime)

    • 所有型变量(如上面的 a)的生命周期在一个 } 结束
    • 引用型变量(如上面的 b, c)在它最后一次被使用后结束

    其他规则还有

    • 所有型变量的生命周期一定长于它的引用型变量,否则会出现悬垂指针的问题

    • 一个所有型变量的不可变引用的生命周期可以交叉

    • 一个所有型变量的可变引用的生命周期与其他任何引用的生命周期都不能交叉

    • 在一个所有型变量的引用的生命周期内,不允许通过原来的所有型变量对值进行修改

      1
      2
      3
      4
      let mut a = 3;
      let r = &a;
      a = 4;
      println!("r={}", r);
      RUST

      报错是:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      error[E0506]: cannot assign to `a` because it is borrowed
      --> src/main.rs:6:5
      |
      5 | let r = &a;
      | -- `a` is borrowed here
      6 | a = 4;
      | ^^^^^ `a` is assigned to here but it was already borrowed
      7 | println!("r={}", r);
      | - borrow later used here
      APPLESCRIPT
    • 将一个可变引用赋值给另一个引用,那么原来的可变引用将无效

类型

数组

  • 与 Go 类似,Rust 中数组的长度也是其类型的一部分

    1
    2
    // Array type syntax: [ <type> ; <number of elements> ]
    let numbers: [u32; 3] = [1, 2, 3];
    RUST
  • 因为数组的大小是编译时可知的,因此其被分配在

动态数组 Vec

1
2
let mut numbers: Vec<u32> = Vec::new();
let numbers = vec![1, 2, 3]; // 使用宏创建
RUST
  • Vec 本身在中存储:指向堆中数据的指针、lencap。因此也可以将 Vec 类型本身视作一种智能指针

  • 放入 Vec 中的元素,这个 Vec 拥有它的所有权;如果放入 Vec 的是一个引用,那么要保证这个引用的生命周期至少跟这个 Vec 的生命周期一样久(下面的 HashMap 同样适用于这个规则)

切片

  • 切片的类型是 &[T],本质上是对底层数组的借用
  • Rust 中的切片语法与 Go 中的相似,只不过使用 .. 连接左右索引
  • 字符串切片的类型是 &str(可以看做是对底层 String 的引用)。对字符串使用切片语法时需要注意,切片索引的是字节,如果存在某些非英文字符,此时需要考虑每个 Unicode 字符占据多少个字节
  • 一个比较 tricky 的点是,可以使用 &mut[u32] 去修改这个切片的底层数组,但是并不适合&mut str 去修改底层字符串的内容(虽然使用 unsafe 依旧可以做到)

哈希表 HashMap

  • HashMap 的 get 方法,参数和返回值均是引用类型。使用 for in 语法进行迭代时,所迭代的键值也都是引用

  • 考虑我们要使用一个哈希表实现统计单词词频的任务,这包含创建不存在的单词的键值对、更新已经存在的单词的词频,在 Java 中,可以使用以下方式:

    1
    2
    3
    4
    5
    6
    var map = new HashMap<String, Integer>();
    for (String word: text) {
    map.put(word, map.getOrDefault(word, 0)+1);
    // optional
    // map.merge(word, 1, Integer::sum);
    }
    JAVA

    在 Rust 中实现类似的功能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    use std::collections::HashMap;

    fn main() {
    let text = String::from("text text text");
    let mut map = HashMap::new();
    for word in text.split_whitespace() {
    let cnt = map.entry(word).or_insert(0); // 返回的cnt时一个可变的引用
    *cnt += 1 // 通过这个可变的引用实现value++
    }
    }
    RUST

元组

1
2
3
4
5
6
7
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
// 使用.运算符访问
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
RUST
  • 元组中的元素可以是不同类型的,这在函数的多返回值时十分有用
  • 元组的元素个数是固定的
  • 当元组没有任何元素时,它变成 (),对应的类型是 unit 类型。unit 类型的唯一实例就是 ()

结构体

  • 命名结构体

    1
    2
    3
    4
    struct User {
    name: String,
    age: u32,
    }
    RUST
  • 元组结构体

    实际上是为一个元组取了一个名字。对于元组结构体中字段的访问,与元组一样,使用下标

    1
    struct Color(i32, i32, i32);
    RUST
  • 单元结构体

    1
    2
    struct Foo;
    let foo = Foo; // 这里创建了Foo的一个实例并绑定给foo
    RUST

    单元结构体不包含任何字段,它拥有的唯一信息就是它的名字

Rust 中类(结构体)的字段定义与类的方法定义是分离的:

1
2
3
4
5
6
struct Foo {
// some fields here
}
impl Foo {
// some methods here
}
RUST

方法的第一个参数设置为 self: &Self(更常见的情况是简写&self,注意区分大小写。这里的第二个 Self 值得注意一下,它将绑定调用这个方法的实例的类型。即 Self 对应正在被 impl 的那个类型)来调用结构体的字段或其他方法。此时由于 &self 是一个不可变引用,则不能通过这个引用去修改实例。可变引用是 &mut self。实际上第一个参数也可以设置为 self,不过这将导致实例所有权的转移,因此并不常用

impl{} 中定义的所有函数,全部称为关联函数。只要含有 self 参数(还包含 &self, &mut self),它就是一个方法,可以使用 . 操作符进行调用。除方法以外的关联函数,常见于对象的构造函数,比如使用一个名为 new 的函数定义类的构造器(Rust 中 new 不是一个关键字,只是习惯上用 new 命名构造函数)。对于这样的关联函数,使用 :: 调用,即 Foo::new()

可以将上面的两种函数的区别理解成 Java 中的静态方法(从属于类,用 :: 调用)和非静态方法(从属于类的实例,用 . 调用)的区别

如果想为一个 struct 实现一种 triat,使用 impl ... for ... {}

1
2
3
4
5
6
7
8
9
10
trait IpAddr {
fn display(&self);
}

struct V4(String); // 这是一个元组结构体
impl IpAddr for V4 {
fn display(&self) {
println!("ipv4: {:?}",self.0)
}
}
RUST

枚举

1
2
3
4
5
6
enum IpAddr {
V4,
V6
}
let a = IpAddr::V4;
let b = IpAddr::V6;
RUST

使用 enum 定义一个枚举。枚举里面的选项叫做此枚举的变体(variants)。变体是其所属枚举的类型的一部分

可以认为,结构体中,是所有的字段在起作用;而枚举中,是其中的某一个字段在起作用。结构体是一种类型(product type),枚举是一种类型(sum type)。元组也是一种类型(因为所有的字段均起作用)

更形式化的理解,类型的变量的取值是其中每一个成员的取值的笛卡尔积

C/C++ 中的 union 类型也是类型

每一个变体可以关联任意类型的数据,这些数据称为变体的负载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum WebEvent {
PageLoad,
PageUnload,
KeyPress(char), // 关联一个char
Paste(String), // 关联一个String
Click { x: i64, y: i64 }, // 关联一个结构体
}
// 枚举的实例化时也要提供对应的负载

let a = WebEvent::PageLoad;
let b = WebEvent::PageUnload;
let c = WebEvent::KeyPress('c');
let d = WebEvent::Paste(String::from("batman"));
let e = WebEvent::Click { x: 320, y: 240 };
RUST

当然也支持 C 风格的枚举,不过不常用:

1
2
3
4
5
6
7
8
9
10
11
// 给枚举每个变体赋予不同的值
enum Color {
Red = 0xff0000,
Green = 0x00ff00,
Blue = 0x0000ff,
}
fn main() {
// 使用 as 进行类型的转化
println!("roses are #{:06x}", Color::Red as i32);
println!("violets are #{:06x}", Color::Blue as i32);
}
RUST

枚举也是一种类型,可以使用 impl 定义枚举关联的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum MyEnum {
Add,
Subtract,
}
impl MyEnum {
fn run(&self, x: i32, y: i32) -> i32 {
match self { // match 语句
Self::Add => x + y,
Self::Subtract => x - y,
}
}
}
fn main() {
// 实例化枚举
let add = MyEnum::Add;
// 实例化后执行枚举的方法
add.run(100, 200);
}
RUST

match 表达式通常与枚举配套使用。match 的每个分支的返回值类型必须相同(或者是属于同一枚举的不同变体),必须涵盖所有的变体,可以使用 _ 匹配其他剩下的变体

match 也可以处理其他的基础类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let number = 13;
match number {
// 匹配单个数字
1 => println!("One!"),
// 匹配几个数字,使用 `|` 符号
2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
// 匹配一个范围,左闭右闭区间
13..=19 => println!("A teen"),
// 处理剩下的情况
_ => println!("Ain't special"),
}
}
RUST

字符串

  • 字符串的字面量本身存储在静态存储区

  • String 类型将字符串的内容拷贝到堆上

    1
    2
    let a = "1".to_string(); // "1" 本身位于静态存储区
    // 将字面量的值拷贝到堆上
    RUST
  • &String 是对 String 的引用,&str 是对 String切片引用

泛型

泛型其实就是额外的类型参数

1
2
3
4
5
6
7
8
9
struct Point<T> { // 使用多个类型参数:<T, U, K>
x: T, // 表明x是T类型的
y: T,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = Point::<i32> { x: 1, y: 2 };
// 使用turbofish运算符手动指定类型参数T。某些情况下编译器无法推断泛型参数的实际类型,此时就需要用 ::<>这个运算符手动指定类型
}
RUST

有关 ::<> turbofish 运算符的由来:它看上去像一条游动的鱼

遗憾的是,它的提出者在 2021 年患癌去世了

可以将 impl 作用在泛型上:

1
2
3
4
5
6
7
8
9
10
11
12
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn play(n: T) {}
}
impl Point<u32> {
fn play_u32() {}
// fn play_u32() {} 定义这个函数将不能通过编译
}
// 注意,为一个泛型(如 Point<T>)实现一个方法后,不能再为它的一个具体类型(如 Point<u32>)实现同名方法
RUST

Option 与 Result

这是两个十分常用的 enum 类型,起源于函数式编程语言

Option<T> 表示有或无

1
2
3
4
pub enum Option<T> {
Some(T),
None,
}
RUST

Result<T, E> 表示结果正确还是错误

1
2
3
4
pub enum Result<T, E> {
Ok(T),
Err(E),
}
RUST

可以将 Option 与 Result 视作包裹类型,那么如何从中取出被包裹的值?

  • expect():可以指定错误信息。如果 Option 实例为 None,或是 Result 实例为 Err 时,就会 panic,并打印出这个错误信息
  • unwrap():不带提示信息,其余行为与 expect() 一致
  • unwrap_or():可以提供 Option 为 None 或 Result 为 Err时的指定默认参数,不会 panic
  • unwrap_or_default():可以提供 Option 为 None 或 Result 为 Err 时类型的默认参数,不会 panic

迭代器

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

使用迭代器进行遍历时,编译器可以确信不会引起越界访问,所以不会进行越界检查。因此,使用迭代器进行遍历一般要比使用下标进行遍历更快

  • iter():获取集合元素的不可变引用的迭代器
  • iter_mut():获取集合元素的可变引用的迭代器
  • into_iter():获取集合元素的所有权的迭代器

for 语句其实是语法糖for item in c {} 其实是:

1
2
let mut itr = c.into_iter();
while let Some(item) = itr.next() {}
RUST

可以看出,使用 for 循环迭代一个集合,集合中的元素的所有权将被转移。同时,要使用 for 迭代一个类型的实例,这个类型必须 implinto_iter() 这个方法

此外,for ... in &c {}for ... in &mut c 提供了不转移所有权来遍历集合的方法

解构(模式匹配)

注意解构成功后默认会改变变量的所有权

如果希望解构只获得变量的引用而不是所有权,要使用 ref 关键字

let (ref x, ref mut y) = t,其中 x 是一个不可变引用,y 是一个可变引用

  • 解构元组

    1
    2
    3
    4
    5
    6
    7
    let t = (1, 3);
    let (x, y) = t;
    println!("x: {}, y: {}", x, y);
    // 注意解构成功后会改变变量的所有权
    let t = (1, 3);
    let (ref x, ref mut y) = t; // x是原始变量的不可变引用,y是原始变量的可变引用
    println!("x: {}, y: {}", x, y);
    RUST
  • 解构结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct Point {
    x: i32,
    y: i32,
    }
    let p = Point {x: 10, y: 20};
    let Point {x, y} = p;
    println!("x: {}, y: {}", x, y);
    // 当然也可以解构部分字段
    let Point {x, ..} = point; // 使用..忽略字段。不过被忽略的字段的所有权也一样被转移了
    println!("x: {}", x);
    RUST
  • 解构枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    }
    let msg = Message::Write("Hello, World!".to_string());
    match msg {
    Message::Quit => println!("Quitting"),
    Message::Move { x, y } => println!("Moving: ({}, {})", x, y),
    Message::Write(s) => println!("Writing: {}", s),
    }
    RUST
    • 可以使用 | 并列匹配多种情况
    • 使用 _ 处理其他分支
  • 解构数组和切片

    1
    2
    3
    let arr = [1, 2, 3];
    let [a, b, ..] = arr;
    println!("{:?}", a);
    RUST
  • 解构引用

    1
    let &(a, b) = &(1, 2);
    RUST
  • if letwhile let

    解构成功即满足条件

    1
    2
    3
    4
    5
    6
    7
    if let Some(x) = Some(5) {
    // 解构成功
    }
    let mut x = 0;
    while let Some(x) = None::<i32> {
    // 解构失败
    }
    RUST

trait

Rust 中的 trait 与 Java 中的 Interface 代表了两类思想:组合和继承

而软件工程的经验表明,组合优于继承

  • trait 用来限定泛型参数的具体类型。T: TraitA

    类型对变量的取值空间进行约束,trait 对类型参数的取值空间进行约束

  • trait 中可以定义关联函数。这些关联函数要么只提供函数签名(不带 {}),要么提供默认的实现

    1
    2
    3
    4
    5
    6
    7
    8
    trait Sport {
    fn play(&self) {} // 提供了默认实现,尽管这个实现什么都不做
    fn play_mut(&mut self); // 仅仅是函数签名
    }
    struct Tennis;
    impl Sport for Tennis {
    fn play_mut(&mut self) {}; // 提供一个空实现
    }
    RUST
  • trait 中可以带关联类型,起到一种占位的功能,在为某一个类型实现这个 trait 的时候再为关联类型指定具体的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    trait Sport {
    type T; // T仅仅是一个占位符
    fn play(&self, st: Self::T);
    }
    struct Tennis;
    enum SportType {
    Land,
    Water,
    }
    impl Sport for Tennis {
    type T = SportType;
    fn play(&self, st: Self::T) {};
    }

    let t = Tennis{};
    t.play(SportType::Land)
    RUST
  • 可以在使用 trait 对类型参数进行约束时,提供指定的关联类型:

    1
    2
    3
    4
    5
    6
    7
    8
    trait Sport {
    type T; // T仅仅是一个占位符
    fn play(&self, st: Self::T);
    }
    struct Foo<T: Sport<T=SportType>> {
    x: T,
    }

    RUST
  • 关联类型也是可以添加约束的

    1
    2
    3
    4
    trait Sport {
    type T: Display; // 具体的T类型必须实现了Display这个trait
    fn play(&self, st: Self::T);
    }
    RUST
  • trait 间的依赖

    1
    trait A: B {}
    RUST

    这表示任何试图实现 A 的类型也必须同时实现了 B

  • trait 上带类型参数

    1
    trait TraitA<T> {}
    RUST

    这表示这个 trait 里的关联函数,可能会用到这个类型参数 T。此时 TraitA<T> 作为一个整体,比如 TraitA<u32>Trait<i32> 就是两个不同的类型

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    trait Add<T> { // 这里的T表示该trait的关联函数会用到T
    type Output;
    fn add(self, rhs: T) -> Self::Output;
    }

    struct Point {
    x: i32,
    y: i32,
    }

    impl Add<Point> for Point {
    type Output = Self;
    fn add(self, rhs: Point) -> Self::Output {
    Self {
    x: self.x + rhs.x,
    y: self.y + rhs.y,
    }
    }
    }
    impl Add<i32> for Point {
    type Output = Self;
    fn add(self, rhs: i32) -> Self::Output {
    Self {
    x: self.x + rhs,
    y: self.y + rhs,
    }
    }
    }

    fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };
    let p3 = p1.add(p2); // p1与另一个Point相加
    let delta = 2;
    let p4 = p3.add(delta); // p4与一个i32相加
    }
    RUST

    可以看出,Rust 虽然不支持 C++/Java 中的函数重载,但是用 trait 灵活地实现了类似功能

  • 由标准库定义的 trait

    • Operator traits (e.g. Add, Sub, PartialEq, etc.)(C++ 中用操作符重载实现类似的功能。一种简单的理解方式是将内置操作符看成是一种特殊的方法名,x <op> y 其实是 x.op(y)

    • From and Into, for infallible conversions(无损的类型转换)

    • Clone and Copy, for copying values

    • Deref and deref coercion

      这是一个很方便编程的 trait,可以将对于一个类型的访问替换为另一个类型的访问,比如:

      • 将对 Vec<T> 的访问转换为对切片 &[T] 的访问,从而使得 Vec<T> 类型可以调用对应的切片类型的方法
      • 将对智能指针的访问转换为对其所包裹的类型的访问

      这些行为全都是编译器自动实现的

      1
      2
      3
      4
      5
      6
      7
      mod std::ops {
      pub trait Deref {
      type Target: ?Sized;

      fn deref(&self) -> &Self::Target;
      }
      }
      RUST
    • Sized, to mark types with a known size

    • Drop, for custom cleanup logic

  • 在 Rust 中,可以为任何一个内置类,通过实现一个自定义的 trait 以附加新方法。而在 Java 中,要为内置类添加新方法并不是一件容易的事

  • trait 的定义,实现该 trait 的 struct 的定义,与具体对于 trait 的 impl,这三者的作用域要符合孤儿规则中的至少一点

    1
    2
    3
    4
    5
    trait t_name {} // trait 的定义

    struct s_name {} // type 的定义

    impl t_name for s_name {} // 具体的实现
    RUST
    • 本地类型:trait 或 struct 至少有一个是在当前 crate 中定义的

    • 外部类型和外部 trait:如果想为一个外部类型(例如来自标准库或第三方库的类型)实现一个外部 trait(同样可能来自标准库或第三方库),则不能这么做,除非这种实现是在类型或 trait 的原始定义的 crate 中进行的。

      例如下面的代码将无法通过编译:

      1
      2
      3
      4
      5
      impl PartialEq for u32 {
      fn eq(&self, _other: &Self) -> bool {
      todo!()
      }
      }
      RUST

      因为 trait PartialEq 和 type u32 均是在 std 中定义的,而不是在当前作用域

  • Marker Trait / Auto Trait

    这类 trait 一般是标准库内置的,且 trait 的体是空的,意味着声明该类型具有某种性质,以便编译器进行优化;如 Sized,它表明这个类型的实例的大小在编译期是可知的;再如 Copy,它的声明如下:

    1
    pub trait Copy: Clone { }
    RUST

    即使不需要为 Copy 本身实现方法,但是得保证为 Clone 实现了方法

  • 一个符合 Rust 风格的泛型示例

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    trait Power<M> {
    fn power(&self, arg: M) -> Self;
    }
    impl Power<u16> for u32 { // Self就是u32
    fn power(&self, arg: u16) -> Self {
    let mut res = 1;
    for _ in 0..arg {
    res *= *self;
    }
    res
    }
    }
    impl Power<u32> for u32 {
    fn power(&self, arg: u32) -> Self {
    let mut res = 1;
    for _ in 0..arg {
    res *= *self;
    }
    res
    }
    }
    impl Power<&u32> for u32 {
    fn power(&self, arg: &u32) -> Self {
    let mut res = 1;
    for _ in 0..*arg {
    res *= *self;
    }
    res
    }
    }
    #[cfg(test)]
    mod tests {
    use super::*;

    #[test]
    fn test_power_u16() {
    let x: u32 = 2_u32.power(3u16);
    assert_eq!(x, 8);
    }

    #[test]
    fn test_power_u32() {
    let x: u32 = 2_u32.power(3u32);
    assert_eq!(x, 8);
    }

    #[test]
    fn test_power_ref_u32() {
    let x: u32 = 2_u32.power(&3u32);
    assert_eq!(x, 8);
    }
    }
    RUST

生命周期

严格来讲,为了避免悬垂引用的出现,借用检查器会基于一套规则为每一个引用都标注生命周期。不过当这些规则不足以推导出引用的生命周期时,借用检查器要求进行手动标注

对于生命周期的简单理解:一个引用不能比它所引用的值的生命周期还长,如果一个引用所引用的对象活得比自己长,那这个引用就是安全的

NLL

Non Lexical Lifetime 是 Rust >= 1.63 引入的特性,旨在更加智能化地判断变量的生命周期

简单来说,在 NLL 之前,一个变量的生命周期是从它的声明点开始,到它的声明点所在的词法作用域的结束点(例如一个用 {} 标记的作用域,它的结束点就是 }),即基于词法分析的生命周期

引入 NLL 之后,一个变量的声明周期是从的它的声明点开始,到它的最后一次被使用结束,即基于控制流信息的声明周期)

例如在未引入 NLL 之前,下面的代码不能通过编译

1
2
3
4
5
6
7
8
9
10
11
fn example() {
let s = String::from("Hello");
let r;
{
let t = String::from("World");
r = &t;
}
// 这里会发生借用检查错误,因为r的生命周期会被推断到example函数的末尾,
// 而t的生命周期仅限于内部作用域,因此r成为悬垂引用
// 通用规则:一个引用的生命周期不能长于它所引用的对象,否则这个引用就会成为悬垂引用
}
RUST

在引入了 NLL 之后,编译器能够更加「智能」地推导 r 的生命周期

1
2
3
4
5
6
7
8
fn example() {
let s = String::from("Hello");
let r;
{
let t = String::from("World");
r = &t; // NLL下,r的生命周期到它的最后一次被使用就结束,不会长于它所引用的对象的生命周期,因此不会成为悬垂引用
}
}
RUST

下面的代码仍然不能通过编译,因为 NLL 仍然判断出引用的生命周期长于它所引用的对象

1
2
3
4
5
6
7
8
9
fn example() {
let s = String::from("Hello");
let r;
{
let t = String::from("World");
r = &t;
}
println!("r: {}", r); // r的生命周期比它引用的对象还长,成为悬垂引用
}
RUST

智能指针

Box<T>

Rust 的内存模型中,栈上存放的数据类型的大小必须是在编译时即可获知的。如果需要创建编译时大小不可知的数据类型、或者该数据类型的大小过大以致于不便放在栈上,那么可以使用 Box,它包裹的对象是分配在堆上的。由于 Box 本身的大小是编译时可确定的,因此 Box 本身分配在栈上。不过注意这个例子,let v = vec![Box::new(2), Box::new(3)]v中的这两个 Box 其实是分配在堆上的,因为 Vec 类型本身也是一个智能指针

1
2
3
4
5
6
7
8
enum List {
Cons(i32, List), // 编译器报错,这种递归定义的数据类型是不可以的,因为递归类型 `List` 拥有无限长的大小
Nil,
}
enum List {
Cons(i32, Box<List>), // ok,因为Box<>本身是长度固定的
Nil,
}
RUST

或者用 Box 来实现特征对象 trait object

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
26
27
28
29
30
trait Draw {
fn draw(&self);
}

struct Button {
id: u32,
}
impl Draw for Button {
fn draw(&self) {
println!("这是屏幕上第{}号按钮", self.id)
}
}

struct Select {
id: u32,
}

impl Draw for Select {
fn draw(&self) {
println!("这个选择框贼难用{}", self.id)
}
}

fn main() {
let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })];

for e in elems {
e.draw()
}
}
RUST

RefCell<T>

用于不可变结构体的内部可变性

Rc<T>Arc<T>

二者均用于共享对象。基于引用计数机制,一旦一个对象的所有 RcArc 均被 drop 了,那么这个对象就自动被 drop 了

Rc<T> 用于单线程中,Arc<T> 用于多线程中

Rc<RefCell<T>> 十分常用,用于实现链表这样的自包含结构

Arc<Mutex<T>> 十分常用,用于在多线程间共享锁

Cow<T>

Copy-on-write

并发和异步编程

  • 利用 std::thread::spawn 开启一个新线程,它的参数是一个闭包(但是这个闭包几乎不用指定参数,如果要捕获外部参数,要使用 move 关键字),返回值是一个句柄

    1
    2
    3
    4
    5
    pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static
    RUST
  • 注意在 main 函数中开启的其他线程,一旦 main 函数执行结束(这个函数也叫主线程),它开启的所有线程也将立即结束;

    其余的线程与其开启的线程之间没有这样的约束关系

  • use std::thread;
    fn main() {
        let handle = thread::spawn(|| {
            println!("Hello from a thread!");
        });
    
        handle.join().unwrap();
        // join(): the main thread will wait for the spawned thread to finish before exiting
        // also get the result from the newly-spawned thread
    }
    <!--code55-->
    
    
    TEXT
  • 多线程编程中要格外小心生命周期带来的约束,因为 spawn 出的线程的生命周期很可能超出原来的线程的生命周期。设父线程为 A,spawn 出的线程为 B,B 中不应该借用(引用)那些 A 中可能被提前 drop 的值。我们结合上面的第 9 行中的 move 关键字来理解。move 关键字将外部的 v1 的所有权转移到新线程中来

    如果确实需要在线程中使用外部的引用,一定要保证它是 'static 的,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    pub fn sum(slice: &'static [i32]) -> i32 {
    if slice.len() <= 1 {
    return slice.iter().sum();
    }
    let (s1, s2) = slice.split_at(slice.len() / 2); // s1与s2均是 'static 引用
    let h1 = thread::spawn(move || {
    s1.iter().sum::<i32>()
    });
    let h2 = thread::spawn(move || {
    s2.iter().sum::<i32>()
    });
    let sum1 = h1.join().unwrap();
    let sum2 = h2.join().unwrap();
    sum1 + sum2
    }
    RUST

    'static 声明的值的声明周期是整个程序的运行时间

    还有一种方式,即用 leak,告知编译器,程序员永远不会手动释放一个堆上的值的内存,因此编译器可以为其返回一个 'static 的引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    pub fn sum(v: Vec<i32>) -> i32 {
    let tmp: &'static[i32] = v.leak(); // deliberately leak
    let (v1, v2) = tmp.split_at(tmp.len()/2);
    let h1 = thread::spawn(move || {
    v1.iter().sum::<i32>()
    });
    let h2 = thread::spawn(move || {
    v2.iter().sum::<i32>()
    });
    h1.join().unwrap()+h2.join().unwrap()
    }
    RUST

    还有一种方式,使用 scope

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    pub fn sum(v: Vec<i32>) -> i32 {
    if v.len() <= 1 {
    return v.iter().sum();
    }
    let (v1, v2) = v.split_at(v.len()/2);
    thread::scope(|scope| {
    let h1 = scope.spawn(|| {
    v1.iter().sum::<i32>()
    });
    let h2 = scope.spawn(|| {
    v2.iter().sum::<i32>()
    });
    h1.join().unwrap()+h2.join().unwrap()
    })
    }
    RUST
  • channel

    Rust 原生的 channel 是 mpsc 风格的,即 multiple producer, single consumer

    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
    26
    27
    28
    29
    30
    31
    use std::sync::mpsc;
    use std::thread;
    use std::time::Duration;

    fn main() {
    let (tx, rx) = mpsc::channel(); // tx是sender,rx是receiver

    // 创建多个生产者
    for i in 0..3 {
    let tx_clone = tx.clone(); // sender允许clone
    thread::spawn(move || {
    let val = format!("hello from thread {}", i);
    tx_clone.send(val).unwrap();
    println!("Thread {} finished", i);
    });
    }

    // 创建单个消费者
    thread::spawn(move || {
    for received in rx { // rx的所有权被转移
    println!("Got: {}", received);
    }
    });

    // 注意这里我们如果想要使用多个消费者的话,情况就变得复杂了。因为receiver是不允许clone的
    // 可以考虑使用Arc<Mutex<>>,即共享了receiver的引用,又考虑了互斥的问题

    // 主线程暂停一段时间,确保子线程全部执行完毕
    thread::sleep(Duration::from_secs(1));
    }

    RUST
  • Go 的 sync.Mutex 相关的操作是十分简洁的,使用 Lock() 和相应的 Unlock()。在 Rust 中,由于所有权的限制,同一个 Mutex 要想被多个线程共享,还要包裹在 Arc 中。一个典型的多线程锁的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    use std::sync::{Arc, Mutex};
    use std::thread;

    fn main() {
    let data = Arc::new(Mutex::new(Vec::new())); // Arc包裹Mutex,初始值为空Vec
    let mut handles = vec![];

    for i in 0..5 {
    let data_clone = Arc::clone(&data);
    let handle = thread::spawn(move || {
    let mut vec = data_clone.lock().unwrap(); // 注意这里的lock操作
    vec.push(i); // 向 Vec 中插入数据
    }); // vec在这里被drop,对应的锁自动被释放
    handles.push(handle);
    }

    for handle in handles {
    handle.join().unwrap();
    }

    let result = data.lock().unwrap();
    println!("Final vector: {:?}", *result);
    }
    RUST
    • let data = Arc::new(Mutex::new(Vec::new())); 这样的代码在 Rust 并发编程中十分常见

    • 注意到上面的代码中没有显式的 unlock 操作,因为 unlock 操作其实与对象的 drop 操作绑定在一起了,持有锁的对象(如上面 11 行中的 vec 对象)离开其作用域而被 drop 时,它持有的锁自动被释放。在 Rust 的并发代码中,尝尝会见到在局部作用域中进行加锁操作,使得结束该作用域时锁能被释放

      1
      2
      3
      4
      5
      let ok = { // Rust的局部作用域可以返回值
      let raft = self.inner.lock().unwrap(); // raft变量是MutexGuard类型
      raft.state.role != Role::Leader
      && raft.election_timeout_start.elapsed() >= raft.election_timeout_duration
      }; // make sure 'raft'(the lock) is scoped
      RUST

条件编译

有点类似 C++ 中的 #ifdef 和其他条件编译手段

Rust 不愧是 modern C++

根据编译目标机器的特性选择性执行代码 。比如说在一个命令行程序中,想根据 OS 的不同提示用户:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
use librkv::RKV;

#[cfg(target_os = "windows")]
const USAGE: &str = "
Usage:
rkv_mem.exe FILE get KEY
rkv_mem.exe FILE delete KEY
rkv_mem.exe FILE insert KEY VALUE
rkv_mem.exe FILE update KEY VALUE
";

#[cfg(not(target_os = "windows"))]
const USAGE: &str = "
Usage:
rkv_mem FILE get KEY
rkv_mem FILE delete KEY
rkv_mem FILE insert KEY VALUE
rkv_mem FILE update KEY VALUE
";

fn main() {
let args: Vec<String> = std::env::args().collect();
let fname = args.get(1).expect(&USAGE);
let op = args.get(2).expect(&USAGE).as_ref();
let key = args.get(3).expect(&USAGE).as_ref();
let value = args.get(4);
let path = std::path::Path::new(&fname);
let mut store = RKV::open(path).expect("cannot open file");
store.load().expect("cannot load file");
match op {
"get" => match store.get(key).unwrap() {
Some(value) => println!("{}", String::from_utf8(value).unwrap()),
None => eprintln!("{:?} not found", key),
},
"delete" => store.delete(key).unwrap(),
"insert" => {
let val = value.expect(&USAGE).as_ref();
store.insert(key, val).unwrap();
}
"update" => {
let val = value.expect(&USAGE).as_ref();
store.update(key, val).unwrap();
}
_ => eprintln!("{:?} not found", op),
}
}
RUST

Todo

第三方库

下面是 Web 框架(Rust 的 Web 框架基本是所有语言中最快的。不过用 Rust 写 Web 应用应该是挺虐的 🤣)


Rust 基础
https://exapricity.tech/Rust-Basics.html
作者
Peiyang He
发布于
2024年5月8日
许可协议