Rust 入门

    Programming Language

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

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

Rust 的这些 ownershiplifetime 之类的静态检查设计,主要目的是静态地管理内存的分配和释放。在编译环节而不是运行时环节就解决内存问题,进而减少运行时垃圾回收(GC)的计算开销。不过到了复杂的数据结构,或者说必须用 GC 的地方,基本上还是得用 Rust 的 Rc(引用计数),比如解释器。只是比较简单的那一部分代码你可以不用 Rc

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

C++ 稍微“扩展”一下 unique_ptr,也能实现 Rust 的内存管理精髓。但若没有 Rust 这套内存规则的作为大方向指导我们的思路,那我们可能就会不知道如何有效地使用这些 unique_ptr 等指针。不过像这种 ownership 的思路可能在 C++ 里就已经有了(unique_ptrshared_ptr),只不过 C++ 没有类型系统来检查它:比如 C++ 中指针 p1move 之后,仍然能使用,只不过 p1 指向的地址是 0 而已。

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

新的理解包括也许除了垃圾回收(GC)和引用计数(RC),内存管理没有其它更好更简单的办法。也包括“应该还是有办法避免 GC(这里指定期扫描的GC) 或者 RC。比如给同一种对象一大块空间,只是拿来用,不释放。到这种数据不用的时候一起释放掉就行。这样内存管理几乎没有计算开销,只是多用点内存而已(类似内存池 memory pool)。”

总的来说,想完全静态地管理所有内存,目前看是不太可能的,除非牺牲程序的表达能力和灵活性。比如 Jet Propulsion Laboratory(美国喷气推进实验室)在它的 JPL Institutional Coding Standard for the C Programming Language 规范里就禁止使用动态内存分配(malloc 等,见第 10 页)。JPL 在设计阶段就会把所有运行时可能的内存使用情况提前规划好了。让内存行为可预测,规避内存泄漏风险。

对于一般软件,是灵活性优先(动态分配随时申请)。而对于 JPL 之类的航天软件,则是硬可靠性优先(所有内存行为可预测)。

Do not use dynamic memory allocation after initialization.

Gerard J. Holzmann (NASA Jet Propulsion Laboratory)

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 函数的花括号

打赏