Rust编译器如何知道`Cell`具有内部可变性?

9
考虑以下代码(Playground version):
use std::cell::Cell;

struct Foo(u32);

#[derive(Clone, Copy)]
struct FooRef<'a>(&'a Foo);

// the body of these functions don't matter
fn testa<'a>(x: &FooRef<'a>, y: &'a Foo) { x; }
fn testa_mut<'a>(x: &mut FooRef<'a>, y: &'a Foo) { *x = FooRef(y); }
fn testb<'a>(x: &Cell<FooRef<'a>>, y: &'a Foo) { x.set(FooRef(y)); }

fn main() {
    let u1 = Foo(3);
    let u2 = Foo(5);
    let mut a = FooRef(&u1);
    let b = Cell::new(FooRef(&u1));

    // try one of the following 3 statements
    testa(&a, &u2);         // allow move at (1)
    testa_mut(&mut a, &u2); // deny move -- fine!
    testb(&b, &u2);         // deny move -- but how does rustc know?

    u2;                     // (1) move out
    // ... do something with a or b
}

我很好奇 rustc 是如何知道 Cell 具有内部可变性并可能持有另一个参数的引用。

如果我从头开始创建另一个类似于 Cell 具有内部可变性的数据结构,我该如何告诉 rustc 呢?

3个回答

16

这段代码之所以能够编译(忽略 u2),并且能够进行变异,是因为 Cell 的整个 API 都采用了 & 指针:

impl<T> Cell<T> where T: Copy {
    fn new(value: T) -> Cell<T> { ... }

    fn get(&self) -> T { ... }

    fn set(&self, value: T) { ... }
}

它被精心编写,以允许在共享时进行变异,即内部可变性。这使得它能够将这些变异方法暴露在&指针后面。通常的变异需要一个&mut指针(以及其关联的非别名限制),因为独特地访问一个值是确保一般情况下对其进行变异是安全的唯一方法。
因此,创建允许在共享时进行变异的类型的方法是确保它们用&指针而不是&mut的API进行变异。一般来说,这应该通过让类型包含预先编写的类型(如Cell)来实现,即将其用作构建块。
之后使用u2失败的原因是一个更长的故事...

UnsafeCell

在更低的级别上,当一个值被共享(例如有多个&指针指向它)时,改变这个值是未定义的行为,除非该值包含在UnsafeCell中。这是内部可变性的最低级别,旨在用作构建其他抽象的构建块。

允许安全内部可变性的类型,如CellRefCell(用于顺序代码)、Atomic*MutexRwLock(用于并发代码)都在内部使用UnsafeCell,并对其施加一些限制以确保其安全性。例如,Cell的定义如下:

pub struct Cell<T> {
    value: UnsafeCell<T>,
}

Cell确保通过仔细限制其提供的API来使变异更安全:上面代码中的T: Copy是关键。

(如果您希望编写自己的低级类型并具有内部可变性,只需确保在共享时进行变异的内容包含在UnsafeCell中即可。但我建议不要这样做:Rust具有几个现有工具(我上面提到的工具)用于内部可变性,这些工具经过精心审核,可以在Rust的别名和变异规则内安全正确地执行;违反规则会导致未定义行为,并可能轻易导致错误编译的程序。)

生命周期变量

无论如何,使编译器理解“&u2”是借用于单元格情况的关键在于生命周期的差异。通常情况下,当您将某些内容传递给函数时,编译器会缩短生命周期,这使得事情变得更加顺畅,例如,您可以将字符串字面值(“&'static str”)传递给期望“&'a str”的函数,因为长时间的“'static”生命周期缩短为“'a”。对于“testa”,也是这样的:在“testa(&a, &u2)”调用中,引用的生命周期从可能最长的位置(整个“main”函数体)缩短到仅限于该函数调用。编译器可以这样做,因为普通引用在其生命周期中是可变的1,即它们可以变化。
然而,对于testa_mut&mut FooRef<'a>阻止编译器缩短生命周期(技术术语中,&mut TT中是“不变的”),正因为类似testa_mut的情况可能发生。在这种情况下,编译器看到&mut FooRef<'a>并理解'a生命周期根本无法缩短,因此在调用testa_mut(&mut a, &u2)时,它必须采用u2值的真实生命周期(整个函数),从而导致u2被借用该区域。

因此,回到内部可变性: UnsafeCell<T>不仅告诉编译器一个东西可能在别名中被改变(因此抑制了一些将未定义的优化),而且它也是T中的不变量,即它在生命周期/借用分析方面的作用就像&mut T,正因为它允许像testb这样的代码。

编译器会自动推断此种变化;当某些类型参数/生命周期包含在 UnsafeCell&mut 中时,它将变为不变量,就像在类型中的 FooRef(例如 Cell<FooRef<'a>>)一样。 Rustonomicon 讨论了这个问题以及其他详细考虑因素。
严格来说,类型系统术语中有四个级别的差异:双变、协变、逆变和不变。我认为 Rust 实际上只有不变性和协变性(存在一些逆变性,但会导致问题并已被删除/正在被删除)。当我说“variant”时,实际上意味着“covariant”。有关更多详细信息,请参见上面的 Rustonomicon 链接。

6
来自Rust源代码的相关部分如下所示:

cell.rs中的内容:

#[lang = "unsafe_cell"]
pub struct UnsafeCell<T: ?Sized> {
    value: T,
}

具体来说,#[lang = "unsafe_cell"] 告诉编译器这个特定的类型映射到其内部的"内在可变性类型"。这种东西被称为"语言项"。

不能为此目的定义自己的类型,因为你不能有多个单一语言项的实例。唯一的方法是完全用你自己的代码替换标准库。


1
但是UnsafeCellCell有什么关系呢? - John

0
在`testb`中,您将`Foo`引用的生命周期`'a`绑定到`FooRef`参数。这告诉借用检查器`&u2`必须至少与`b`对它的引用一样长寿。请注意,这种推理不需要了解函数体。
在函数内部,由于生命周期注释,借用检查器可以证明第二个参数至少与第一个参数一样长,否则函数将无法编译。
编辑:请忽略此内容;请阅读huon-dbaupp的答案。我保留这个内容,以便您可以阅读评论。

这不也适用于 testa 吗?我觉得你没抓住重点... 或者是我没理解你的意思... 当只调用 testa 时,rustc 不允许移动语句 u2; // (1),但在调用 testa_muttestb 时可以。 - John
不同之处在于,testa 只获得了一个不可变的 FooRef 引用(该引用也需要与给定的 Foo 一样长寿),而在 testb 中,Cell 拥有 FooRef - llogiq
1
@llogiq,所有权并不是重要的事情,例如fn testc<'a>(_: &Option<FooRef<'a>>, _: &'a Foo) {} let c = Some(FooRef(&u1)); testc(&c, &u2);的行为类似于testa而不是testb,尽管Option拥有FooRef就像Cell一样。(区别在于Option<T>T上是变体的,但Cell<T>T上是不变的。) - huon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接