71、Interior mutability(内部可变性)

Interior mutability(内部可变性)

分析一下Sendersend签名

impl<T> Sender<T> {
    pub fn send(&self, t: T) -> Result<(), SendError<T>> {
        // [...]
    }
}

完整:

impl<T> Sender<T> {
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn send(&self, t: T) -> Result<(), SendError<T>> {
        self.inner.send(t)
    }
}

send 的参数是 &self
但显然它引发了某种可变性:它向通道中添加了一条新消息。更有趣的是,Sender是可克隆的:我们可以让多个Sender实例同时从不同的线程修改通道状态。

这就是我们用来构建客户端-服务器架构的关键特性。

但为什么它能做到这一点呢?

难道这不违反 Rust 的借用规则吗?

我们是如何通过不可变引用执行可变操作的?

 

共享引用而不是不可变引用

在学习借用检查器的时候,我们学到了Rust中的两种引用类型:

  • 不可变引用(&T
  • 可变引用(&mut T

其实,可以给他们起一个更加精确的名字:

  • 共享引用(&T
  • 独占引用(&mut T

不可变/可变是一种适用于绝大多数场景的思维模型,也是初学 Rust 时非常有用的切入点。但是&T 实际上并不能保证它所指向的数据是不可变的。不过Rust在这的基础上仍然遵循基本的原则,只是规则比最初的更加精细一些。

UnsafeCell

只要一种类型允许我们通过共享引用来修改数据,我们就在处理interior mutability(内部可变性)。

默认情况下,Rust 编译器认为共享引用是不可变的。编译器会根据这一假设优化代码。编译器可以重新排列操作顺序、缓存值,并施展各种魔法让代码更快。

我们可以用 UnsafeCell 封装数据,告诉编译器 "这个共享引用实际上是可变的"。

 

每当我们看到允许内部可变的类型时,我们都可以肯定 UnsafeCell 直接或间接地参与其中。
通过 UnsafeCell、原始指针和 unsafe 代码,我们可以修改共享引用的数据。

 

要明确的是,使用 UnsafeCell 并不代表我们可以忽略借用检查。

unsafe代码仍需遵守 Rust 关于借用和别名的规则。它是一个(高级)工具,我们可以利用它来构建 Rust 的类型系统无法直接表达其安全性的 抽象。

无论何时使用 unsafe 关键字,都是在告诉编译器:“我知道自己在做什么,我不会破坏你的不变量,相信我。”

 

每次调用unsafe函数时,都会有文档解释其安全前提条件:在什么情况下执行其unsafe代码块是安全的。我们可以在 std 的文档中找到 UnsafeCell 的安全前提条件。

在这段学习中,我们不会直接使用 UnsafeCell,也不会编写unsafe代码。但重要的是要知道它的存在、它存在的原因以及它与我们每天在 Rust 中使用的类型之间的关系。

 

关键示例

让我们来看看几个利用内部可变性的重要 std 类型。

这些类型在 Rust 代码中会经常遇到,尤其是当我们在使用某些库时。

引用计数

Rc 是一个引用计数指针。

它包裹一个值并跟踪存在多少对该值的引用。当最后一个引用被删除时,该值被释放。Rc 中包含的值是不可变的:我们只能获取对它的共享引用。

use std::rc::Rc;

let a: Rc<String> = Rc::new("My string".to_string());
// 字符串数据只存在一个引用。
assert_eq!(Rc::strong_count(&a), 1);

// 当我们调用 `clone` 时,字符串数据不会被复制!
// 相反,`Rc`的引用计数会增加。
let b = Rc::clone(&a);
assert_eq!(Rc::strong_count(&a), 2);
assert_eq!(Rc::strong_count(&b), 2);
// ^ `a`和`b`都指向同一个字符串数据
// 并共享同一个引用计数器。

Rc 在内部使用 UnsafeCell,允许共享引用递增和递减引用计数。

RefCell

RefCell 是 Rust 中最常见的内部可变性示例之一。它允许我们更改封装在 RefCell 中的值,即使我们只有对 RefCell 本身的不可变引用。

这是通过runtime borrow checking(运行时借用检查)完成的。RefCell 跟踪运行时它所包含的值的引用的数量(和类型)。

如果我们尝试在已经不可变地借用该值时以可变方式借用该值,程序会Panic,确保 Rust 的借用规则始终被遵循。

use std::cell::RefCell;

let x = RefCell::new(42);

let y = x.borrow(); // 不可变借用
let z = x.borrow_mut(); // Panics! 已经有一个存活的不可变借用

 

练习

// TODO: Use `Rc` and `RefCell` to implement `DropTracker<T>`, a wrapper around a value of type `T`
//  that increments a shared `usize` counter every time the wrapped value is dropped.
// TODO: 使用 `Rc` 和 `RefCell` 来实现 `DropTracker<T>`, 这是一个对 `T` 类型的值的封装。
//  每当被包装的值被丢弃时,它都会递增一个共享的 `usize` 计数器。

use std::cell::RefCell;
use std::rc::Rc;

pub struct DropTracker<T> {
    value: T,
    counter: /* TODO */,
}

impl<T> DropTracker<T> {
    pub fn new(value: T, counter: /* TODO */) -> Self {
        Self { value, counter }
    }
}

impl<T> Drop for DropTracker<T> {
    fn drop(&mut self) {
        /* TODO */
    }
}

// TODO: Use `Rc` and `RefCell` to implement `DropTracker<T>`, a wrapper around a value of type `T`
//  that increments a shared `usize` counter every time the wrapped value is dropped.
// TODO: 使用 `Rc` 和 `RefCell` 来实现 `DropTracker<T>`, 这是一个对 `T` 类型的值的封装。
//  每当被包装的值被丢弃时,它都会递增一个共享的 `usize` 计数器。

// 所以这个题目的意思大概就是,每次调用drop,都会让counter计数器的值加一

use std::cell::RefCell;
use std::rc::Rc;

pub struct DropTracker<T> {
    value: T,
    counter: Rc<RefCell<usize>>,//Rc的作用是允许多个所有者共享数据,RefCell的作用是允许内部可变
}

impl<T> DropTracker<T> {
    pub fn new(value: T, counter: Rc<RefCell<usize>>) -> Self {
        Self { value, counter }
    }
}

impl<T> Drop for DropTracker<T> {
    fn drop(&mut self) {
        let mut value = self.counter.borrow_mut();
        *value += 1;
    }
}

Rc(Reference Counted)是一种引用计数智能指针,适用于单线程环境中。当你需要多个所有者共享同一个数据时,可以使用 RcRc 允许你在多个地方持有同一个数据的不可变引用,直到最后一个引用被释放时,数据才会被销毁。例如:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("value"));
    let b = a.clone();
    println!("{}, {}", a, b);
}

在这个例子中,ab 都持有同一个字符串的引用。

RefCell 允许你在运行时检查借用规则,而不是在编译时。它提供了一种内部可变性模式,使你可以在不可变引用的情况下修改数据。RefCell 通过 borrowborrow_mut 方法来借用数据,分别返回不可变和可变引用。例如:

use std::cell::RefCell;

fn main() {
    let a = RefCell::new(String::from("hello, world"));
    let mut s = a.borrow_mut();
    s.push_str("!!!!!");
    println!("{}", s);
}

在这个例子中,我们使用 RefCell 来修改一个不可变字符串。

 

阅读剩余
THE END