2.9.6 &Struct 与协变
译文 · 基于 Learning Rust
&Struct 与协变
原文链接: https://quinedot.github.io/rust-learning/pf-shared-nested.html
这里有一个看起来类似于永远借用某物的情况,但实际上有些不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| struct Person<'a> {
given: &'a str,
sur: &'a str,
}
impl<'a> Person<'a> {
// vvvvvvvv `&'a Person<'a>`
fn given(&'a self) -> &'a str {
self.given
}
}
fn example(person: Person<'_>) {
// 与永远借用某物不同,这可以编译
let _one = person.given();
let _two = person.given();
}
|
区别在于 &U 在 U 上是协变的,所以
生命周期可以在引用背后"缩短"
(与 &mut U 不同,它在 U 上是不变的)。Person<'a> 在 'a 上也是协变的,因为我们在定义中对 'a 的所有使用都处于协变位置。
这一切意味着 &'long Person<'long> 可以强制转换为 &'short Person<'short>。因此,调用 Person::given 不必永远借用 person——它只需要在返回值被使用期间借用 person。
注意协变性是必需的!内部生命周期不变时的共享嵌套借用,几乎和"永远借用"的 &mut 情况一样糟糕。本页大部分讨论协变情况;我们将在末尾考虑不变情况。
这仍然是一个黄色警告
尽管不像 &mut 情况那么成问题,那个签名仍然有些不理想:它迫使对 person 的借用比必要的更长。例如,这会失败:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # struct Person<'a> { given: &'a str }
# impl<'a> Person<'a> { fn given(&'a self) -> &'a str { self.given } }
# struct Stork(String);
# impl Stork { fn deliver(&self, _: usize) -> Person<'_> { Person { given: &self.0 } } }
fn example(stork: Stork) {
let mut last = "";
for i in 0..10 {
let person = stork.deliver(i);
last = person.given();
// ...
}
println!("Last: {last}");
}
|
person 必须在返回值存在期间保持借用,因为我们说 &self 和返回的 &str 必须有相同的生命周期。
如果我们改为允许生命周期不同:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # struct Person<'a> { given: &'a str }
# struct Stork(String);
# impl Stork { fn deliver(&self, _: usize) -> Person<'_> { Person { given: &self.0 } } }
impl<'a> Person<'a> {
// vvvvv 我们从 `&self` 中移除了 `'a`
fn given(&self) -> &'a str {
self.given
}
}
fn example(stork: Stork) {
let mut last = "";
for i in 0..10 {
let person = stork.deliver(i);
last = person.given();
// ...
}
println!("Last: {last}");
}
|
那么对 person 的借用可以在调用后立即结束,即使返回值仍然可用。这是可能的,因为我们只是复制引用出来。或者如果你更喜欢另一种思考方式,我们给出的是对已有借用的重借用,而不是借用我们自己拥有的东西。
所以现在 stork 仍然必须在 last 被使用期间存在,但 person 可以在循环结束时消失。
当你有一个以某种方式管理借用资源的结构体时,允许生命周期不同通常是你想要的——当你分发借用资源的片段时,你希望它们与原始借用的生命周期绑定,而不是与方法调用上的 &self 或 &mut self 的生命周期绑定。借用迭代器就是这样工作的, 例如。
主题的一个变体
考虑这个版本的方法:
1
2
3
4
5
6
| # struct Person<'a> { given: &'a str }
impl<'a> Person<'a> {
fn given(&self) -> &str {
self.given
}
}
|
它与 given(&'a self) -> &'a str 有同样的缺点:返回值与 self 绑定,而不是与 'a 绑定。在开发借用结构体时很容易犯这个错误,因为生命周期省略规则会引导你朝这个方向走。它也更难发现,因为没有 &'a self 来提示你。
但有时完全没问题
另一方面,由于我们在本页开头讨论的协变性,这两个方法之间没有实际区别:
1
2
3
4
5
| # struct Person<'a> { given: &'a str }
impl<'a> Person<'a> {
fn foo(&self) {}
fn bar(&'a self) {}
}
|
没有返回值来强制生命周期更长,所以这些方法的行为是一样的。&'a self 上的 'a 没有理由,但也没有害处。
同样,在结构体内部,将嵌套生命周期分开很少有好处,所以你可能不如使用:
1
2
3
4
| # struct Person<'a> { given: &'a str }
struct Cradle<'a> {
person: &'a Person<'a>
}
|
而不是使用两个生命周期的写法。
(话虽如此,更好的方法是根本不要有复杂的嵌套借用数据结构。)
不变情况
最后,让我们看一个通常不太行的情况:内部借用不变的共享嵌套借用。
也许最可能出现的原因是共享可变性:能够改变共享引用(&)背后的东西的能力。标准库中的一些例子包括 Cell<T>、
RefCell<T> 和
Mutex<T>。这些共享可变性类型在其泛型参数 T 上必须是不变的,就像 &mut T 在 T 上是不变的一样。
让我们看一个例子,类似于我们之前见过的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # use std::cell::Cell;
#[derive(Debug)]
struct ShareableSnek<'a> {
owned: String,
borrowed: Cell<&'a str>,
}
impl<'a> ShareableSnek<'a> {
fn bite(&'a self) {
self.borrowed.set(&self.owned);
}
}
let snek = ShareableSnek {
owned: "🐍".to_string(),
borrowed: Cell::new(""),
};
snek.bite();
// 与 `&mut` 情况不同,我们仍然可以使用 `snek`!它被永远借用了,
// 但只是*共享*-借用永远。
println!("{snek:?}");
|
这似乎不太糟糕,对吧?嗯,它不像 &mut 情况那么糟糕,但通常仍然太受限而无法实用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # use std::cell::Cell;
# #[derive(Debug)]
# struct ShareableSnek<'a> {
# owned: String,
# borrowed: Cell<&'a str>,
# }
#
# impl<'a> ShareableSnek<'a> {
# fn bite(&'a self) {
# self.borrowed.set(&self.owned);
# }
# }
#
# let snek = ShareableSnek {
# owned: "🐍".to_string(),
# borrowed: Cell::new(""),
# };
#
snek.bite();
let _mutable_stuff = &mut snek;
let _move = snek;
// 有非平凡的析构函数也会导致失败
|
一旦被永远借用,snek 只能以"共享"方式使用。它只能通过共享可变性被修改,不能被移动——它被永远固定在了原地。