原文
#![allow(unused)]
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(unused_variables)]
fn main() {
//! 一个展示 Rust 中生命周期型变 (variance) 的示例
//!
//! 这个示例可以通过 Cargo 进行编译及测试
use std::cell::Cell;
use std::collections::HashSet;
use std::fmt;
// -------------------------
// (I) 建立一个直观感受
// -------------------------
// 这个函数传入了一个静态字符串,并缩短了其生命周期
fn lifetime_shortener<'a>(s: &'static str) -> &'a str {
s
}
// 直观上来看,它的编译原则应该是:如果一个字符串在整个过程中持续存在,那么它的组成部分也应该持续存在。确实如此!
// 现在,让它稍微复杂一些。让我们在图片中引入一个`Cell`。
// 提醒一下,`Cell`允许修改其内部的数据
#[cfg(feature = "compile-fail")]
fn cell_shortener<'a, 'b>(s: &'a Cell<&'static str>) -> &'a Cell<&'b str> {
s
}
// cell_shortener 函数不能编译:(你能说明是因为什么吗?花一分钟时间思考一下,尝试使用你的直觉
#[cfg(feature = "compile-fail")]
fn cell_example() {
// 思考一下这个 Cell。它持有一个静态的字符串
let foo: Cell<&'static str> = Cell::new("foo");
// 你认为这个可以起作用吗?
let owned_string: String = "non_static".to_owned();
foo.replace(&owned_string);
// 看起来好像不行,是吗?foo 承诺它包含的应该是一个 &'static str,
// 但我们试图放入一个范围为该函数所拥有的字符串。
}
#[cfg(feature = "compile-fail")]
fn cell_counterexample() {
let foo: Cell<&'static str> = Cell::new("foo");
let owned_string: String = "non_static".to_owned();
// 我们假设 cell_shortener 起作用了
let shorter_foo = cell_shortener(&foo);
// 然后 shorter_foo 和 foo 将是彼此的别名,这意味着你可以使用 shorter_foo 将 foo 的 Cell 替换为非静态字符串
shorter_foo.replace(&owned_string);
// 现在的 foo,是 shorter_foo 的别名,里面包含一个非静态字符串!哎呀呀
}
// 不只是 Cell 会出现这种问题,RefCell,OnceCell,Mutex,&mut 引用 -- 某种可变上下文的内部都会出现这种问题
// 现在,假设 (hypothetical) 该函数会将 s 的生命周期变长会怎么样呢?
#[cfg(feature = "compile-fail")]
fn lifetime_lengthener<'a>(s: &'a str) -> &'static str {
s
}
// 这显然是伪造的 (bogus),对吗?你不能将任意借用字符串的生命周期变得和整个过程的持续时间一样长。同样的还有:
#[cfg(feature = "compile-fail")]
fn cell_lengthener<'a, 'b>(s: &'a Cell<&'b str>) -> &'a Cell<&'static str> {
s
}
// 但是这种呢?fn 是一个指向函数的指针,它以任意借用的字符串作为参数。
fn fn_ptr_lengthener<'a>(f: fn(&'a str) -> ()) -> fn(&'static str) -> () {
f
}
// 啊啊啊,直觉是这应该可以工作。并且它确实如此。你可以使用一个传入任意借用字符串的回调,然后将其转换为采用静态字符串的回调。
// -------------------------
// (II) 形式化型变
// -------------------------
// 如何将所有这些直觉形式化?它是通过*型变*的想法完成的
//
// 某些类型的内存生命周期比其他类型的内存更长。这是通过 *outlives* 关系的想法捕获的。如果 'b 的寿命比 'a 长,则写为 'b: 'a。举个例子,在定义中:
struct OutlivesExample<'a, 'b: 'a> {
a_str: &'a str,
b_str: &'b str,
}
// 借用字符串`b_str`的生命周期至少和`a_str`一样长,可能会更久。
// Rust 编译器使用以下三个设置的其中之一来标注每一个生命周期。对于类型 T<'a>, 'a 可能是:
//
// * *协变 (covariant)*,它意味着如果 'b: 'a 则 T<'b>: T<'a>。这是不可变数据的默认配置。
//
// * *不变 (invariant)*,它意味着即使 'b: 'a,也无法描述 T<'b> 和 T<'a> 之间的关系。满足这两个原因其中之一,就会发生这种情况:
//
// * 如果生命周期存在于某些可变上下文中 -- 无论是 &mut 引用,还是内部可变的结构 (如 Cell/RefCell/Mutex)。
// * 如果将生命周期用于型变冲突的多个位置。请看 (III) 的示例
//
// * *逆变 (contravariant)*,它意味着如果 'b: 'a 则 T<'a>: T<'b>。这不是很常见,并且只在函数指针的参数中出现。
//
// 参数的型变完全通过类型定义确定,没有为其标记特征。
// ---
// 快速练习。在下面的结构中,每一种参数的生命周期都是哪一种型变?
struct Multi<'a, 'b, 'c, 'd1, 'd2> {
a: &'a str,
b: Cell<&'b str>,
c: fn(&'c str) -> usize,
d: &'d1 mut &'d2 str,
}
// ...
// 答案是:
// * 'a 是协变,因为它只在不可变的上下文中显式。
// 这就意味着。和上面的 shortener 函数一样,你可以定义一个如下函数:
fn a<'a, 'b, 'c, 'd1, 'd2>(x: Multi<'static, 'b, 'c, 'd1, 'd2>) -> Multi<'a, 'b, 'c, 'd1, 'd2> {
x
}
// * 'b 是不变,因为它位于可变的 Cell 上下文中。
// (练习:尝试写出一个由于 'b 是不变,从而导致无法编译的函数)
// * 'c is 逆变,因为它显式在回调的参数中。
fn c<'a, 'b, 'c, 'd1, 'd2>(x: Multi<'a, 'b, 'c, 'd1, 'd2>) -> Multi<'a, 'b, 'static, 'd1, 'd2> {
x
}
// * 'd1 是*协变*!尽管它是一个可变引用,但它不在 &mut 指针中。
fn d1<'a, 'b, 'c, 'd1, 'd2>(x: Multi<'a, 'b, 'c, 'static, 'd2>) -> Multi<'a, 'b, 'c, 'd1, 'd2> {
x
}
// * 'd2 是不变,因为它在可变引用里。
// -----------------------------------
// (III) 冲突和类型参数
// -----------------------------------
// 如果将一个生命周期参数用于不同型变的多个地方会怎样?举个例子
struct TwoSpots<'a> {
foo: &'a str,
bar: Cell<&'a str>,
}
// 如你所料:
// * 如果所有的用法都满足一个特定的型变,则该参数具有该类型的型变。
// * 否则,该参数的生命周期默认为不变。
// 那么这种情况呢?
struct TypeParams<T, U> {
t: Vec<T>,
u: fn(U) -> (),
}
// 当 T 和 U 被替换为包含生命周期参数的类型时,也会使用型变对其进行标注。举个例子:
struct LifetimeParams<'a, 'b> {
nested: TypeParams<&'a str, &'b str>,
}
// 在这里,'a 是一个型变,'b 是一个逆变。让我们一起对他们进行测试:
fn lifetime_check<'a, 'b>(x: LifetimeParams<'static, 'b>) -> LifetimeParams<'a, 'static> {
x
}
// -------------------------
// (IV) 实践中的型变
// -------------------------
// 那么,作为 Rust 开发人员,你为什么要关心呢?
// 许多 Rust 开发人员开始使用引用计数的智能指针(如:`Rc` 或者 `Arc`),而不是到处借用数据。如果你这么做的话,你就不太可能遇到生命周期的问题。
// 但是最终你可能会为了更高的性能,尝试借用数据 -- 如果这样子的话,你可能会在你的代码中引入生命周期参数。这时型变就变得很重要。
// 一些最棘手的问题使得 rustc 接受普通使用借用数据的代码,并最终以某种方式产生型变。
//
// 举个例子,考虑以下情景,从真实场景提取的 Rust 代码:
// 考虑一下这个代表消息的结构。
struct Message<'msg> {
message: &'msg str,
}
// ... 这个结构收集将要展示的消息。
struct MessageCollector<'a, 'msg> {
list: &'a mut Vec<Message<'msg>>,
}
impl<'a, 'msg> MessageCollector<'a, 'msg> {
// 这里在 list 的末尾添加了一条消息。
fn add_message(&mut self, message: Message<'msg>) {
self.list.push(message);
}
}
// 这个结构展示了收集的消息
struct MessageDisplayer<'a, 'msg> {
list: &'a Vec<Message<'msg>>,
}
impl<'a, 'msg> fmt::Display for MessageDisplayer<'a, 'msg> {
// 以换行作为分隔符,这里展示了所有的消息。
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for message in self.list {
write!(f, "{}\n", message.message)?;
}
Ok(())
}
}
fn message_example() {
// 这是一个简单的消息池
let mut message_pool: HashSet<String> = HashSet::new();
message_pool.insert("ten".to_owned());
message_pool.insert("twenty".to_owned());
// 一切就绪,让我们尝试收集并展示一些消息!
collect_and_display(&message_pool);
}
fn collect_and_display<'msg>(message_pool: &'msg HashSet<String>) {
let mut list = vec![];
// 收集一些信息。(这很简单,但是你可以想象将收集器传递到其他的代码中)。
let mut collector = MessageCollector { list: &mut list };
for message in message_pool {
collector.add_message(Message { message });
}
// 现在,让我们展示这些消息
let displayer = MessageDisplayer { list: &list };
println!("{}", displayer);
}
// 它起作用了,但是可以更简单一点吗?让我们尝试减少生命周期参数的数量,首先,对于 displayer
struct SimpleMessageDisplayer<'a> {
list: &'a Vec<Message<'a>>,
}
impl<'a> fmt::Display for SimpleMessageDisplayer<'a> {
// 这里展示了所有的消息
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for message in self.list {
write!(f, "{}\n", message.message)?;
}
Ok(())
}
}
fn collect_and_display_2<'msg>(message_pool: &'msg HashSet<String>) {
// OK,让我们做与 collect_and_display 相同的事,除了使用简单的 displayer。
let mut list = vec![];
// 收集一些消息
let mut collector = MessageCollector { list: &mut list };
for message in message_pool {
collector.add_message(Message { message });
}
// 最后展示它们
let displayer = SimpleMessageDisplayer { list: &list };
println!("{}", displayer);
}
// OK,它起作用了。我们可以对 collector 做同样的事情吗?让我们尝试以下:
struct SimpleMessageCollector<'a> {
list: &'a mut Vec<Message<'a>>,
}
impl<'a> SimpleMessageCollector<'a> {
// 在 list 的末尾添加一条消息
fn add_message(&mut self, message: Message<'a>) {
self.list.push(message);
}
}
#[cfg(feature = "compile-fail-final")]
fn collect_and_display_3<'msg>(message_pool: &'msg HashSet<String>) {
// OK,再一次
let mut list = vec![];
// 收集一些消息
let mut collector = SimpleMessageCollector { list: &mut list };
for message in message_pool {
collector.add_message(Message { message });
}
// 最后展示它们
let displayer = SimpleMessageDisplayer { list: &list };
println!("{}", displayer);
}
// 这没有用!rustc (1.43.1) 显式错误:“无法借用不可变的`list`,因为它也作为可变被借用了”
//
// 为什么减少生命周期参数对于 MessageDisplayer 起作用了,但是却没有对 MessageCollector 起作用?这都是因为型变。让我们再一次看下这个结构,首先查看 displayer:
struct MessageDisplayer2<'a, 'msg> {
// 两个生命周期参数
list: &'a Vec<Message<'msg>>,
// 在这里,编译器可以独立的改变两者,因此 list 的生命周期可以比 'msg 更短,然后释放。
}
// 简单的版本:
struct SimpleMessageDisplayer2<'a> {
// 'a 被用于两个位置:
//
// | |
// v v
list: &'a Vec<Message<'a>>,
//
// 但是由于它们两个都是协变的(在不可变的上下文中),'a 也是协变的。
// 这意味着编译器可以在内部将 &'a Vec<Message<'msg>> 转换为 &'a Vec<Message<'a>>,并将 list 保留较短的 'a 的持续时间。
}
// 现在是 collector:
struct MessageCollector2<'a, 'msg> {
// 又是两个生命周期参数:
list: &'a mut Vec<Message<'msg>>,
// 在这里,'a 是一个协变,但是 'msg 是不变,因为它在 &mut 引用的内部。
// 编译器可以分别更改两者,这意味着 list 可以保留比 'msg 更短的生命周期。
}
// 最后,有问题的简单版本:
struct SimpleMessageCollector2<'a> {
// 'a 再一次用于两个位置:
//
// | |
// v v
list: &'a mut Vec<Message<'a>>,
//
// 第一个 'a 是协变,第二个是不变,因为它在 &mut 引用的内部!
// 这意味着 'a 是不变的,这最终造成编译器尝试并拥有比标准 MessageCollector 生命周期更长的 list。
}
// ---
// 如果要编写 Rust 库,请注意以下几点:
//
// 将参数(生命周期或类型)的型变从协变改为其他类型,或者从逆变改为其他类型,是一个巨大的变更。如果你遵循 semver,则只能使用最新的主要版本来完成。
//
// 将参数从不变转换成协变或逆变,不是一种重大的变更
// ---
// 无论如何,希望这可以让你在 Rust 代码中更自信的使用生命周期,他们是编写安全,快速代码的有效方式。但是在实践中,型变常常会造成晦涩的问题 -- 了解其工作的方式是有效使用生命周期的关键。
// 感谢以下同学的反馈:
// * Nikolai Vazquez (@NikolaiVazquez on Twitter, nvzqz on GitHub)
// * Inanna Malick (@inanna_malick on Twitter, inanna-malick on GitHub)
}