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

Joh*_*ohn 6 mutability rust

请考虑以下代码(Playground版本):

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
}
Run Code Online (Sandbox Code Playgroud)

我很好奇,rustc知道它Cell有内部可变性,可能会继续引用另一个论点.

如果我从头开始创建另一个数据结构,类似于Cell内部可变性,我该怎么说rustc呢?

huo*_*uon 12

Cell编译(忽略u2)和mutates 的代码是Cell整个API的原因需要&指针:

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

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

    fn set(&self, value: T) { ... }
}
Run Code Online (Sandbox Code Playgroud)

它经过精心编写,允许在共享时进行突变,即内部可变性.这允许它在&指针后面公开这些变异方法.传统的变异需要一个&mut指针(及其相关的非混叠限制),因为具有对值的唯一访问权是确保通常对其进行变异安全的唯一方法.

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

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

UnsafeCell

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

允许安全内部可变性,如类型Cell,RefCell(为顺序代码)时,Atomic*S,MutexRwLock(并发代码)都使用UnsafeCell内部,并处周围的一些限制,以确保它是安全的.例如,定义Cell是:

pub struct Cell<T> {
    value: UnsafeCell<T>,
}
Run Code Online (Sandbox Code Playgroud)

Cell通过仔细限制它提供的API来确保突变是安全的:T: Copy上面的代码是关键.

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

终身差异

无论如何,使编译器理解&u2为单元格情况借用的关键是生命周期的变化.通常,编译器会在将函数传递给函数时缩短生命周期,这会使事情变得很好,例如,您可以将字符串literal(&'static str)传递给期望的函数&'a str,因为长'static寿命缩短为'a.这种情况正在发生testa: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>不仅告诉编译器一个事物可能在别名时发生变异(因此抑制了一些未定义的优化),它也是不变的T,即它就像是&mut T为了这个生命周期的目的/借用分析,正是因为它允许代码一样testb.

编译器自动推断出这种差异; 当某种类型的参数/寿命被包含在它变得不变UnsafeCell&mut某处的类型(如FooRefCell<FooRef<'a>>).

Rustonomicon讨论了这个以及其他类似的详细考虑因素.

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


DK.*_*DK. 5

Rust 源代码中的相关部分是这样的:

#[lang = "unsafe_cell"]
pub struct UnsafeCell<T: ?Sized> {
    value: T,
}
Run Code Online (Sandbox Code Playgroud)

具体来说,#[lang = "unsafe_cell"]就是告诉编译器这个特定类型映射到它的“内部可变性类型”的内部概念。这种东西被称为“lang item”。

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