Rust 入门

    Programming Language

Rust 算是一个底层的系统编程语言。和有垃圾回收(Garbage Collection)的语言不一样,Rust 作为一种底层的系统语言,在内存管理方面有一些特殊的地方。它用某种语言特性设计(ownership)(部分)取代了垃圾回收。

应该说 C++指针也能实现类似的功能,但 Rust 增加了 ownership 这些概念和与之相应的使用规则,并且会对代码进行这些规则相关的静态检查——即运行代码前,编译器检会查你代码对内存的使用是否按照 Rust 的规则做对了。总的来说, Rust 的静态类型检查相比 C++ 等其他语言增加了内存使用的规范的检测,防止代码运行时出现内存问题。

Rust 的这些 ownershiplifetime 之类的静态检查设计,主要目的是让人们可以尽量少地使用 Rc(引用计数)。不过到了复杂的数据结构,比如解释器,仍然是要用 Rc 的。只是比较简单的那一部分代码你可以不用 Rc

由于 Rust 语言的内存管理通过少用或不用 Rc ,减少了 Rc 的开销,代码性能据说在某些情况下会快很多。所以可以稍微重视一下。当然,它的这种设计并没有很完善,其中有一些多余而复杂东西(比如 Rust 总有一些自以为方便的隐式操作,而非写什么代码就是什么的所见即所得,类型系统也反直觉地水),但理解它的内存管理思路能给人们提供启发。进而可以改进自己的代码思路。

C++ 稍微“扩展”一下 unique_ptr,也能实现 Rust 的内存管理精髓。但若没有 Rust 这套内存规则的作为大方向指导我们的思路,那我们可能就会不知道如何有效地使用这些 unique_ptr 等指针。

初次使用 Rust 语言编程很可能会非常痛苦,除了没有所见即所得的设计(各种隐式操作),期间还有复杂的内存管理规则要适应。多数时候更是要连蒙带猜。不过如果能耐心坚持到成功写出一个解释器,也许对内存管理的理解会上升一个台阶。


Rust 编程环境设置

简单的代码可以直接先去官网的 Rust Playground 在线运行。这样不用进行任何配置和安装,最方便。之后的正式学习,建议使用友好的 VS Code 来运行 Rust 代码。

MacOS 操作系统为例,先安装 Rust

## 官方推荐的安装方式,适用于 Linux 和 MacOS
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

## 如果有 Homebrew ,也可以使用它安装 Rust
$ brew install rust

## 安装完后调用下面的 rustc 命令应该能正确查看版本号
$ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09) (Homebrew)

最后参考上手 Visual Studio CodeVS Code 软件安装设置完成即可。

如果想方便地一键格式化自己写的代码,比如设置 TAB 键的空格数量等,可以继续安装 rustfmt

## 通过上面官方推荐方式 curl 安装的 Rust ,才有 rustup 这个命令
$ rustup component add rustfmt

## 如果是 Homebrew 安装的 Rust ,就要用 Homebrew 来安装 rustfmt
$ brew install rustfmt

完成安装之后根据 rustfmt 文档新建 rustfmt.toml 文件设置好想要的格式,即可在 VS Code 中右键点击 Format Document 或是快捷键:option + shift + F 来格式化代码。注意,rustfmt.toml 文件里的设置项分为 stable 和 unstable ,如果要开启 unstable 的设置项,需要使用 Nightly 版本的 Rust ,即最不稳定版。

也可以在终端中调用 rustfmt 命令来格式化:

$ rustfmt demo.rs

Rust 的基本操作

每次接触新的编程语言,基本都可以从「变量定义」,「函数调用」,「条件分支」,「类型」和「常见数据结构」这几个方向入手快速熟悉。至于语言的一些特有的特性,之后再根据需要补充学习即可。

由于 Rust 代码需要编译,加上期间产生的各式各样的文件,最好专门建一个文件夹 demo 来进行下面的编程操作。以便管理。

在文件夹 demo 中新建后缀为 .rs 的文件,比如 demo.rs ,之后编辑完代码,就能用 VS Code 里的 Code Runner 来一键调用上面安装的 rustc 完成编译并运行。关于 VS Code 的具体操作和设置参见上手 Visual Studio Code

// 这部分 allow 代码是消除 rust 编译器警告的信息
// 这样就能只看到代码打印出的结果,没有多余视线干扰
// 注意这不是全局生效的,对每一个需要的函数,都要写一次
#[allow(unused_variables)]
#[allow(dead_code)]
#[allow(unused_mut)]
#[allow(unused_imports)]
#[allow(unused_parens)]

// 和 Java 类似,Rust 需要一个 main 函数作为入口来运行其中的代码
// fn 相当于 function ,是定义函数的关键词
fn main()
{
  let x = 2 * 3;

  // 格式化字符串中的变量有时可以写在花括号里,有时不行
  // 所以统一写到外面比较好,统一视觉,避免写 "x = {x}"
  println!("x = {}", x);  

  // Rust 是需要写类型的,i32 代表 32 位带符号的整数,上面 x 没写是因为有类型推导
  // 「类型推导」其实是错误的语言设计,应该在定义处直接标记类型。方便阅读,且提高代码的表达能力
  // 「类型推导」会限制代码的表达能力,导致很多写法不能写,否则没法正确推导出这些写法下的数据类型
  // Rust 是系统编程的语言,每个类型的尺寸(占内存的空间)都是固定的
  let y: i32 = 1 + x;
  println!("y = {}", y);

  // 定义好的变量默认不能赋值修改
  // y = "9";
  // error: cannot assign twice to immutable variable

  // 用 mut 关键词来定义可赋值的变量(mut 表示 mutable)
  let mut z = 5;
  println!("first z = {z}");

  // 这样的写法不好,任何变量都应该给一个初始值才对
  // 设计得好的语言里面没有 variable declaration 这个概念
  // let mut z;

  z = 10;
  println!("first z = {z}");

  // 同名变量在 rust 里可以重复定义,且可以不同类型(这里同样有类型推导 type inferred)
  // 注意!Rust 里,此时 y 的类型不是 String ,而是 &str 类型,该类型是指向字符串的一个“引用”
  let y = "Hello";
  println!("y = {}", y);

  // 数组 - 默认也是不能更改,若要可更改则用 let mut a: [i32; 3] = [1, 2, 3]
  let a: [i32; 3] = [1, 2, 3];
  println!("a = {:?}", a);
  println!("a[0] = {}, a[1] = {}, a[2] = {}", a[0], a[1], a[2]);

  // 定义重复数组内容的方便写法 - 全是 5 的数组为例
  let b = [5; 20];
  println!("b = {:?}", b);

  // 元组 - tuples(可以存在不同类型的内容)
  // tuple 定义虽没有用 mut 关键词,但 t 的内容是可以赋值更改的:t.1 = True
  let t: (i32, bool, char) = (42, false, 'X');
  println!("t = {:?}", t);
  println!("t.0 = {}, t.1 = {}, t.2 = {}", t.0, t.1, t.2);
  println!("{}", (42, false, 'X').1);

  // 匿名函数:x => x + 1
  println!("(|x| x + 1)(5) = {}", (|x| x + 1)(5));

  // 定义 add1 函数 - 不标记(输入和返回)类型,依赖类型推导
  // Rust 是从下一行的 println! 里的调用 add1(5) 推导出来的
  let add1 = |x| x + 1;
  println!("add1(5) = {}", add1(5));   // 若无 add1(5),Rust 推不出类型会报错,除非标记类型

  // 标记类型的函数定义
  let add2 = |x: i32| -> i32 { x + 2 };
  println!("add2(5) = {}", add2(5));

  // 上面展示的类型推导有局限,比如 identity 函数
  let id = |x| x;
  println!("id(5) = {}", id(5));  // 正常输出 5

  // 但下面这行会报错,因为从上面的 id(5) 推导出输入类型是 i32 ,不能是 bool
  // 然而 id 函数应该是无论你输入什么类型的数据都能原封不动返回它
  // println!("id(5) = {}", id(True));  

  // 正确实现 identity 函数,需要用到「泛型」
  // 下面第 1 个 T 代表 T 是类型参数,它先会生成类似这样一个东西:
  // T => fn id2(x: T) -> T { return x; }
  // 类型 T 作为输入,然后构造出一个具有类型标记的函数 id2
  // 第 2 个 T 是输入参数 x 的类型;第 3 个 T 是函数 id2 的返回类型;
  fn id2<T>(x: T) -> T { x }
  println!("id2(5) = {}", id2(5));
  println!("id2(true) = {}", id2(true));

  // curried functions: x => y => x + y
  // 这里需要用到关键词 move 才能让 y 取到 3 这个操作数,这和 Rust 的内存管理有关
  // Rust 制造了这些麻烦设计,是为了能够不用垃圾回收(Garbage Collection)这一语言特性
  println!("(|x| (move |y| x + y))(2)(3) = {}", (|x| (move |y| x + y))(2)(3));

  // 条件分支
  let x = 3;
  if x < 5 {
    println!("condition was true");
  } else {
    println!("condition was false");
  }

  fn fib(n: u64) -> u64
  {
    if (n == 0)
    {
      return 0;
    }
    else if n == 1
    {
      return 1;
    }
    else
    {
      return fib(n - 1) + fib(n - 2);
    }
  }

  println!("fib(8) = {}", fib(8));

  // while loop
  let mut total = 0;
  let mut x = 1;

  while x <= 10 {
    total += x;
    x += 1;   // Rust 没有 ++ 或 -- 操作,这是个正确的设计。好的语言里不应该有这种东西,可避免一些愚蠢的写法
  }

  println!("total = {}", total);


  // for loop
  let mut a = [2, 3, 5, 7, 11, 13, 17];  // a 的类型为 [i32; 7]
  // 由于数组 a 的内容是 i32 整数
  // 所以定义 type_test 变量会 copy 数组内容,而不会 move ,和 i32 一样
  // 如果内容是 String 字符串,那 let type_test: [String; 7] = a; 就会 move 了
  let type_test: [i32; 7] = a;           
  for x in a {
    println!("array element: {}", x);
  }

  // range (like Python)
  for x in 1..5 {
    println!("range 1..5: {x}");
  }

  for x in 1..=5 {
    println!("range 1..5: {x}");
  }

  // reverse range
  for x in (1..5).rev() {
    println!("(1..5).rev() range: {x}");
  }

  // vector - 类似 Java 中的 ArrayList ,长度不固定,可添加数据(上面的数组长度固定不能添加)
  // 变量 v 的类型是 Vec<i32>,且注意就算内容是 i32 ,也不能像上面的 a 变量那样默认 copy
  // let type_test: Vec<i32> = v; 会发生 move
  let mut v = vec![1, 2, 3, 4, 5];   

  // 这里如果不用 &v 而采用 v ,虽然仍然能顺利输出,但会发生 move ,后续 v 变量就不能用了
  for x in &v {
    println!("vector element: {x}");
  }

  // let x = &v[3];   // can't borrow
  let x = v[3];       // But can copy

  v.push(6);
  for x in &v {
    println!("changed vector element: {x}");
  }

  println!("v[3] = {}", x);  // for 循环中的 x 在循环结束后就没了,这里的 x 是上面的 let x = v[3];

  // access by index - 下标访问
  println!("v[2] = {}", v[2]);


  // 标记类型的 vector
  let mut v2: Vec<String> = vec!["a".to_string(), "b".to_string(), "c".to_string()];
  // let x = v2[1];  // 这样写会报错;和 Rust 内存管理有关 - cannot move out of index of `Vec<String>`
  let x = &v2[1];  

  println!("Vec<String> v2[1] = {}", x);


  // hash map - 数据结构,属于库函数,所以先要用 use 来加载一下,相当于 Java 的 import
  println!("----- hash map -----");
  use std::collections::HashMap;

  // 注意这里没有声明 table 类型
  // Rust 会从后面的 table.insert("one", 1); 推导出 table 的类型
  // table 的完整类型是 HashMap<&str, i32>
  // 为什么整数部分是 i32 而不是 i64 ?其实 Rust 编译器无法确切地知道是 i32 还是 i64
  // Rust 编译器会根据上下文和已知的类型信息尝试找到最适合的类型,以满足所有 insert 的值的类型
  // 下方 insert 的值 1 、2 、3 通常在 Rust 中默认为 i32 类型,所以这里是 i32
  // 但是如果在其他部分的代码中,插入了 i64 类型的值,那么编译器则会认为是 HashMap<&str, i64>
  // 「类型推导」是错误的设计,会限制语言的表达,正确做法应该在定义的地方显式标记类型
  let mut table = HashMap::new();  

  table.insert("one", 1);
  table.insert("two", 2);
  table.insert("three", 3);

  // 可以通过 key 访问 table 的值
  println!("table[\"one\"] = {}", table["one"]);

  // 但如果 key 来访问的值不存在,程序会报错中断
  // println!("table[\"four\"] = {}", table["four"]);

  // 所以更合理的方式是调用 get 函数配合 {:?} 来尝试取值,这样程序不会中断
  println!("table[\"four\"] = {:?}", table.get("four"));         // table["four"] = None

  // get 函数返回的是一个 option type 类型的值,比如这里的 Some(2)
  // 这里的 option 类型表示为 Option<&{integer}> ,意思是「有可能是 integer 或者 None(没有)」
  // Rust 用这个 option 设计避免了 null pointer exception 之类的问题
  // Rust 是不会出现 null pointer exception 的
  println!("table[\"two\"] = {:?}", table.get("two"));           // table["two"] = Some(2)

  // Tips: 直接打印 None 会报错
  // println!("None: {:?}", None); 

  // 需要添加类型 T 即 None::<T> 才能顺利打印出 None: None
  // 这说明 table.get("four") 得到的 None 其实是 None::<i32> ,编译器类型推导出了这个 T
  println!("None: {:?}", None::<i32>);  

  // 处理这种 option 类型可以用 match ,对不同的取值进行不同的操作
  match table.get("three") 
  {
    Some(value) => println!("value: {}", value),   // value 在这里直接被取出
    None => println!("one: not found"),
  }

  // match 是一种程序语言里通用的构造,有点像 if 的那个分支的构造


   // struct - 结构体,类似面向对象里的「对象」
  struct User {
    username: String,
    email: String,
    active: bool,
  }

  // struct instance
  let mut user1 = User {
    username: String::from("user1"),
    email: String::from("user1@example.com"),
    active: true,
  };

  // field access - 访问成员
  println!("user1: username = {}, email = {}", user1.username, user1.email);

  // field mutation - 对成员赋值
  user1.email = String::from("other1@example.com");
  println!("changed user1: username = {}, email = {}", user1.username, user1.email);

  // struct constructor - 构造函数来创建 struct instance
  fn build_user(email: String, username: String) -> User 
  {
    User {  // 注意这里没有写 return ,Rust 会自动隐式返回最后一个表达式的值
            // 除非表达式有分号或者没有表达式。表达式末尾有分号就要显示地写 return

        // 在 Rust 语法里,这里可以就只写 username 和 email,值也能正确对应
        // 但这样并不好,还是写 username: username 这样看起来更直观逻辑更清楚
        // 少打几个字,增加逻辑思考的负担,是不明智的
        // username,   
        // email,
        // active: true,

        username: username,   
        email: email,
        active: true,
    }  // 这里不能加分号,否则函数 build_user 无法隐式返回,只能写 return
  }

  let user2 = build_user(String::from("user2@example.com"), String::from("user2"));
  println!("user2: username = {}, email = {}", user2.username, user2.email);

  // 一种可能可以提供方便的构造语法
  // user3 和 user1 只有 email 的值不同,其他都相同,就可以通过这种方法构造 user3
  let user3 = User {
    email: String::from("user3@example.com"),
    ..user1
  };

  println!("user3 (created from user1): username = {}, email = {}", user3.username, user3.email);


  // tuple struct
  // 和上面的 struct 区别在于,tuple struct 的成员是没有名字的,要靠位置(下标)来访问
  // tuple struct 一般用于最简单的构造,比如后续的 enum 会用到。日常还是用上面那种有成员名字的 struct
  struct Color(i32, i32, i32);
  struct Point(i32, i32, i32);

  let black = Color(1, 2, 3);
  let origin = Point(2, 3, 5);

  println!("black = ({}, {}, {})", black.0, black.1, black.2);
  println!("origin = ({}, {}, {})", origin.0, origin.1, origin.2);

  // Unit-like struct
  // 这种 struct 内部没有数据,它就是它自己而已
  #[derive(Debug)]  // 给 AlwaysEqual 自动实现 Debug trait ,以便能够用 {:?} 格式化符号打印它的信息
  struct AlwaysEqual;

  let a = AlwaysEqual;
  println!("a = {:?}", a);

  // {} 和 {:?} 是 println! 两种常用的格式化符号
  // {} 是默认的显示格式,用于打印实现了 Display trait 的类型,类似 .to_string() 函数
  // {:?} 是调试输出格式,用于打印实现了 Debug trait 的类型,旨在提供详细输出,包含类型的更多内部细节
  // 使用 #[derive(Debug)] 可以方便地实现 Debug trait ,所以类型的调试输出一般用这个
  // #[derive(Debug)] 需要直接应用于每个需要它的结构体或枚举上。它不是全局性的,不能应用一次就影响整个模块或包
  // 这意味着每个想用 {:?} 格式化输出的类型都要单独写一次 #[derive(Debug)]
  // 注意这个 derive 只能用于 struct ,enum 和 union

  // methods - Struct 的方法是定义在外面的
  // 这里给 Point 这个 struct 添加(定义)方法
  // 关键词 impl 表示 implementation
  impl Point
  {
    // 注意,函数(方法)的第一个参数是 self(参数名只能是 self 不能是 this 或其他),它的类型是 &Self
    // 第一个参数 self: &Self 也可以简写为 &self ,可省略额外的类型标记
    fn distance(self: &Self, other: &Point) -> f64
    {
      let dx = self.0 - other.0;
      let dy = self.1 - other.1;
      let dz = self.2 - other.2;

      ((dx * dx + dy * dy + dz * dz) as f64).sqrt()
    }
  }

  let p1 = Point(1, 2, 3);
  let p2 = Point(4, 5, 6);
  println!("distance between p1 and p2 = {}", p1.distance(&p2));


  // enum (like abstract class in OOP, sum type in FP) -  枚举,类似 Java 中的 abstract class
  // 这里定义了一个 Mybool 类型,它包含 2 个子类型 MyTrue 和 MyFalse
  enum MyBool
  {
    MyTrue,    // Unit-like struct
    MyFalse
  }

  let x = MyBool::MyTrue;
  match x
  {
    MyBool::MyTrue => println!("x is true"),
    MyBool::MyFalse => println!("x is false")
  }

  println!("----- lists -----");

  // // 用 enum 定义链表:
  // // 注意这里用了泛型:可理解为类型 T 作为参数的一种类型函数
  // // 这样不同类型就能通过 T 被传入进而得到内容类型不同的 List
  // // 类似 List:T => enum List { Pair(T, List), Nil}
  // // 这里意思是:List<T> 要么是空链表 Nil ,要么是一个 Pair 装了一个 T 类型的数据和另一个链表 List<T>
  // // 但类似 Pair(T, List<T>) 这样的写法在 Rust 里是不行的,因为这里涉及了递归
  // // Rust 的内存管理不知道要为递归分配多少内存空间,所以会报错
  // enum List<T>
  // {
  //   Pair(T, List<T>),
  //   Nil
  // }

  // 在 Rust 里,递归的部分要放在一个 Box<...> 里,即 Box<List<T>> ,才能正确运行
  // 这个 Box 相当于其他语言里的「指针」,它的大小是固定的
  // 所以这里放进去的是 T 类型的数据,和一个指向另一个 List<T> 的指针
  // 于是 Rust 就能确定它的空间大小,从而顺利分配内存 - Rust 是系统语言,必须要知道每个东西占多少空间
  enum List<T>
  {
    Pair(T, Box<List<T>>),
    Nil
  }

  // length 函数,计算输入链表的长度
  fn length<T>(ls: &List<T>) -> usize
  {
    match ls
    {
      List::Pair(_, tail) => 1 + length(tail),
      List::Nil => 0,
    }
  }

  let ls1 = List::Pair(1, Box::new(List::Pair(2, Box::new(List::Pair(3, Box::new(List::Nil))))));
  println!("list length = {}", length(&ls1));

  // sum 函数,计算输入链表所有成员的和
  fn sum(ls: &List<i32>) -> i32
  {
    match ls
    {
      List::Pair(head, tail) => head + sum(tail),
      List::Nil => 0,
    }
  }

  println!("list sum = {}", sum(&ls1));

  println!("----- calculator -----");

  // enum for arithmetic expressions - 用 enum 来构造算术表达式
  // 算术表达式 exp 要么是一个 Lit 要么是一个 Binop
  // 同样地,递归的部分放进 Box 里
  enum Exp
  {
    Lit(i32),    // tuple struct
    Binop(char, Box<Exp>, Box<Exp>)
  }

  // calculator
  fn calc(exp: Exp) -> i32
  {
    match exp
    {
      Exp::Lit(n) => n,
      Exp::Binop(op, e1, e2) =>
      {
        // * 代表 dereferencing ,即读取“引用”对应的值
        // 顺便说一下 & 符号用于创建“引用”,而 * 符号用于“解引用”,获取其所指向的值
        // 注意,我把所有的「引用」和「指针」都打了引号,代表 Rust 的引用和 C 语言、C++ 的引用(指针)概念不一样
        // & 和 * 只能暂时近似理解为「创建引用」和「解引用」的效果,不能认为是等同的概念
        // 之后会有例子暴露 Rust 的问题:&&String 和 &String 的类型是否一样
        // 准确的理解应该抛弃 C 语言的指针思维,进一步还得查看 Rust 官方文档关于 ownership 的说明
        // https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
        // 由于这里的 e1 和 e2 都是 Box<Exp> ,上面提到 Box 其实是“指针”,所以要通过 * 把对应的值取出来
        let v1 = calc(*e1);  
        let v2 = calc(*e2);

        match op
        {
          '+' => v1 + v2,
          '-' => v1 - v2,
          '*' => v1 * v2,
          '/' => v1 / v2,
          _ => panic!("Unknown operator")
        }
      }
    }
  }

  let exp1 = Exp::Binop('*', Box::new(Exp::Lit(2)), Box::new(Exp::Lit(3)));
  println!("2 * 3 = {}", calc(exp1));

  let exp2 = Exp::Binop('+', Box::new(Exp::Lit(1)), Box::new(Exp::Binop('*', Box::new(Exp::Lit(2)), Box::new(Exp::Lit(3)))));
  println!("1 + 2 * 3 = {}", calc(exp2));

  // 下面是另一个版本的计算器 calc2
  // 主要区别在于表达式的定义。
  // 上面的表达式 Exp 是 tuple struct ,里面的成员没有名字
  // 而下面的 NamedExp 则是常见的 struct ,里面的成员是有名字的
  // 推荐写法还是下面这种有名字的 struct ,也就是 NamedExp
  // 注意两者的括号类型也有区别
  // 可以具体观察对比 exp1 和 exp3 的定义内容

  // with named fields
  enum NamedExp
  {
    Lit { n: i32 },
    Binop { op: char, e1: Box<NamedExp>, e2: Box<NamedExp> }
  }

  fn calc2(exp: NamedExp) -> i32
  {
    match exp
    {
      NamedExp::Lit { n } => n,

      // free ordering of fields
      NamedExp::Binop { e1, e2, op }=>
      {
        let v1 = calc2(*e1);
        let v2 = calc2(*e2);

        match op
        {
          '+' => v1 + v2,
          '-' => v1 - v2,
          '*' => v1 * v2,
          '/' => v1 / v2,
          _ => panic!("Unknown operator")
        }
      }
    }
  }

  let exp3 = NamedExp::Binop { op: '*', e1: Box::new(NamedExp::Lit { n: 2 }), e2: Box::new(NamedExp::Lit { n: 3 }) };
  println!("2 * 3 = {}", calc2(exp3));

  // option types (already defined in Rust standard library)
  // enum Option<T> {
  //   Some(T),
  //   None,
  // }

  // 前面提到的 Option 类型其实也就是一个 enum ,这里我们自己定义一个 MyOption 感受一下
  enum MyOption<T> {
    Some(T),
    None,
  }

  let x: MyOption<i32> = MyOption::Some(5);
  let y: MyOption<i32> = MyOption::None;

  println!("----- MyOption -----");

  match x {
    MyOption::Some(n) => println!("x = {}", n),
    MyOption::None => println!("x is None"),
  }

  match y {
    MyOption::Some(n) => println!("y = {}", n),
    MyOption::None => println!("y is None"),
  }

}

Rust 的内存管理

Rust 最重要的功能就是「内存管理」。它和其他语言(比如 Java)不一样。因为它没有垃圾回收(GC) 。它是靠编译器的静态检查来实现内存管理的。Rust 可以静态地保证大部分的内存不会出问题。

Rust 的内存管理导致有很多代码写法上的限制,使得 Rust 写一些复杂的代码会有点痛苦。这就是为啥这个语言不适合作为初学者的第一门语言。因为太多这个内存管理的东西夹在里面。你就没法思考你的那些逻辑。写一个链表都要想想 Box

总的来说,核心思路有 3 个:

  • 拥有数据所有权 ownership 的变量,才能确保数据不会随便失效;borrow 来的数据,别人失效了,你就失效了

  • move 转移 ownership 会让原变量失效,borrow 不会,要思考是否希望(允许)原变量失效

  • 要注意什么情况下会 move ,什么情况下只是 borrow ,以及这两种情况下数据如何读取、如何修改

  // ......
  // ...... 下面这些代码是接着上文的代码写在 main 函数的函数体里的
  // ......

  // ---------------------------- Ownership ----------------------------

  println!("--------------- Ownership ---------------");

  let x = 5;
  let y = x;  // copy (because i32 is Copy)
  println!("x: {}, y: {}", x, y);

  // Example 1: String
  let s1 = String::from("hello");
  let s2 = s1;                     // move - move 之后变量 s1 就失效了
  println!("s2: {}", s2);
  // error: value borrowed here after move
  // println!("s1: {}", s1);

  // Rust 里的传递基本都是 move ,只有专门标记 & 才是 borrow
  // 函数调用的操作数也是发生 move ,例如  f(s2) 后,s2 就失效了(下文有描述)
  // 因为你把值传递给函数的参数,本质和变量定义没有差别

  // clone
  println!("----- clone -----");
  let s1 = String::from("hello");
  let s2 = s1.clone();
  println!("s1: {}", s1);
  println!("s2: {}", s2);

  // borrow
  println!("----- borrow -----");
  let s1 = String::from("hello");
  let s2 = &s1;
  println!("s1: {}", s1);
  println!("s2: {}", s2);


  // function that takes ownership
  fn take_ownership(s: String)
  {
    println!("take_ownership: {}", s);
  }

  let s = String::from("hello");
  take_ownership(s);
  // error: value borrowed here after move
  // println!("s: {}", s);


  // function that gives ownership
  fn give_ownership() -> String
  {
    let s = String::from("hello");
    println!("give_ownership: {}", s);
    return s;
  }

  let s = give_ownership();
  println!("s: {}", s);

  fn take_and_give_back(s: String) -> String
  {
    println!("take_and_give_back: {}", s);
    return s;
  }

  let s = String::from("hello");
  let s1 = take_and_give_back(s);
  println!("s1: {}", s1);
  // borrow of moved value: `s`
  // println!("s: {}", s);


  // function that borrows
  fn borrow(s: &String)
  {
    println!("borrow: {}", s);
  }

  let s = String::from("hello");
  borrow(&s);
  println!("s: {}", s);


  // mutable borrow
  fn mutate(s: &mut String)
  {
    s.push_str(", world");
  }

  let mut s = String::from("hello");
  mutate(&mut s);
  println!("after mutatable borrow, s: {}", s);

  // only one mutable borrow is allowed
  // let s1 = &mut s;
  // let s2 = &mut s;
  // println!("s1: {}, s2: {}", s1, s2);
  // cannot borrow `s` as mutable more than once at a time

  let s1 = &mut s;
  println!("s1: {}", s1);

  // can borrow because s1 is no longer used later
  let s2 = &mut s;
  println!("s2: {}", s2);
  // println!("s1: {}", s1);


  // can't have immutable and mutable borrows at the same time
  // let s1 = &s;
  // let s2 = &mut s;
  // println!("s1: {}, s2: {}", s1, s2);

  // fn dangle() -> &String {
  //   let s = String::from("hello");
  //   return &s;
  // }

  // string slice is borrowed reference
  let s = String::from("hello");
  let slice1 = &s[0..2];

  // will not compile with this line
  // let s1 = s;
  println!("slice1: {}", slice1);


  // Generics
  println!("----- Generics -----");

  // revisit id function
  fn id3<T>(x: T) -> T { x }
  println!("id3(5) = {}", id3(5));
  println!("id3(true) = {}", id3(true));


  // Two arguments must be the same time
  fn second1<T>(x: T, y: T) -> T
  {
    return y;
  }

  println!("second1(1, 2) = {}", second1(1, 2));
  println!("second1(true, false) = {}", second1(true, false));
  // error: mismatched types
  // println!("second1(1, true) = {}", second1(1, true));


  // Two arguments can be different types
  fn second2<T, U>(x: T, y: U) -> U
  {
    return y;
    // Change to return x will not compile
    // return x;
  }

  println!("second2(1, 2) = {}", second2(1, 2));
  println!("second2(true, false) = {}", second2(true, false));
  println!("second2(1, true) = {}", second2(1, true));


  // Lifetime (very similar to generics)
  // Lifetime is a way to specify relationship between references

  println!("----- Lifetime -----");

  // Won't compile without lifetime specifier (why?)

  // fn second_string1(s1: &String, s2: &String) -> &String
  // {
  //   return s2;
  // }


  // broad lifetime (not knowing exact which one is returned)
  fn second_string2<'a>(s1: &'a String, s2: &'a String) -> &'a String
  {
    return s2;
  }

  let s1 = String::from("s1");
  let s2 = String::from("s2");
  let s3 = second_string2(&s1, &s2);

  // s1 moved here, but compiler doesn't know s3 is not referring to s1
  // compiler thinks s3 could refer to both s1 and s2
  // let s4 = s1;
  println!("s3: {}", s3);


  // This one works because we know s3 refers to s2, not s1
  fn second_string3<'a,'b>(s1: &'a String, s2: &'b String) -> &'b String
  {
    return s2;
    // Change to return s1 will not compile
    // return s1;
  }

  let s1 = String::from("s1");
  let s2 = String::from("s2");

  let s3 = second_string3(&s1, &s2);

  // s1 moved here, but compiler konws know s3 is not referring to s1
  let s4 = s1;
  println!("s3: {}", s3);


  // We don't know which is longer string
  // so lifetime of returned reference could be either of s1 or s2
  fn longer_string<'a>(s1: &'a String, s2: &'a String) -> &'a String
  {
    if s1.len() > s2.len()
    {
      return s1;
    }
    else
    {
      return s2;
    }
  }

  let s1 = String::from("hello");
  let s2 = String::from("world!");
  let s3 = longer_string(&s1, &s2);

  // s1 moved here, and compiler knows s3 could refer to s1 or s2
  // so uncommenting either of the following lines will not compile
  // let s4 = s1;
  // let s4 = s2;
  println!("longer string: {}", s3);


  // Lifetime in struct
  println!("----- Lifetime in struct -----");

  // Error: missing lifetime specifier
  // struct Person {
  //   name: &String,
  //   email: &String
  // }

  // Struct 里标记 lifetime 是 Rust 复杂多余的设计
  // 这意味着这个 Struct 只能临时用一下
  // 一旦出了 name1 和 email1 的作用域,p1 就用不了了
  // 正常的 Struct 都应该是内部成员的 owner 而不是 borrow
  // 这和函数参数的 lifetime 是不一样的
  // GitHub 上的 Rust 项目,只有【极少数】的 Struct 里存在这种 reference
  // 所以建议 struct 的成员不使用 & ,即不使用 reference 这样的引用成员

  struct Person<'a> {
    name: &'a String,
    email: &'a String
  }

  let name1 = String::from("name1");
  let email1 = String::from("email1");
  let p1 = Person { name: &name1, email: &email1 };

  // name1 moved here, compiler knows p1 contains reference to name1 and email1
  // so uncommenting either of following lines will not compile
  // let name2 = name1;
  // let email2 = email1;
  println!("p1: name = {}, email = {}", p1.name, p1.email);


  // Lifetime of hashmaps
  println!("----- Lifetime of hashmaps -----");
  let mut table = HashMap::new();      

  // 这里 HashMap 的类型应为 HashMap<String, String>
  // 但 Rust 是通过下面的 table.insert(key1, value1); 推导出这个类型的
  // 这是个错误的设计,不应该从调用的地方反推 HashMap 类型,应该在定义的时候就写清楚
  // 逻辑上,类型标记本来就是要检查后续数据是否正确使用,而现在却要从后续数据的使用推导,万一后续数据用错了呢?
  // 此外,定义的时候不写类型,就要用人脑来推导一下才能理解代码,大大降低了代码可读性,增加理解难度

  let key1 = "key1".to_string();
  let value1 = "value1".to_string();

  table.insert(key1, value1);

  // Can no longer use key1 and value1
  // println!("key1: {}", key1);
  // println!("value1: {}", value1);


  // boxes
  println!("----- boxes -----");
  let x = 5;
  let y = Box::new(5);    // Box 相当于指针,Box::new(5) 效果相当于创造一个指向 5 的指针:&5
  // let y = &x;          // also work
  println!("y: {}", y);

  // 被 box 封装的数据无法在其他位置被更改,只能在所有权变量处更改,无法从其他变量访问更改
  // 因为如果从其他变量修改,那原始变量就会失效
  // box 独占封装的数据,不能像 Rc 引用计数那样共享数据,比如 2 个 box 实现的 list 无法共享其中部分数据
  // 比如 box 实现的 ls1 中一部分是 ls2 ,则 ls2 会发生 move 转移 ownership ,这将使 ls2 变量失效不可用

  // assert_eq! 接受两个参数(表达式),比较它们是否相等。若不相等,程序将 panic,并显示两个不匹配值的信息
  // 主要用于测试中,确保代码在开发和维护过程中符合预期行为
  assert_eq!(5, x);
  assert_eq!(5, *y);

  // Box 会转移所有权(ownership) ,即 move 操作
  // 下面 Box::new(s) 使得 s 的所有权转移给了 Box ,b 这个 Box 是 "hello" 的新 owner
  // 所以下一行 move s 给 s1 的操作就会报错
  // 后续的 Rc (Reference Counted) 也是同样的这个行为,Rc::new(x) 也会拥有 x 的数据
  let s = String::from("hello");
  let b = Box::new(s);
  // let s1 = s;
  // error: use of moved value: `s`


  // Define smart pointer - 可以认为 Box 和 Rc 都是 Smart Pointer 和 C++ 的原理一样
  // 提示:MyBox 是上文提到的 tuple struct ,里面只存有一个数据,通过下标来访问,如 mb.0
  struct MyBox<T>(T);

  impl<T> MyBox<T> {
      fn new(x: T) -> MyBox<T> {
          MyBox(x)
      }
  }

  let x = String::from("hello");
  let y = MyBox::new(x);      // 这行把 x 数据 move 给了一个 MyBox ,然后这个 MyBox 的 owner 则是 y
  // let z = x;               // error: use of moved value: x

  // 使用 C++ 的代码里的 unique_ptr 可以实现同样的效果
  // 不过 C++ 中要明确写出是在 move ,即 move(x) ,而 Rust 中默认就是 move 操作
  // unique_ptr<string> x = make_unique<string>("hello");
  // unique_ptr<string> z = move(x);

  let x = 5;
  let y = MyBox::new(x);
  // let z = MyBox(x);    // also work

  // 直接使用 MyBox(x) 构造实例的方式直接通过类型的构造器初始化,适用于简单的包装或当不需要额外逻辑处理的情况。
  // 这通常更简单,但功能上可能较为有限,主要用于简单地包装或转换类型。
  // MyBox::new(x) 是一个更常见的构造函数模式,其中 new 是一个静态方法,用于创建并初始化类型的实例。
  // 这种方式可以包含更复杂的初始化逻辑。如设置默认值、进行验证或其他必要的设置步骤。

  use std::ops::Deref;

  // Deref Trait - trait 相当于 Java 中 interface 的概念,即要求实现 interface 中规定的方法,如 get 和 add 等
  impl<T> Deref for MyBox<T> {
    type Target = T;   // 指定 Target 为 T 类型

    // Self::Target 是 Deref trait 的一个关联类型,表示被解引用时得到的目标类型,即 self.0 的类型
    // 所以,这里 deref 函数返回的 &self.0 是 &Self::Target 类型,& 代表它是个引用的类型
    fn deref(&self) -> &Self::Target {
        &self.0
    }

    // C++ 中类似的 smart pointer 定义
    // T& operator * () const { *return m_ptr; }
    // T* operator -> () const { return m_ptr; }
  }

  // 当使用 * 符号的时候,其实就是在调用这个 deref 函数
  // 所以 *y 相当于 y.deref() ,从上面的代码得知此时返回的是一个引用,即 &self.0
  // 然后 Rust 又会再自动地隐式解引用一次,于是得到嘞 self.0 ,也就是真正的值
  assert_eq!(5, x);
  assert_eq!(5, *y);  // equiv to *(y.deref()) , *y is &i32 type
  assert_eq!(5, *(y.deref()));

  fn foo(x: &i32) {
    println!("x: {}", x);
  }

  foo(&y);


  // Drop trait
  println!("----- Drop trait -----");

  struct Pointer {
    data: String,
  }

  // 这个 drop trait 相当于 C++ 里的 destructor
  impl Drop for Pointer {
    fn drop(&mut self) {

      // drop 出了作用域后就会打印这些信息作为 debug 信息,让人知道确实 drop 掉了
      println!("Dropping Pointer with data `{}`!", self.data);
    }
  }

  let a = Pointer {
    data: String::from("a"),
  };

  {
    let b = Pointer {
        data: String::from("b"),
    };
  }

  drop(a);

  let c = Pointer {
      data: String::from("c"),
  };

  let d = Pointer {
      data: String::from("d"),
  };

  println!("Pointers created.");


  // Rc (Reference Counted) - 引用计数
  println!("----- Rc -----");

  // shared list can't work with Box - Box 的链表实现无法共享数据
  // let a = List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))));
  // let b = List::Cons(5, Box::new(a));  // a moved here
  // let c = List::Cons(8, Box::new(a));  // can't use a again

  // Rc 的主要功能就是共享数据,也就是 Rc::clone 这个操作
  // Rc::clone 不会复制数据,只会复制一个引用
  // Rc::clone 的参数类型也必须是一个 Rc 类型
  use std::rc::Rc;

  enum List2 {
    Cons(i32, Rc<List2>),
    Nil,
  }

  let a = Rc::new(List2::Cons(2, Rc::new(List2::Cons(3, Rc::new(List2::Nil)))));
  let b = Rc::new(List2::Cons(5, Rc::clone(&a)));    // Rc::clone(arg) 接收的参数类型 arg 必须是一个 Rc 的引用:&Rc<_>
  let c = Rc::new(List2::Cons(8, Rc::clone(&a)));    // 这里 a 是一个 Rc ,所以 &a 就是一个 &Rc<_>

  println!("a's ref count: {}", Rc::strong_count(&a));  // 查看变量 a 的引用次数
  println!("b's ref count: {}", Rc::strong_count(&b));
  println!("c's ref count: {}", Rc::strong_count(&c));
  drop(b);
  drop(c);

  // 释放 b 和 c 之后,a 的引用次数减少
  println!("a's ref count after dropping b, c: {}", Rc::strong_count(&a));  

  // 虽然 Rc::clone 可以让我们在多个地方共享数据,但它并不提供数据的内部可变性
  // 以下面这个 struct 为例,使用 Rc 之后,无法更改成员 name 的值
  #[derive(Debug)]
  struct TestRc
  {
    name: String,
    age: i32,
  }

  let mut tr1 = TestRc 
  {
    name: String::from("user1"),
    age: 19,
  };

  println!("original tr1: {:?}", tr1);

  tr1.name = String::from("guest");   // 可更改 tr1 内部成员 name 的内容
  println!("changed tr1: {:?}", tr1);

  // 套上 Rc 封装之后,就无法更改 tr1 内部数据的内容了
  // 注意这里 tr1 发生 move 了,tr1 失效,新 owner 是 Rc ,而 Rc 的 owner 是变量 rc_tr1
  let rc_tr1 = Rc::new(tr1);

  // rc_tr1.name = String::from("guest_2nd");   // cannot assign to data in an `Rc
  // *rc_tr1.name = String::from("guest_3rd");  // mismatched types


  // 如果希望用 Rc::clone 共享数据的同时,还能够改变变量内部数据,就要配合 RefCell 使用
  // RefCell - 本质相当于 Reader-Writer Lock(读写锁)
  // 可以有 2 个(多个)immutable 的 borrow(Reader)
  // 但只能有 1 个 mutable 的 borrow(Writer)
  println!("----- RefCell -----");
  use std::cell::RefCell;

  // Reader-Writer Lock(读写锁)是一种常用的同步机制,用来解决多个线程同时访问同一资源(如数据或文件)时的并发问题。
  // 读写锁非常适合那些读操作远多于写操作的场景,因为它允许多个读线程同时访问资源,而写线程则需要独占访问。
  // 与读写锁并列的还有互斥锁(mutex)等

  let value = RefCell::new(42);

  // 可修改的 mut_borrow 相当于 writer
  // 如果一个 writer 把一个对象锁掉了,那所有的 reader 就都没法读取了
  let mut mut_borrow = value.borrow_mut(); // 调用 .borrow_mut 函数,并加关键词 mut 来定义
  println!("value: {}", *mut_borrow);

  *mut_borrow = 9;                         // 改变指针(引用) mut_borrow 所指向的数据
  println!("value: {}", *mut_borrow);

  drop(mut_borrow);   // 释放 mut_borrow 后,下面才能调用 .borrow() ,因为读写锁不能同时读和写

  // 不可修改的 imm_borrow 相当于 reader
  // 同理,如果一个 reader 拿到了锁,那 writer 就拿不到这个锁(没法写入)
  // 但是其他的 reader 也能拿到这个锁
  let imm_borrow1 = value.borrow();
  println!("value: {}", *imm_borrow1);

  let imm_borrow2 = value.borrow();
  println!("value: {}", *imm_borrow2);

  // Rc 配合 RefCell 可以让我们既可以 Rc::clone 共享数据,又可以 .borrow_mut 来改变数据
  // 这对复杂一点的数据结构来说是必要而实用的工具,比如解释器里的数据结构
  // 不过新手也许还有个疑问:封装到底是谁包裹谁?是 Rc<RefCell<_>> 还是 RefCell<Rc<_>> ?
  // 其实从上面 Rc 部分的例子就能知道,我们可以改动 tr1 ,但无法改动 Rc<tr1>
  // 对于 RefCell<Rc> ,当我们调用 .borrow_mut() 之后,拿到的就是 Rc<_> ,而我们无法改动 Rc<_>
  // 对于 Rc<RefCell<_>> ,调用 .borrow_mut() 后拿到的是数据,可以被更改
  // 所以正确的封装顺序应该是 Rc<RefCell>
  println!("----- Rc with RefCell -----");

  let tr2 = TestRc     // 这里定义 tr2 不需要 mut
  {
    name: String::from("user2"),
    age: 17,
  };

  let tr2_rfrc = Rc::new(RefCell::new(tr2));  // tr2 发生 move 转移所有权,新 owner 是 RefCell

  println!("tr2_rfrc: {:?}", tr2_rfrc.borrow());
  // tr2_rfrc: TestRc { name: "user2", age: 17 }

  tr2_rfrc.borrow_mut().name = String::from("admin");  // 改变 name 成员(field)的值

  println!("changed tr2_rfc: {:?}", tr2_rfrc.borrow());
  // tr2_rfrc: TestRc { name: "admin", age: 17 }


  // 有了 RefCell 和 Rc,我们还可以造出一个「环」来:
  // A -> B -> A( A 指向 B ,B 又指向 A )
  // 这样内存就可能会泄露了,因为 Rc(Reference Counting)互相指来指去就不会为 0 了
  // 这会使得内存就会无法释放,导致泄露(Memory Leak)
  // 不过正常的垃圾回收机制可以正确地处理这类「循环引用」导致的问题
  // reference cycles
  println!("----- reference cycles -----");
  struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
  }

  // 定义 node1 和 node2 两个被 Rc 和 RefCell 封装包裹的 Node
  let node1 = Rc::new(RefCell::new(Node {
      value: 1,
      next: None,
  }));

  // 这里 Node2 的成员(field)next 通过 Rc::clone 指向了 node1
  let node2 = Rc::new(RefCell::new(Node {
      value: 2,
      next: Some(Rc::clone(&node1)),
  }));

  // 由于有 RefCell ,所以可以使用 borrow_mut
  // 这里 borrow_mut 取得一个指向 node1 数据且可以改变这些数据的东西
  // 然后 .next = Some(Rc::clone(&node2)) 让 node1 中本来为 None 的值换成了 Node2
  // 这样 node1 和 node2 就相互指向了
  // creating cycle
  node1.borrow_mut().next = Some(Rc::clone(&node2));

  // node2 也可以指向自己
  node2.borrow_mut().next = Some(Rc::clone(&node2));

} // <--- 这个是 main 函数的花括号

打赏