Rust 编译器如何知道“Cell”具有内部可变性?
How does the Rust compiler know `Cell` has internal 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
?
Rust source code 中的相关部分是这样的:
#[lang = "unsafe_cell"]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
具体来说,#[lang = "unsafe_cell"]
告诉编译器此特定类型映射到其内部概念 "the interior mutability type"。这种东西叫做"lang item".
您不能为此目的定义您自己的类型,因为您不能拥有单个 lang 项的多个实例。唯一的办法就是用自己的代码完全替换标准库。
在 testb
中,您将 Foo
引用的生命周期 'a
绑定到 FooRef
参数。这告诉借用检查器 &u2
必须至少与 b
对它的引用一样长。请注意,此推理不需要了解函数体。
在函数内,借用检查器可以证明第二个参数至少与第一个参数一样长,因为生命周期注释,否则函数将无法编译。
编辑:忽略这个;阅读 huon-dbaupp 的回答。我要离开这里,以便您可以阅读评论。
带有 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
中。这是最低级别的内部可变性,旨在用作构建其他抽象的构建块。
允许安全内部可变性的类型,如 Cell
、RefCell
(对于顺序代码)、Atomic*
s、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
的整个主体)到仅是该函数调用。编译器可以自由地执行此操作,因为普通引用在其生命周期中是 variant1,即它可以改变它们。
然而,对于 testa_mut
,&mut FooRef<'a>
阻止编译器缩短生命周期(在技术术语中 &mut T
是 "invariant in T
"),正是因为类似testa_mut
可能会发生。在这种情况下,编译器看到 &mut FooRef<'a>
并理解 'a
生命周期根本不能缩短,因此在调用 testa_mut(&mut a, &u2)
时它必须采用真正的生命周期u2
值(整个函数),因此导致为该区域借用 u2
。
因此,回到内部可变性:UnsafeCell<T>
不仅告诉编译器一个事物在使用别名时可能会发生变异(因此抑制了一些未定义的优化),它在 T
,即为了这个 lifetime/borrowing 分析的目的,它的行为类似于 &mut T
,正是因为它允许像 testb
.
这样的代码
编译器自动推断出这种差异;当某些类型 parameter/lifetime 包含在 UnsafeCell
或 &mut
中某处时,它变得不变(例如 Cell<FooRef<'a>>
中的 FooRef
)。
The Rustonomicon talks about this 等详细的考虑就好了。
1 严格来说,类型系统术语中有四个级别的变化:双变、协变、逆变和不变。我相信 Rust 真的只有不变性和协变性(有一些逆变性,但它引起了问题并且是 removed/in 被移除的过程)。当我说 "variant" 时,它的实际意思是 "covariant"。有关详细信息,请参阅上面的 Rustonomicon link。
考虑以下代码 (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
?
Rust source code 中的相关部分是这样的:
#[lang = "unsafe_cell"]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
具体来说,#[lang = "unsafe_cell"]
告诉编译器此特定类型映射到其内部概念 "the interior mutability type"。这种东西叫做"lang item".
您不能为此目的定义您自己的类型,因为您不能拥有单个 lang 项的多个实例。唯一的办法就是用自己的代码完全替换标准库。
在 testb
中,您将 Foo
引用的生命周期 'a
绑定到 FooRef
参数。这告诉借用检查器 &u2
必须至少与 b
对它的引用一样长。请注意,此推理不需要了解函数体。
在函数内,借用检查器可以证明第二个参数至少与第一个参数一样长,因为生命周期注释,否则函数将无法编译。
编辑:忽略这个;阅读 huon-dbaupp 的回答。我要离开这里,以便您可以阅读评论。
带有 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
中。这是最低级别的内部可变性,旨在用作构建其他抽象的构建块。
允许安全内部可变性的类型,如 Cell
、RefCell
(对于顺序代码)、Atomic*
s、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
的整个主体)到仅是该函数调用。编译器可以自由地执行此操作,因为普通引用在其生命周期中是 variant1,即它可以改变它们。
然而,对于 testa_mut
,&mut FooRef<'a>
阻止编译器缩短生命周期(在技术术语中 &mut T
是 "invariant in T
"),正是因为类似testa_mut
可能会发生。在这种情况下,编译器看到 &mut FooRef<'a>
并理解 'a
生命周期根本不能缩短,因此在调用 testa_mut(&mut a, &u2)
时它必须采用真正的生命周期u2
值(整个函数),因此导致为该区域借用 u2
。
因此,回到内部可变性:UnsafeCell<T>
不仅告诉编译器一个事物在使用别名时可能会发生变异(因此抑制了一些未定义的优化),它在 T
,即为了这个 lifetime/borrowing 分析的目的,它的行为类似于 &mut T
,正是因为它允许像 testb
.
编译器自动推断出这种差异;当某些类型 parameter/lifetime 包含在 UnsafeCell
或 &mut
中某处时,它变得不变(例如 Cell<FooRef<'a>>
中的 FooRef
)。
The Rustonomicon talks about this 等详细的考虑就好了。
1 严格来说,类型系统术语中有四个级别的变化:双变、协变、逆变和不变。我相信 Rust 真的只有不变性和协变性(有一些逆变性,但它引起了问题并且是 removed/in 被移除的过程)。当我说 "variant" 时,它的实际意思是 "covariant"。有关详细信息,请参阅上面的 Rustonomicon link。