Rust 入门

    Programming Language

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

应该说 C++指针也能实现类似的功能,但 Rust 增加了 ownership 这些概念和与之相应的使用规则,并且设计上会对代码进行这些规则相关的静态检查——即运行代码,编译器检会查你代码对内存的使用是否按照 Rust 的规则做对了。而 C++ 没有像 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()
{

  // --------------------  Basics --------------------
  let x = 2 * 3;

  // Rust 常用 println! 这个宏(Macro)来打印输出(结尾带!号的都是宏)
  // 格式化字符串中的变量有时可以写在花括号里,有时不行
  // 所以统一写到外面比较好,统一视觉,避免写 "x = {x}"
  println!("x = {}", x);  

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


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

  // &str vs.String
  // &str 类型只读、不拥有数据;
  // String 类型拥有所有权,支持修改;
  // 读就用 &str ,改/存/造就用 String
  // String 可 borrow 成 &str 来传参;&str 需要时再 to_string() 变成 owner


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

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

  // 下面这样没给 z 赋值的写法不好,任何变量都应该给一个初始值
  // 设计优秀的语言里面没有 variable declaration 这个概念
  // let mut z: i32; 这种写法在设计优秀的语言里不应该合法

  z = 10;
  println!("1st Edited z = {z}");  // z 的值可被改为 10


  // --------------------  Borrow --------------------
  // Borrow 类似 C++ 的引用,但又多了些功能,比如能被静态分析查错
  // 变量 z 可以被多个不同变量 unmutable borrow
  let b1 = &z; 
  let b2 = &z;

  // 处于 unmutable borrow 的状态下的 z 值不可更改
  // 否则可能会导致 borrow 的那些变量失效,出现 Dangling Reference
  // b1,b2 的生命周期没结束,因为后面要 println! 访问打印 b1 和 b2
  // z = 9;  // error

  // 处于 unmutable borrow 的状态下的 z 值可访问
  println!("z = {z}");  // 10

  println!("unmutable borrow b1 and b2: {}, {}", b1, b2);  // 访问 b1 和 b2 

  // 此时 z 值可变了,因为 Rust 静态编译看到后面没有再用到 b1 和 b2
  // 于是编译器直接判断和决定 b1,b2 的生命周期在上面 println! 后结束
  // 于是 z 不再处于 unmutable borrow 的状态下,于是可再次修改
  z = 9;  
  println!("2nd Edited z = {}", z); 

  // 通过 &mut 关键词来进行 mutable borrow ,和 b1,b2 不同,b3 能更改 z 的值
  // 要实现 &mut z ,首先要有 mut z
  let b3 = &mut z;
  println!("mutable borrow b3: {}", b3);

  // 只能从一个可变变量(mutable variable)获取唯一可变引用(mutable reference)
  // let b4 = &z;     // error - 等 b3 生命周期结束才能 unmutable borrow
  // let b5 = &mut z; // error - 等 b3 生命周期结束才能 mutable borrow

  // 注意,通过 b3 更改 z 的值,要解引用(dereference),类似 C++
  // 因为 b3 是个「引用」。b3 类型为 &mut i32 ,而 z 的类型为 i32
  *b3 = 29;

  // 处于 mutable borrow 的状态下的 z 值不可访问,也不可赋值修改
  // println!("b3 Edited z = {}", z);    // error
  // z = 19;                             // error

  println!("Edited b3: {}", b3);    // b3 = 29

  // 编译器直接判断 b3 的生命周期在上面 println! 后结束
  // 于是 z 不再处于 mutable borrow 的状态下,于是可再次修改和访问
  println!("b3 Edited z = {}", z);  // z = 29 (被 b3 更改)


  // --------------------  Array --------------------  
  // 数组 - 默认也是不能更改,若要可更改则用 let mut a: [i32; 3] = [1, 2, 3]
  // 数组类型没实现 Display,但实现了 Debug,故格式打印用 {:?} 而不是 {}
  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);


  // --------------------  Function --------------------
  // 匿名函数:x => x + 1(没标记类型是因为 Rust 有类型推导)
  println!("(|x| x + 1)(5) = {}", (|x| x + 1)(5));

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

  // 标记类型的函数定义
  // 注意这里原本的 |x| 变成了 |x: i32| -> i32
  // -> i32 部分表示函数返回类型是 i32 ,函数体也被包裹在花括号 {} 里
  let add2 = |x: i32| -> i32 { x + 2 };
  println!("add2(5) = {}", add2(5));

  // 双参数函数
  let add3 = |x: i32, y: i32| -> i32 { x + y };
  println!("add3(9, 29) = {}", add3(9, 29));

  // 上面展示的类型推导有局限,比如 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)这一语言特性
  // 垃圾回收内存机制无法实施静态检查,而 Rust 想要编译时(静态检查)就发现内存问题
  println!("(|x| (move |y| x + y))(2)(3) = {}", (|x| (move |y| x + y))(2)(3));

  // 代码太长可以换行对齐
  println!("((|x, y| (move |z| x + y + z))(1, 2)(3) = {}", 
            (|x, y| (move |z| x + y + z))(1, 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);


  // --------------------  Data Structure --------------------
  // 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();  

  // let mut table:HashMap<&str, i32> = 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);   // error

  // 需要添加类型 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  // 剩下的数据来自 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)] 需要直接应用于每个需要它的结构体或枚举(enum)上。它不是全局性的,不能应用一次就影响整个模块或包
  // 这意味着每个想用 {:?} 格式化输出的类型都要单独写一次 #[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
  // 简单来说就是 enum 里可以放各种 struct
  // 这里定义了一个 Mybool 类型,它包含 2 个子类型 MyTrue 和 MyFalse
  enum MyBool
  {
    MyTrue,    // Unit-like struct
    MyFalse
  }

  // 注意,这里 x 的类型是 MyBool 而不是 MyBool::MyTrue
  // MyBool::MyTrue 是类型 MyBool 下的一个值
  // 类似布尔类型(bool)下有 2 个值:true 和 false
  // let x:MyBool = MyBool::MyTrue;
  let x = MyBool::MyTrue;
  match x
  {
    MyBool::MyTrue => println!("x is true"),
    MyBool::MyFalse => println!("x is false")
  }


  // --------------------  List --------------------
  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>>),   // tuple struct
    Nil                      // Unit-like struct
  }

  // length 函数,计算输入链表的长度
  // i32 类型可以是正数、负数或零;usize 类型无符号,只能是正数或零
  // 这里将 usize 改为 i32,代码仍可编译和正确运行
  // 但 Rust 中所有与内存中对象数量、大小、容量或索引相关的值,都应使用 usize,以保证内存安全
  // 这是整个 Rust 语言生态系统的标准和惯例
  fn length<T>(ls: &List<T>) -> usize
  {
    match ls
    {
      // 注意这里 Rust 的语言特性 Match Ergonomics :
      // 当 match 一个引用时(ls 类型是 &List<T>),它会遵循这个引用的模式。
      // 在解构 List::Pair 时,它不会 move 数据到 tail,而是自动为你 borrow
      // 也就是自动加上 ref
      // List::Pair(_, tail) 会被自动转为 List::Pair(_, ref tail)
      // 因此,tail 的类型不是 Box<List<T>>,而是 &Box<List<T>>
      // 如果 match 的不是引用而是值时,match 的默认行为会发生 move
      // 即 Pair 中的数据(所有权)会被 move 到变量 tail ,这也许不是你想要的
      // 在 Rust 的模式匹配(match, let, if let 等)中,下划线 _ 的意思是:
      // “匹配任何值,但不要将它绑定到任何变量上,直接把它丢弃。”
      List::Pair(_, tail) => 1 + length(tail),
      List::Nil => 0,
    }
  }

  // 这样外层再套一个 Box 来定义 ls1 也行,此时 &ls1 的类型就是 &Box<List<T>> 
  // 传递 &Box<List<T>> 类型的值给 &List<T> 类型的函数参数时
  // Rust 的 Deref Coercion(自动/强制解引用)语言特性会解引用得到 &List<T>
  // let ls1 = Box::new(List::Pair(1, Box::new(List::Pair(2, Box::new(List::Pair(3, Box::new(List::Nil)))))));
  let ls1 = List::Pair(1, Box::new(List::Pair(2, Box::new(List::Pair(3, Box::new(List::Nil))))));
  println!("list length = {}", length(&ls1));


  // 自动解引用只在特定条件下剩下,比如函数传参,不会作用于二元运算符(如 ==, +, - 等)的操作数
  // T == &T 这样的比较是需要手动写 * 来解引用的
  // 自动解引用是 Rust 的糟糕设计,很多情况下不是所见即所得


  // 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>)  // tuple struct
  }

  // calculator
  // 注意,这个 calc 函数是破坏性的
  // 即调用时会发生 move ,转移 exp1 和 exp2 所有权
  // 思考题:用上述知识调整代码为引用传入,使 exp1 和 exp2 在调用后不失效
  fn calc(exp: Exp) -> i32
  {
    match exp
    {
      Exp::Lit(n) => n,
      Exp::Binop(op, e1, e2) =>
      {
        // * 代表 dereferencing ,即读取“引用”对应的值
        // 顺便说一下 & 符号用于创建“引用”,而 * 符号用于“解引用”,获取其所指向的值
        // 注意,Rust 的引用和 C 语言、C++ 的引用(指针)概念有点不一样
        // C++ 的操作比较精确,Rust 有一些自动的隐式操作
        // & 和 * 只能暂时近似理解为「创建引用」和「解引用」的效果,不能认为是等同的概念
        // 之后会有例子暴露 Rust 的问题:&&String 和 &String 的类型是否一样
        // 准确的理解应该抛弃 C 语言的指针思维,进一步还得查看 Rust 官方文档关于 ownership 的说明
        // https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
        // 由于这里的 e1 和 e2 都是 Box<Exp> ,上面提到 Box 其实是“指针”,所以要通过 * 把对应的值取出来
        // 注意这里同样会发生 move 转移所有权
        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
  // 因为这样 match 后的参数顺序可不同,参数名对了就能取到正确的值
  // NamedExp::Binop { op, e1, e2 } 和 NamedExp::Binop { e1, e2, op }
  // 注意两者的括号类型也有区别
  // 可以具体观察对比 exp1 和 exp3 的定义内容

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

  // calc2 和 calc 一样,调用会发生 move 转移所有权
  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));


  // 注意,类似 NamedExp::Lit { n: 5 } 的类型是 NamedExp,而不是 Lit
  // 如果你写 struct Lit { n: i32 },你就创建了一个全新的类型,叫 Lit
  // 然后你就可以声明一个 Lit 类型的变量 let l: Lit = ...;
  // 而上面 NamedExp 内部的 Lit { n: i32 } 不是一个类型
  // 它只是 NamedExp 这个 enum 类型的一种可能形态
  // 但从行为上,基本上可以把它看作一个定义在 enum 内部的 struct
  // 所以 exp3 如果要显式标记类型,应该是:
  // let exp3: NamedExp = NamedExp::Binop ...


  // 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 来的数据,别人失效了,你就失效了(所以 Rust 中处于 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);

  // 这里是展示 move 这个语义是怎么进行的
  // 正常不会写类似 take_and_give_back 的函数,而是传入 borrow,不 move
  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");
  println!("before mutatable borrow, s: {}", s); // hello
  mutate(&mut s);
  println!("after mutatable borrow, s: {}", s);  // hello, world

  // 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);

  // dangle 函数试图返回一个指向已经被销毁的数据的引用
  // 当函数调用完成,s 的生命周期就结束了,函数返回的引用会指向已被销毁的 s
  // 也就是出现 Dangle Reference ,所以 Rust 编译时就会报错
  // 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
  // s 处于 borrowed 状态不允许 move 所有权,不然会出现 Dangle Reference
  // 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?)

  // 编译器只知道函数的类型,不看函数体内容(否则相当于运行代码了)
  // 所以只知道返回的是 &String 这个引用
  // 故而编译器不知道引用 &String 到底能”活“多久
  // Rust 编译器觉得这样的函数定义太模糊,无法让它确定 &String 的生命周期
  // 于是编译不通过。需要添加信息让编译器知道返回的 &String 引用的生命周期
  // 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
  // 可以看到泛型(Generics)和 lifetime 同时存在的语法
  fn second_string3<'a,'b, T, U>(s1: &'a T, s2: &'b U) -> &'b U
  {
    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;

  // Box::new(5) 相当于造了个指向 5 的指针:int* y = &5
  // 也可以认为把 5 封装在 Box 中,通过 *y 可以读取 5
  let y = Box::new(5);    
  // let y = &x;          // also work
  println!("y: {}", y);

  // 定义可更改的 y2
  let mut y2 = Box::new(6);
  println!("y2: {}", y2);

  // 先解引用拿出 Box 封装的数据,然后再赋值更改
  *y2 = 9;
  println!("y2: {}", y2);

  // Box<T> 可以调用 .clone(),执行的是深拷贝 (Deep Copy)
  let y3 = y2.clone();
  println!("y3: {}", y3);
  println!("y2: {}", y2);

  // 被 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 - 因为 x 是 5 有 copy trait 不会 move

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

  // 到目前为止,MyBox 只是个容器。比如 let y = MyBox::new(x); 之后,y 就是一个 MyBox<i32> 类型的值,
  // 它装着 5。你还不能对它做 *y 这样的”解引用“操作。
  // 要把 MyBox<T> 变成一个「智能指针」,就需要能进行解引用操作,即 *y
  // 下面我们通过 Deref Trait 来实现这个效果

  use std::ops::Deref;  // 载入所需标准库

  // Deref Trait - 相当于 Java 中 interface 的概念
  // 即要求实现 interface 中规定的方法,如 get 和 add 等
  // 这里的意思是,要为 MyBox<T> 类型实现 Deref trait(解引用)
  impl<T> Deref for MyBox<T> 
  {
    // Deref trait 要求必须指定一个名为 Target 的类型
    // 意思是「当解引用一个 MyBox<T> 时,你期望得到什么类型的引用?」
    // 这里等于 T 是说比如解引用一个 MyBox<i32> 会得到一个 i32 的引用
    type Target = T;   

    // 这是一个名为 deref 的方法,它接收 &self 作为参数
    // &self 是对 MyBox<T> 类型的实例(instance)的不可变引用(unmutable borrow)
    // Self::Target 指的是我们在上面定义的关联类型 Target,也就是 T
    // 所以返回值类型 &Self::Target 其实就是 &T。
    fn deref(&self) -> &Self::Target 
    {
      // self.0 指的是 MyBox 这个 tuple struct 的第一个(也是唯一一个)字段
      // 也就是那个被包装起来的 T 类型的值
      // 所以 &self.0 的类型就是 &T
      &self.0
    }

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

  // 当对 y 使用 * 符号的时候,程序做了两件事:
  // 1、确认 y 实现 deref trait 后调用其中实现的 deref 函数,即 y.deref()
  // 2、对 y.deref() 的结果进行 * 符号运算,即 *(y.deref())
  // 所以 *y 等价于 *(y.deref())
  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);
  }

  // &y 类型是 &MyBox<i32> 用了我们实现的 Mybox 指针
  // MyBox 实现了 Deref Trait 从而获得 Rust 自动解引用效果(Deref Coercion)
  // 调用 foo 时 &MyBox<i32> 会自动解引用成 &i32
  foo(&y);  


  // MyBox 和 Rust 自带的 Box 在数据所有权行为上还是有区别的
  let mbx = MyBox::new(String::from("MyBox data can't be moved"));
  let rbx = Box::new(String::from("Box data can be moved"));

  // 无法编译通过,MyBox 里的数据不能 move
  // 观察 deref 会发现,返回的是一个 immutable borrow
  // let mval = *mybx;              // error
  // println!("mybx2: {}", mval);   // error

  // Box 是 Rust 底层实现,拥有数据所有权,其中的数据可以 Move
  // MyBox 无法实现这种效果
  let rval = *rbx;
  println!("rbx: {}", rval);



  // 智能指针之所以叫「智能」,不仅是因为解引用这个功能
  // “智能”还体现在「自动的内存管理 (通过 Drop Trait)」
  // 以 Rust 自带智能指针 Box<T> 为例
  // 当一个 Box<T> 变量被销毁时,它的 drop 方法会自动被调用
  // 这个方法会释放掉它在堆 (Heap) 上分配的内存。这就在编译层面杜绝了内存泄漏
  // 上面的 MyBox<T>,我们没有为它实现 Drop trait
  // 所以当 MyBox<T> 离开作用域时,它只是简单地销毁了所包含的字段 T
  // 它本身没有进行额外的内存管理。
  // 我们通过下面的 Pointer 例子来理解 Drop Trait 机制


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

  struct Pointer 
  {
    data: String,
  }

  // 不需要写 use std::ops::Drop;,可以直接 impl<T> Drop for MyBox<T>
  // 这是 Rust 的 Prelude (标准库预导入) 机制的效果
  // Drop Trait 被认为重要,于是被放在 Prelude 中,而 Deref Trait 则不在
  // 所以 Deref Trait 需要通过 use std::ops::Deref; 手动导入

  // 这个 drop trait 相当于 C++ 里的 destructor
  impl Drop for Pointer 
  {
    // drop 作为消耗性质函数,为何不直接用 drop(self) 这种 move 的调用方式?
    // 因为会出现死循环
    // 转移所有权后,drop 函数执行完 self 也要被释放,又会再执行 drop ,不断循环
    // 而如果是 &mut 可变引用,drop 函数执行完后,只需要结束引用就行,不涉及释放
    // 因此不会再执行 drop
    fn drop(&mut self) 
    {
      // drop 出了作用域后就会打印这些信息作为 debug 信息,让人知道确实 drop 掉了
      println!("Dropping Pointer with data `{}`!", self.data);
    }
  }

  // 在 Rust 中,当一个结构体被销毁时,会发生两件事:
  // 1、如果它实现了 Drop trait,那么你写的 drop 方法会【先】被调用;
  // 2、drop 函数运行完后,编译器会自动生成代码,递归地销毁 struct 的每一个字段;

  // 观察 drop 发现没有更改操作,似乎参数变为 &self 也行?
  // 但 Rust 强制要求统一成 &mut self ,因为 drop 是销毁的一环,语义就是更改
  // Rust 认为一个 Trait 只能有一套规则
  // 为保证语言的健全安全,它必须采用能满足所有情况的最高权限要求,即 &mut self

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

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

  // 注意,这里 drop(a) 的 drop 函数不是我们上面实现的 fn drop(&mut self) 
  // 它是一个 move 变量 a 所有权的调用,然后立刻结束(该 drop 函数体是空的)
  // 于是,a 因为所有权被 move 而失效
  // 而此 drop 的参数拿到所有权后又因 drop 函数结束而生命周期结束
  // 到这里,才触发销毁机制,进而调用我们实现的 fn drop(&mut self) 
  drop(a);

  // Rust 禁止手动调用 a.drop() ,不能像上面 deref 函数那样调用 y.deref()
  // 这是为了防止二次释放(Double Drop):释放同一块内存两次是灾难性的事故
  // Rust 要确保每个值在其生命周期结束时,它的 drop 方法只会被精确地调用 1 次

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

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

  println!("Pointers created.");
  // 变量 d 先销毁,再到变量 c - 解释器内容回顾:后进先出 (LIFO)


  // Rc (Reference Counting) - 引用计数
  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)))));

  // Rc::clone(arg) 的参数 arg 的类型必须是一个 Rc 的引用:&Rc<T>
  // 这里 a 是一个 Rc<T> ,所以 &a 就是一个 &Rc<T>
  // 所以,List2 的最外层也得套一层 Rc 才行,不能像上面的 Box 那样,最外层可以不套:
  // let a = List2::Cons(2, Rc::new(...)); 这样是不行的
  // 如果这样写,&a 的类型就是 &List2,而不是 &Rc<T> 了,就用不了 Rc::clone 了
  let b = Rc::new(List2::Cons(5, Rc::clone(&a)));    
  let c = Rc::new(List2::Cons(8, Rc::clone(&a)));    

  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)]   // 这行代码是为了能用 {:?} 打印输出 TestRc 类型的值
  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::new() 封装后就无法再获得它内部数据的可变访问权 (mutable access)
  // Rust 规定:共享 xor 可变 (Shared XOR Mutable) 
  // 通过限制共享数据的可变性,Rust 希望防止数据竞争(data race)避免程序崩溃和诡异 bug


  // 如果希望用 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);  // 42

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

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

  // 由于没有绑定到变量,.borrow_mut() 效果在这条语句结束后就自动释放回收
  // 无需上述手动释放过程
  *value.borrow_mut() += 20;   
  println!("value 2nd edited: {}", *imm_borrow1);  // 29

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

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

  // 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,
  };

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

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

  // 改变 name 成员(field)的值
  // 注意这里 tr2_rfrc 是 Rc 类型,Rust 自动解引用获得内部的 RefCell
  // 所以可以 tr2_rfrc 可以直接调用 .borrow_mut() 而不用先解引用 *tr2_rfrc
  // 效果相当于:
  // (*tr2_rfrc).borrow_mut().name = String::from("admin");
  tr2_rfrc.borrow_mut().name = String::from("admin");  

  // 再次注意
  // *tr2_rfrc.borrow_mut().name = String::from("admin"); 是不行的
  // 因为 * 的计算优先级在 . 之后

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

打赏