这段代码之所以能够编译(忽略 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
中。这是内部可变性的最低级别,旨在用作构建其他抽象的构建块。
允许安全内部可变性的类型,如Cell
、RefCell
(用于顺序代码)、Atomic*
、Mutex
和RwLock
(用于并发代码)都在内部使用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 T
在
T
中是“不变的”),正因为类似
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 链接。
UnsafeCell
和Cell
有什么关系呢? - John