• 什么是Rust的内部可变性?
  • 发布于 1个月前
  • 62 热度
    0 评论
  • 亦東風
  • 0 粉丝 38 篇博客
  •   
“内部可变性”的概念讲起来很有迷惑性,理论知识可能很容易将新手绕晕。先来通俗说一下它的用途。其实这东西的产生,是因为 Rust 的规矩太多,有着严格的内存访问限制。内部可变性是一种变通手段,将“破例”的行为仍然约束在 Rust 可控的范围。就是说,你想破例也要按我的规则来破例。

我们都知道,Rust 的内存安全基于以下规则:给定对象 T,只能有以下情况之一:
1.对该对象有多个不可变引用(&T)(也称为别名)。
2.对该对象有一个可变引用(&mut T)(也称为可变性)。

这一规则由 Rust 编译器强制执行。然而,有些情况下该规则并不够灵活。有时会需要多个引用指向一个对象,并能同时对其进行修改。而 Rust 提供的 可共享的可变容器(Shareable mutable containers) 则允许以一种受控的方式实现可变性,即使已经存在别名。

这些容器包括:Cell<T>、RefCell<T> 等,它们允许以单线程方式进行这样的操作;而多个线程之间进行不可变引用引用并修改时,则可以使用 Mutex<T> 等。事实上 Mutex 不过是一种实现“多个引用指向一个对象,并能同时对其进行修改”这一目的的方法而已。它为内部数据提供独占(mut)访问权限,尽管许多线程可能对 Mutex 本身具有共享(非 mut)访问权限。

总结一下,内部可变性是一种允许在拥有不可变引用的同时修改数据的概念。这种能力是通过特定类型的容器实现的。这些容器允许在不违反借用规则的情况下,在拥有不可变引用的同时对数据进行修改。

如果还不懂,咱们来详细说一下。

首先,既然有内部可变性,就有对应的外部可变性。 外部可变性一言以蔽之就是:变量的可变性直接继承自包含该变量的容器。其实从其英文名 inherited mutability 我们可以感知到这点。

比方说有容器(此处为结构体)Book :
struct Book {
    name: String,
}
如果想为其增加一个方法,用来更改其 name:
impl Book {
    fn change_name(&self, new_name: String) {
        self.name = new_name;
    }
}
然后你会发现编译错误:
cannot assign to `self.name`, which is behind a `&` reference
这是因为该类型需要从父级传递 &mut 访问权限到子级,或者说是从容器到内容。只有当你拥有对容器 self 的 &mut 引用时,才会预期能够调用内容 self.name 并为其赋值。
impl Book {
    fn change_name(&mut self, new_name: String) {
        self.name = new_name;
    }
}
同样的,下面的代码也将无法编译:
let a = Book {
    name: String::from("老子"),
};
a.change_name(String::from("庄子"));
需要将 a 改为 mut a 方可编译。这是默认行为,因为如果你没有对父级的独占访问权限,Rust 通常无法确保你对子级有独占访问权限。内部可变性不同于 Rust 默认的“持有共享引用不能改变”或“持有可变引用不能还有共享引用”的非此即彼的编译期约束。它更应该叫共享可变性——持有共享引用还可以改变数据。

现在来看 Rust 为“破例”而设立的规则。其实就是用特定的容器来包裹你想要“破例”的内容,以便编译器能识别到它们。这类容器典型的有 Cell<T> 与 RefCell<T>。而关于 Cell<T> 与 RefCell<T> 的区别,其实你只要记清:Cell<T> 适用于实现 Copy trait 的类型,而 RefCell<T> 适用于非 Copy 类型即可。

我们来为 Book 增加点内容:
struct Book {
    name: String,
    price: Cell<i32>,
    author: RefCell<String>,
}
price 为 i32 类型的,实现了 Copy ,所以我们用 Cell 包裹,相应的,String 类型的 author 则用了 RefCell 包裹。

再增加两个方法:
fn change_price(&self, new_price: i32) {
    self.price.set(new_price);
}
fn change_author(&self, new_author: String) {
    let mut a = self.author.borrow_mut();
    *a = new_author;
}
我们注意到,此时不再需要 &mut self。调用的时候:
let a = Book {
    name: String::from("老子"),
    price: Cell::new(25),
    author: RefCell::new(String::from("老子")),
};

a.change_author(String::from("李耳"));
let b = &a;
b.change_price(30);
a.change_price(21);
而如果你尝试以下代码,理所当然会出现编译错误:
a.change_name(String::from("庄子"));
这样一来,我们的破例行为又符合 Rust 的规则了。而明白了这一点,再去从 API 文档中详细学习 Cell<T> 、RefCell<T> 或是其它此类可共享的可变容器时,你便会有的放矢了。

最后我们把 author 打印出来看一下:
println!("{:?}", a.author.borrow());  // "李耳"
没有问题。但如果我们尝试把 change_author 方法中的代码 copy 到 main 中来:
let a = Book { ... };
...

let mut d = a.author.borrow_mut();
*d = String::from("老聃");

println!("{:?}", a.author.borrow());
编译没问题,运行时却会报错:

然后我明确地进行 drop(d) 操作后,就又正常了:
let mut d = a.author.borrow_mut();
*d = String::from("老聃");
drop(d);

println!("{:?}", a.author.borrow());
想想看为什么?
用户评论