为什么使用 Cell 来创建不可移动的对象?

Why is a Cell used to create unmovable objects?

所以我 运行 进入 this code snippet 展示如何在 Rust 中创建 "unmoveable" 类型 - 移动被阻止,因为编译器将对象视为在其整个生命周期内借用。

use std::cell::Cell;
use std::marker;

struct Unmovable<'a> {
    lock: Cell<marker::ContravariantLifetime<'a>>,
    marker: marker::NoCopy
}

impl<'a> Unmovable<'a> {
    fn new() -> Unmovable<'a> {
        Unmovable { 
            lock: Cell::new(marker::ContravariantLifetime),
            marker: marker::NoCopy
        }
    }
    fn lock(&'a self) {
        self.lock.set(marker::ContravariantLifetime);
    }
    fn new_in(self_: &'a mut Option<Unmovable<'a>>) {
        *self_ = Some(Unmovable::new());
        self_.as_ref().unwrap().lock();
    }
}

fn main(){
    let x = Unmovable::new();
    x.lock();

    // error: cannot move out of `x` because it is borrowed
    // let z = x; 

    let mut y = None;
    Unmovable::new_in(&mut y);

    // error: cannot move out of `y` because it is borrowed
    // let z = y; 

    assert_eq!(std::mem::size_of::<Unmovable>(), 0)
}

我还不明白这是怎么回事。我的猜测是借用指针参数的生命周期被强制匹配锁定字段的生命周期。奇怪的是,如果出现以下情况,此代码将继续以相同的方式工作:

但是,如果我删除 Cell,直接使用 lock: marker::ContravariantLifetime<'a>,就像这样:

use std::marker;

struct Unmovable<'a> {
    lock: marker::ContravariantLifetime<'a>,
    marker: marker::NoCopy
}

impl<'a> Unmovable<'a> {
    fn new() -> Unmovable<'a> {
        Unmovable { 
            lock: marker::ContravariantLifetime,
            marker: marker::NoCopy
        }
    }
    fn lock(&'a self) {
    }
    fn new_in(self_: &'a mut Option<Unmovable<'a>>) {
        *self_ = Some(Unmovable::new());
        self_.as_ref().unwrap().lock();
    }
}

fn main(){
    let x = Unmovable::new();
    x.lock();

    // does not error?
    let z = x;

    let mut y = None;
    Unmovable::new_in(&mut y);

    // does not error?
    let z = y;

    assert_eq!(std::mem::size_of::<Unmovable>(), 0)
}

然后允许"Unmoveable"对象移动。为什么会这样?

一个有趣的问题!这是我的理解...

这是另一个不使用 Cell 的例子:

#![feature(core)]

use std::marker::InvariantLifetime;

struct Unmovable<'a> { //'
    lock: Option<InvariantLifetime<'a>>, //'
}

impl<'a> Unmovable<'a> {
    fn lock_it(&'a mut self) { //'
        self.lock = Some(InvariantLifetime)
    }
}

fn main() {
    let mut u = Unmovable { lock: None };
    u.lock_it();
    let v = u;
}

(Playpen)

这里的重要技巧是结构需要借用自身。一旦我们这样做了,它就不能再被移动,因为任何移动都会使借用无效。这在概念上与任何其他类型的借用没有区别:

struct A(u32);

fn main() {
    let a = A(42);
    let b = &a;
    let c = a;
}

唯一的问题是您需要某种方法让结构包含它自己的引用,这在构建时是不可能做到的。我的示例使用 Option,这需要 &mut self,而链接的示例使用 Cell,它允许内部可变性并且仅 &self.

这两个示例都使用生命周期标记,因为它允许类型系统跟踪生命周期而无需担心特定实例。

让我们看看你的构造函数:

fn new() -> Unmovable<'a> { //'
    Unmovable { 
        lock: marker::ContravariantLifetime,
        marker: marker::NoCopy
    }
}

这里,放入 lock 的生命周期由调用者选择,它最终成为 Unmovable 结构的正常生命周期。没有借自己。

接下来看看你的锁定方法:

fn lock(&'a self) {
}

在这里,编译器知道生命周期不会改变。但是,如果我们让它可变:

fn lock(&'a mut self) {
}

砰!它再次被锁定。这是因为编译器知道内部字段 可以 改变。我们实际上可以将其应用于我们的 Option 变体并删除 lock_it!

的主体

真正的答案包括对生命周期的适度复杂考虑variancy,以及需要整理的代码的一些误导性方面。

对于下面的代码,'a是一个任意的生命周期,'small是一个小于'a的任意生命周期(这可以用约束'a: 'small), 'static 被用作生命周期大于 'a.

的最常见示例

以下是考虑中要遵循的事实和步骤:

  • 通常,生命周期是逆变&'a T'a 是逆变的(在没有任何变体标记的情况下 T<'a> 也是如此),这意味着如果您有 &'a T,可以替换更长的时间生命周期比 'a,例如你可以把一个&'static T存放在这样一个地方,然后把它当作一个&'a T(你可以缩短生命周期)。

  • 在一些地方,生命周期可以不变;最常见的例子是 &'a mut T,它对于 'a 是不变的,这意味着如果你有一个 &'a mut T,你不能在其中存储一个 &'small mut T(借位不t live long enough), 但你也不能在其中存储 &'static mut T,因为这会给存储的引用带来麻烦,因为它会忘记它 actually lived for更长的时间,因此您最终可能会同时创建多个可变引用。

  • 一个Cell contains an UnsafeCell;不那么明显的是 UnsafeCell 很神奇,被连接到编译器以作为名为“unsafe”的语言项进行特殊处理。重要的是,UnsafeCell<T> 对于 T 不变的 ,原因类似于 &'a mut T 对于 'a 的不变性.

  • 因此,Cell<<em>任何生命周期变异标记</em>>实际上与Cell<InvariantLifetime<'a>>表现相同.

  • 此外,您实际上不再需要使用 Cell;你可以只使用 InvariantLifetime<'a>.

  • 返回示例,删除了 Cell 包装和 ContravariantLifetime(实际上等同于仅定义 struct Unmovable<'a>;,因为逆变是默认值,因为没有Copy 实现):为什么允许移动值? ......我必须承认,我还没有理解这个特殊情况,并且希望能帮助我自己理解为什么允许这样做。似乎回到前面,协变性允许锁的寿命很短,但逆变性和不变性不会,但实际上似乎只有不变性才能执行所需的功能。

无论如何,这是最终结果。 Cell<ContravariantLifetime<'a>> 更改为 InvariantLifetime<'a>,这是唯一的功能更改,使 lock 方法按需要运行,借用具有不变的生命周期。 (另一种解决方案是让 lock 采用 &'a mut self,因为正如已经讨论过的,可变引用是不变的;然而,这是次等的,因为它需要不必要的可变性。)

还有一件事需要提及:locknew_in 方法的内容完全是多余的。函数体永远不会改变编译器的静态行为;只有签名很重要。生命周期参数 'a 被标记为不变的事实是关键点。因此,new_in 的整个“构造一个 Unmovable 对象并对其调用 lock” 部分是完全多余的。同样,在 lock 中设置单元格的内容也是浪费时间。 (请注意,Unmovable<'a>'a 的不变性再次使 new_in 起作用,而不是它是可变引用这一事实。)

use std::marker;

struct Unmovable<'a> {
    lock: marker::InvariantLifetime<'a>,
}

impl<'a> Unmovable<'a> {
    fn new() -> Unmovable<'a> {
        Unmovable { 
            lock: marker::InvariantLifetime,
        }
    }

    fn lock(&'a self) { }

    fn new_in(_: &'a mut Option<Unmovable<'a>>) { }
}

fn main() {
    let x = Unmovable::new();
    x.lock();

    // This is an error, as desired:
    let z = x;

    let mut y = None;
    Unmovable::new_in(&mut y);

    // Yay, this is an error too!
    let z = y;
}