👈 12 闭包和迭代器

Box<T>

将一个值放在堆上(而非栈上)。使用场景:

  • 有一个编译时大小未知的类型,且要在需要确定大小的上下文中使用这个类型值。
  • 有大量数据,且希望在确保数据不被拷贝的前提下转移所有权。
  • 希望拥有一个值,且只关心它的类型是否实现了特定 trait 而不关心其具体类型。
let b = Box::new(5); // b: Box<i32>
println!("b = {b}"); // b = 5

递归类型:拥有同类型的值作为自身的一部分。

Rust 需要在编译时知道类型占用多少空间。递归类型的值理论上可以无限地嵌套下去,所以 Rust 不知道递归类型需要多少空间。通过在循环类型定义中插入 Box<T>,可以创建递归类型。

cons list:一个嵌套列表,每一项都包含当前项的值以及下一项,下一项为 Nil 表示列表终止。

(1, (2, (3, Nil)))

尝试定义一个 cons list:

use crate::List::{Cons, Nil};
 
enum List {
    Cons(i32, List),
    Nil,
}
 
fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

无法过编:

error[E0072]: recursive type `List` has infinite size
 --> src\main.rs:3:1
  |
3 | enum List {
  | ^^^^^^^^^
4 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle     
  |
4 |     Cons(i32, Box<List>),
  |               ++++    +

Rust 如何确定非递归类型的空间大小?以 enum Message 为例:

enum Message {
    Quit,                       // 无需空间
    Move { x: i32, y: i32 },    // i32 * 2
    Write(String),              // String * 1
    ChangeColor(i32, i32, i32), // i32 * 3
}

Message 所占空间为其最大成员所占的空间。

回到建议:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle     
  |
4 |     Cons(i32, Box<List>),
  |               ++++    +

“indirection”的意思是不应储存一个值,而应储存一个指向值的智能指针 / 引用等。

Box<T> 是一个指针,它所需的空间为定值,因为指针的大小不会随其指向的数据的大小改变。修改后:

enum List {
    Cons(i32, Box<List>),
    Nil,
}

原本,List 的成员 Cons(i32, List) 包含 一个 List,现在,Cons(i32, Box<List>) 变成了 指向 一个 List。由于 Box<List> 指针的大小是固定值,编译器现在能够推导出 List 的大小为 i32Box<List> 的大小之和。


智能指针也可以作为常规引用进行处理:

// 常规引用
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
 
// Box<T> 改写
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);

根本上说,Box<T> 是包含一个元素的元组结构体,可以自定义智能指针:

struct MyBox<T>(T);
 
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);

无法过编,因为 Rust 不知道如何解引用 y: MyBox<{integer}>

为了使解引用运算符 * 有效,需要实现 Deref trait。

use std::ops::Deref;
 
impl<T> Deref for MyBox<T> {
    type Target = T; // 定义了用于此 trait 的关联类型
    
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
 
struct MyBox<T>(T);
 
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

*y 现在会被 Rust 展开为 *(y.deref())

TODO: 函数和方法的隐式 Deref 强制转换

TODO: Deref 强制转换如何与可变性交互

Rc<T>

有些特殊情况下,单个值可能有多个所有者(图中的一个结点可能被多条边指向)。

要启用多重所有权,需要显式使用 Rc<T>Rc引用计数(reference counting),意味着通过记录一个值的引用数量检查这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

想象客厅中的电视:第一个人进入客厅并打开电视,随后其他人也可以进入并观看或者选择离开,当最后一个人离开时他将关闭电视。

使用场景:要在堆上分配一些内存供程序的多个部分读取,且无法在编译时确定程序的哪一部分会最后使用它。(如果知道最后使用的部分,用一般的所有权规则即可)。

以上节的 cons list 为例,现在想要定义这样一个列表:

用一般的所有权写法,尝试让 bc 都指向 a

let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a)); // error: use of moved value

无法编译,因为在定义 b 时已经移动 a

修改 List 的定义,用 Rc<T> 代替 Box<T>,并改为克隆 2 次 a 所包含的 Rc<List>

enum List {
    Cons(i32, Rc<List>),
    Nil,
}
 
use crate::List::{Cons, Nil};
use std::rc::Rc; // Rc 不在 prelude 中
 
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a)); // 克隆语法, 可以改为 a.clone(),
    let c = Cons(4, Rc::clone(&a)); // 但 Rc::clone 只增加计数, 高效
}

测试引用计数的变化规律:

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after dropping c = {}", Rc::strong_count(&a));
}
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after dropping c = 2

使用 Rc<T> 允许一个值有多个所有者,引用计数则确保:只要还存在任何所有者,其值就有效。

RefCell<T>

内部可变性:一种设计模式,允许当存在不可变引用时修改数据。它在数据结构中使用 unsafe 以模糊可变性与借用规则。

unsafe:表明程序员在手动检查规则,而禁止编译器检查。

[回顾] 借用规则:

  • 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用 之一(而不是两者)。
  • 引用必须总是有效的。

对于引用和 Box<T>,借用规则的不可变性作用于 编译时,违反时无法过编;而对于 RefCell<T> 则作用于 运行时,违反时线程 panic

代码的一些属性不可能通过分析代码发现(参见 停机问题),这也是为什么编译器有时会出于保守而拒绝编译实际上正确的代码。这就是 RefCell<T> 的用武之地。

以下代码违反了借用规则,因为试图可变地借用一个不可变值:

fn main() {
    let x = 5;
    let y = &mut x;
}

但是在特定情况下,使一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的(这样,只有在值的方法内部才能进行修改)。

TODO


智能指针对比:

  • Rc<T> 允许相同数据有多个所有者;Box<T> 和 RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T> 仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

TODO: 引用循环和内存泄漏

👉 14 并发