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

时间:2015-10-20 09:32:35

标签: rust mutability

考虑以下代码(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 个答案:

答案 0 :(得分:12)

Cell代码编译(忽略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指针(及其相关的非别名限制),因为对值的唯一访问是确保变异安全的唯一方法。

因此,在共享时创建允许变异的类型的方法是确保其变异API使用&指针而不是&mut。一般来说,这应该通过让类型包含预先编写的类型Cell来完成,即将它们用作构建块。

后来使用u2失败的原因是一个较长的故事...

UnsafeCell

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

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

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

Cell通过谨慎限制其提供的API来确保突变是安全的:上面代码中的T: Copy是关键。

(如果您希望编写具有内部可变性的自己的低级类型,您只需要确保在共享时变异的内容包含在UnsafeCell中。但是,我建议不要这样做:Rust有几个现有的工具(我上面提到的)用于内部可变性,在Rust的别名和变异规则中经过仔细审查是安全和正确的;破坏规则是未定义的行为,很容易导致错误编译的程序。)

终身差异

无论如何,使编译器理解为单元格案例借用&u2的关键是生命周期的变化。通常,编译器会在将事物传递给函数时缩短生命周期,这会使事情变得很好,例如,您可以将字符串文字(&'static str)传递给期望&'a str的函数,因为长'static生命周期缩短为'atesta发生了这种情况:testa(&a, &u2)调用正在将参考的生命周期从最长的(main的整体)缩短为该函数调用。编译器可以自由地执行此操作,因为正常引用在其生命周期中是变体 1 ,即它可以改变它们。

但是,对于testa_mut&mut FooRef<'a>会阻止编译器缩短生命周期(在技术术语&mut T中“T中的”不变“),这完全是因为像testa_mut这样的事情可能会发生。在这种情况下,编译器会看到&mut FooRef<'a>并了解'a生命周期根本不能短路,因此在调用testa_mut(&mut a, &u2)中它必须采用u2值(整个函数)因此导致u2被借用于该区域。

所以,回到内部可变性:UnsafeCell<T>不仅告诉编译器一个事物可能在别名时被突变(因此抑制了一些未定义的优化),它在{{1}中也是不变的,就像这个生命周期/借用分析一样,它就像一个T,正是因为它允许像&mut T这样的代码。

编译器自动推断出这种差异;当某个类型参数/生命周期包含在testbUnsafeCell类型中的某个位置(如&mut中的FooRef)时,它会变为不变。

The Rustonomicon talks about this以及其他类似的详细考虑因素。

1 严格来说,类型系统术语有四个方差级别:双变量,协方差,逆变量和不变性。我相信Rust实际上只有不变性和协方差(存在一些逆转,但它会导致问题并被删除/在被删除的过程中)。当我说“变种”时,它实际上意味着“协变”。有关更多详细信息,请参阅上面的Rustonomicon链接。

答案 1 :(得分:2)

Rust source code的相关部分是:

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

具体来说,#[lang = "unsafe_cell"]告诉编译器这个特定类型映射到其内部概念&#34;内部可变性类型&#34;。这种事情被称为&#34; lang item&#34;。

无法为此目的定义自己的类型,因为你不能拥有单个lang项目的多个实例。唯一可行的方法是,如果您使用自己的代码完全替换标准库。

答案 2 :(得分:0)

testb中,您将'a引用的生命周期Foo绑定到FooRef参数。这告诉借阅检查器&u2必须至少与b对它的引用一样长。请注意,这种推理不需要了解函数体。

在函数中,借用检查器可以证明第二个参数至少与第一个参数一样长,这是由于生命周期注释,否则函数将无法编译。

编辑:忽略这一点;阅读huon-dbaupp的回答。我离开了,所以你可以阅读评论。