Rust 入门
Rust 算是一个底层的系统编程语言。和有垃圾回收(Garbage Collection)的语言不一样,Rust 作为一种底层的系统语言,在内存管理方面有一些特殊的地方。它试图用某种语言设计(ownership)取代垃圾回收,同时实现静态内存管理。
应该说 C++ 的指针也能实现类似的功能,但 Rust 增加了 ownership 这些概念和与之相应的使用规则,并且设计上会对代码进行这些规则相关的静态检查——即运行代码前,编译器检会查你代码对内存的使用是否按照 Rust 的规则做对了。而 C++ 没有像 Rust 那样的编译器级别的检查机制,代码运行前无法查错(静态分析)。总的来说, Rust 的静态类型检查相比 C++ 等其他语言增加了内存使用的规范的编译检测,提前发现问题,防止代码运行时出现内存问题。
Rust 的这些 ownership,lifetime 之类的静态检查设计,主要目的是静态地管理内存的分配和释放。在编译环节而不是运行时环节就解决内存问题,进而减少运行时垃圾回收(GC)的计算开销。不过到了复杂的数据结构,或者说必须用 GC 的地方,基本上还是得用 Rust 的 Rc(引用计数),比如解释器。只是比较简单的那一部分代码你可以不用 Rc 。
由于 Rust 语言的内存管理通过少用或不用 Rc ,减少了 Rc 的开销,代码性能据说在某些情况下会快很多。所以可以稍微重视一下(保持怀疑态度看待 Rust 的性能)。当然,它的这种设计并没有很完善,其中有一些多余而复杂东西(比如 Rust 总有一些自以为方便的隐式操作,而非写什么代码就是什么的所见即所得,类型系统也反直觉地水,因为参杂了利于静态分析的内存规则),但理解它的内存管理思路能给人们提供启发。进而可以改进自己的代码思路。
像 C++ 稍微“扩展”一下 unique_ptr,也能实现 Rust 的内存管理精髓。但若没有 Rust 这套内存规则的作为大方向指导我们的思路,那我们可能就会不知道如何有效地使用这些 unique_ptr 等指针。不过像这种 ownership 的思路可能在 C++ 里就已经有了(unique_ptr 和 shared_ptr),只不过 C++ 没有类型系统来检查它:比如 C++ 中指针 p1 被 move 之后,仍然能使用,只不过 p1 指向的地址是 0 而已。
初次使用 Rust 编程很可能会非常痛苦,除了没有所见即所得的设计(Rust 会”帮“你各种自动隐式操作),期间还有复杂的内存管理规则要适应。它的类型检查系统里混杂着内存管理检查系统,也就是你两者都要按照它的规则做对。大多数时候,更是要连蒙带猜加分析。
有时候你看到编译器报错,说你数据类型弄错了,你以为只是一个类型错误,但实际上还包含着多个内存错误(如 ownership 没有按照 Rust 做对)。不像其他语言,你改一下类型就能通过。在 Rust 中,除了改对类型,还要做对 ownership 。而且因为 ownership ,很多数据都要封装在各种容器里,导致数据类型也是眼花缭乱。最终,类型系统和内存规则系统会在你脑中纠缠在一起。
不过如果能耐心坚持到成功写出一个解释器,也许对内存管理的理解会上升一个台阶。
新的理解包括也许除了垃圾回收(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.
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 Code 把 VS 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.rsRust 的基本操作
每次接触新的编程语言,基本都可以从「变量定义」,「函数调用」,「条件分支」,「类型」和「常见数据结构」这几个方向入手快速熟悉。至于语言的一些特有的特性,之后再根据需要补充学习即可。
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 = "Hello".to_string(); // z: String
println!("Original z = {z}");
// 下面这样没给 z 赋值的写法不好,任何变量都应该给一个初始值
// 设计优秀的语言里面没有 variable declaration 这个概念
// let mut z: String; 这种写法在设计优秀的语言里不应该合法
z = "World".to_string();
println!("1st Edited z = {z}"); // z 的值被改为 "World"
// -------------------- Borrow --------------------
// Borrow 类似 C++ 的引用,但又多了些功能,比如能被静态分析查错
// C++ 的 & 是一个纯粹的“别名”或“地址”,不保证数据的有效性和安全性
// Rust 的 & 代表 borrow ,它不仅仅是一个地址,还背后附带一系列规则
// 让编译器可以检测 borrow 以确保引用的生命周期和访问权限(如读写锁)等数据安全
// 变量 z 可以被多个不同变量 unmutable borrow
let b1 = &z;
let b2 = &z;
// 处于 unmutable borrow 的状态下的 z 值不可更改
// 否则可能会导致 borrow 的那些变量失效,出现 Dangling Reference
// b1,b2 的生命周期没结束,因为后面要 println! 访问打印 b1 和 b2
// z = "Hi".to_string(); // error
// 处于 unmutable borrow 的状态下的 z 值可访问
println!("z = {z}"); // "World"
println!("unmutable borrow b1 and b2: {}, {}", b1, b2); // 访问 b1 和 b2
// 此时 z 值可变了,因为 Rust 静态编译看到后面没有再用到 b1 和 b2
// 于是编译器直接判断和决定 b1,b2 的生命周期在上面 println! 后结束
// 于是 z 不再处于 unmutable borrow 的状态下,于是可再次修改
z = "Hi".to_string();
println!("2nd Edited z = {}", z); // "Hi"
// 通过 &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 = "Magnificent".to_string();
// 处于 mutable borrow 的状态下的 z 值不可访问,也不可赋值修改
// 编译器看到后续还会访问 b3 ,故 b3 生命周期没结束,所以 z 未解除状态
// println!("b3 Edited z = {}", z); // error
// z = "Gorgeous".to_string(); // error
println!("Edited b3: {}", b3); // b3 = "Magnificent"
// let b6 = *b3; // error - 不允许透过引用 move 背后的值
let b6 = b3; // move b3 to b6
*b6 = "Elegent".to_string(); // mutate z through b6
// 后续代码不再涉及 b3 和 b6 ,所以编译器判断它们的生命周期结束
// 于是 z 不再处于 mutable borrow 的状态下,于是可再次修改和访问
println!("3rd Edited z = {}", z); // z = "Elegent" (被 b6 更改)
// 注意辨析下面两个 mut 的不同意义
let mut m = 9; // 变量 m 可通过赋值变为其他同类型的值
let m = &mut z; // 变量 m 是 z 的可变 borrow(z 也需是 mutable)
// -------------------- 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 。它相当于把底层的一些内存机制暴露给你,让你根据 Rust 的 ownership 规则来处理数据在内存中的流动、共享以及生命周期,并在此过程中同时弄对各种数据类型。否则编译不过。
无论如何,总的来说,Rust 内存机制的核心思路有 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 函数的花括号内存管理对比
- C/C++:
手动管理内存。给你最大的自由和责任。 - Java/C#/Go:
追踪式垃圾回收(Tracing GC)。用一定的性能和内存开销,换取了极高的开发效率和安全性。 - Python/Swift:
自动引用计数(ARC)。在回收及时性和 GC 暂停之间取得了很好的平衡,但留下了需要手动处理循环引用的问题。 - Rust:
一条全新的道路,通过在编译时强制执行一套严格的所有权(ownership)和借用(borrow)规则,试图在零运行时开销的前提下,同时实现C++级别的性能和Java级别的内存安全。这使得它的学习曲线最为陡峭,但一旦掌握,就能编写出性能极高且极其安全的系统级软件(当然这点有待观察)。
打赏