8

考虑以下代码(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
}

我很好奇如何rustc知道它Cell具有内部可变性并且可能会保留对另一个论点的引用。

如果我从头开始创建另一个数据结构,类似于Cell它也具有内部可变性,我该如何判断rustc

4

3 回答 3

14

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*sMutexRwLock(用于并发代码)都UnsafeCell在内部使用并对其施加一些限制以确保它是安全的。例如, 的定义Cell是:

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

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

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

终身差异

无论如何,让编译器理解&u2为单元案例借用的关键是生命周期的变化。通常,当您将内容传递给函数时,编译器会缩短生命周期,这会使事情变得更好,例如,您可以将字符串文字 ( &'static str) 传递给期望的函数&'a str,因为长'static生命周期缩短为'a. 发生这种情况的原因是testatesta(&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即它的作用类似于 a &mut T/借用分析,正是因为它允许像testb.

编译器会自动推断出这种差异;UnsafeCell当某些类型参数/生命周期包含在类型中或&mut某处(如FooRefin Cell<FooRef<'a>>)时,它变得不变。

Rustonomicon 讨论了这一点以及其他类似的详细考虑。

1严格来说,类型系统行话中有四个级别的方差:双变、协变、逆变和不变。我相信 Rust 确实只有不变性和协变(有一些逆变性,但它会导致问题并被移除/正在被移除)。当我说“变体”时,它实际上意味着“协变”。有关更多详细信息,请参见上面的 Rustonomicon 链接。

于 2015-10-20T10:08:41.347 回答
5

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

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

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

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

于 2015-10-20T09:50:46.083 回答
0

在中,您将引用testb的生命周期绑定到参数。这告诉借用检查器必须至少与' 对它的引用一样长。请注意,这种推理不需要函数体的知识。'aFooFooRef&u2b

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

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

于 2015-10-20T09:50:50.577 回答