为什么 Rust 中的通用生命周期参数可以专门用于一个对象的两个不相交的生命周期?
Why can a generic lifetime parameter in Rust be specialized to two disjoint lifetimes for one object?
在下面的代码中,我试图了解通用生命周期参数 'a
是如何专门化的。
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0; // the lifetime of x, call it 'x, starts from here |
r = Wrapper(&x); // 'a parameter in Wrapper is specialized to 'x |
drop(r); // |
} // --------------------------------- 'x ends here |
{
let y = 0; // the lifetime of y, call it 'y, starts from here |
// 'y is distinct from 'x |
// neither outlives the other one |
r = Wrapper(&y); // why 'a parameter can again be specialized to 'y? |
drop(r); // |
} // ------------------------------------ 'y ends here |
}
为什么通用生命周期参数 'a
可以专门用于一个对象 'r
的两个不相交的生命周期 'x
和 'y
?
从另一个角度来说,我对r
的具体类型很困惑。从 Rustonomicon 的 subtyping and variance 章节中,我了解到生命周期 'a
是泛型类型 Wrapper<'a>
的一部分。当泛型变得特化时,它既不能是 Wrapper<'x>
也不能是 Wrapper<'y>
。那么r
的类型是什么?
也许higher-rank trait bound与它有关?
如果有人能解释 Rust 编译器内部如何解释这一点,我将不胜感激。
更新:
现有答案表明 r
的生命周期可以有多个起点和终点。如果这是真的,那么 'r
就是 'x
和 'y
的并集。但是,这种解释与 subtyping 规则不太吻合。例如,考虑下面的代码,'r2
和 'r
都不是另一个的子类型(或长于),所以我们不应该调用 bar(r, r2)
,但实际上我们可以。矛盾。
struct Wrapper<'a>(&'a i32);
fn bar<'a>(_p: Wrapper<'a>, _q: Wrapper<'a>) {}
fn foo() {
let mut r;
{
let z = 0;
let r2 = Wrapper(&z); // -> |
{ // |
let x = 0; // -> | // +--'r2
r = Wrapper(&x); // |--'x--+ // |
bar(r, r2); // | | // <- |
} // <- | |
} // |
{ // +--'r
let y = 0; // -> | |
r = Wrapper(&y); // |--'y--+
drop(r); // |
} // <- |
}
你对人生的看法是错误的。 r
的生命周期不会专门化为 'x
或 'y
。它有自己的生命周期(我们称之为 'r
)。
如 Non-Lexical Lifetimes RFC 中所述,值的生命周期(或 RFC 所称的作用域)是程序中该值将来可能仍会被使用的时间点。至关重要的是,生命周期不一定是线性的,也不必适合特定的语法范围(即 {}
块)。此外,生命周期不连接到变量(如 r
),而是连接到它们绑定的值。因此,如果您分配给一个变量(即 r = ..
),您实际上会杀死一个值并启动另一个值。它不算作使用。但是,分配给一个值的成员(即 r.0 = ..
), 是 一种用法,因为您正在更改现有值的一小部分。
在您的例子中,'r
有两个起点和两个终点。第一个起点在 r = Wrapper(&x);
,第一个终点在第一个 drop(r)
。第二个起点和终点在 y 块中。可视化:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x); // -> |
// |--'x--+
drop(r); // <- | |
} // |
{ // +--'r
let y = 0; // |
r = Wrapper(&y); // -> | |
// |--'y--+
drop(r); // <- |
}
}
这里我也插入了'x
和'y
供参考。请注意 'r
具体 而不是 存在于两个范围之间。
借用检查时,生命周期要求是生命周期需要比长寿。假设您有界限 'a: 'b
('a
必须比 'b
长)。为了使这个必然成立,任何 'b
是活的,'a
也必须是活的。这就是outlives的意思。另一种说法是:'a
lasts longer than 'b
。
然而,在这里我们还必须考虑位置感知的借用检查。这意味着 'a
only needs to outlive 'b
starting from the point their relation begins.
让我们看看我们的例子。对于像 r = Wrapper(&'x x)
这样的语句(我在其中添加了引用的生命周期),借用检查器必须确保引用 &'x x
的生命周期超过 r
的生命周期(即 'x: 'r
) 从 r = Wrapper(&x)
开始。查看上面的生命周期,我们可以看到任何地方 'x
都是活的,'r
也是活的。 'x
在范围结束时死亡,但幸运的是,'r
也是如此。所以这个检查出来了。冲洗并重复 'y
.
为了对此更有信心,让我们看一下示例的一些变体,看看我们如何预测编译器是接受还是拒绝它。
示例 1
我们将简单地删除 drop
调用以查看它是否有所作为:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x);
}
{
let y = 0;
r = Wrapper(&y);
}
}
这不会改变任何生命周期。请记住,生命周期与将来是否会使用值有关。由于我们没有添加 r
、x
或 y
的任何用途,因此生命周期没有变化。
正如预期的那样,it compiles just fine.
示例 2
如果 x
不在范围内怎么办:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
let x = 0;
r = Wrapper(&x);
{
let y = 0;
r = Wrapper(&y);
}
}
此处 'r
和 'x
已移入父范围,但除此之外它们都与原始示例非常相似。同样 'y
。因此所有的界限仍然成立,it compiles。 y
.
也是一样的
示例 3
所以让我们改变一些 r
的用法。如果我们不为 y
创建一个新的 Wrapper
,我们只是简单地覆盖引用:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x);
}
{
let y = 0;
r.0 = &y;
}
}
这发生了实质性的变化 'r
,因为在第一个作用域之后,它将在第二个作用域中使用,以分配一个新的引用。因此,'r
现在从 r = Wrapper(&x)
跨越到 r.0 = &y
。然而,'x
不能匹配这个,因为 x
不会活得足够长(它超出范围)。因此,我们应该得到一个错误,说 x
的寿命不够长,which we do.
示例 4
如果我们只在两个作用域后使用 r
会怎么样:
#[derive(Debug)]
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x);
}
{
let y = 0;
r = Wrapper(&y);
}
println!("{:?}", r)
}
此处,'r
现在比原始示例稍大,在 r = Wrapper(&y)
之后继续打印。但是,它仍然不存在于两个块之间,因为在第一个范围中分配的值从未在第二个范围中使用。因此,'x
比 'r
长。但是,'y
现在需要足够大才能打印出来(因为 'r
这样做了),但它不能,因为 y
会超出范围。因此,编译器应该抱怨 y
活得不够长,而不要说 x
。幸运的是,that is the case.
关于子类型化
问题的更新询问为什么上面的内容似乎与子类型化规则相矛盾。给定一个函数fn some_fn<'a>(a: &'a i32, b: &'a i32)
,为什么我们可以传递两个不同生命周期的引用给这个函数。在这两种情况下,函数声明似乎需要完全相同的生命周期。
答案是子类型有specific rules for lifetimes:
Whenever references are copied from one location to another, the Rust subtyping rules require that the lifetime of the source reference outlives the lifetime of the target location.
这意味着 rust 从不检查两个生命周期是否相等。它只检查它们是否以正确的方式比彼此长寿。在更新的示例中,'a
是对 bar
的调用的生命周期。将 r
和 r2
传递给它时,我们得到边界 'r: 'a
和 'r2: 'a
。这使得它变得非常容易,因为 'a
仅在调用期间处于活动状态,其他生命周期也处于活动状态。调用后,所有三个生命周期都已死亡,因此绑定平凡成立。
我希望我的想法不会与编译器内部实际发生的事情相差太多。
根据 rustc_middle (MIR) 的直接答案:
let mut r;
的类型推断为 Wrapper<'free_region>
,其中 'free_region
没有附加边界。
- 粗略地说,每个表达式都有自己的区域实例,其边界根据它们所在的范围而定。在每个地方,MIR 都会进行关于是否满足特定区域边界的局部推理。对于这种情况:
r = Wrapper(&x);
它检查 r'
类型中的 'free_region
是否可以被 'x
替代,并且因为前者没有边界(因为 [1])它成功了。请注意,它在本地进行替换,因此其他作用域看不到它,它们会看到它们的本地替换。
让我们将 'free_region
绑定到外部范围:
struct Wrapper<'a>(&'a i32);
fn foo<'a>() {
let mut r: Wrapper<'a>;
{
let x = 0;
r = Wrapper(&x); //~ ERROR `x` does not live long enough
drop(r);
}
}
有人可能会想 但是 r
被排除在 'x
的范围内! 原因再次是 局部推理.
Update:值得注意的是,r
只是一个绑定,而不是OOP理解中的对象。就是这样,一个名称的类型,您在其中两次分配了一个右值。 (rvalue表示表达式值)
在下面的代码中,我试图了解通用生命周期参数 'a
是如何专门化的。
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0; // the lifetime of x, call it 'x, starts from here |
r = Wrapper(&x); // 'a parameter in Wrapper is specialized to 'x |
drop(r); // |
} // --------------------------------- 'x ends here |
{
let y = 0; // the lifetime of y, call it 'y, starts from here |
// 'y is distinct from 'x |
// neither outlives the other one |
r = Wrapper(&y); // why 'a parameter can again be specialized to 'y? |
drop(r); // |
} // ------------------------------------ 'y ends here |
}
为什么通用生命周期参数 'a
可以专门用于一个对象 'r
的两个不相交的生命周期 'x
和 'y
?
从另一个角度来说,我对r
的具体类型很困惑。从 Rustonomicon 的 subtyping and variance 章节中,我了解到生命周期 'a
是泛型类型 Wrapper<'a>
的一部分。当泛型变得特化时,它既不能是 Wrapper<'x>
也不能是 Wrapper<'y>
。那么r
的类型是什么?
也许higher-rank trait bound与它有关?
如果有人能解释 Rust 编译器内部如何解释这一点,我将不胜感激。
更新:
现有答案表明 r
的生命周期可以有多个起点和终点。如果这是真的,那么 'r
就是 'x
和 'y
的并集。但是,这种解释与 subtyping 规则不太吻合。例如,考虑下面的代码,'r2
和 'r
都不是另一个的子类型(或长于),所以我们不应该调用 bar(r, r2)
,但实际上我们可以。矛盾。
struct Wrapper<'a>(&'a i32);
fn bar<'a>(_p: Wrapper<'a>, _q: Wrapper<'a>) {}
fn foo() {
let mut r;
{
let z = 0;
let r2 = Wrapper(&z); // -> |
{ // |
let x = 0; // -> | // +--'r2
r = Wrapper(&x); // |--'x--+ // |
bar(r, r2); // | | // <- |
} // <- | |
} // |
{ // +--'r
let y = 0; // -> | |
r = Wrapper(&y); // |--'y--+
drop(r); // |
} // <- |
}
你对人生的看法是错误的。 r
的生命周期不会专门化为 'x
或 'y
。它有自己的生命周期(我们称之为 'r
)。
如 Non-Lexical Lifetimes RFC 中所述,值的生命周期(或 RFC 所称的作用域)是程序中该值将来可能仍会被使用的时间点。至关重要的是,生命周期不一定是线性的,也不必适合特定的语法范围(即 {}
块)。此外,生命周期不连接到变量(如 r
),而是连接到它们绑定的值。因此,如果您分配给一个变量(即 r = ..
),您实际上会杀死一个值并启动另一个值。它不算作使用。但是,分配给一个值的成员(即 r.0 = ..
), 是 一种用法,因为您正在更改现有值的一小部分。
在您的例子中,'r
有两个起点和两个终点。第一个起点在 r = Wrapper(&x);
,第一个终点在第一个 drop(r)
。第二个起点和终点在 y 块中。可视化:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x); // -> |
// |--'x--+
drop(r); // <- | |
} // |
{ // +--'r
let y = 0; // |
r = Wrapper(&y); // -> | |
// |--'y--+
drop(r); // <- |
}
}
这里我也插入了'x
和'y
供参考。请注意 'r
具体 而不是 存在于两个范围之间。
借用检查时,生命周期要求是生命周期需要比长寿。假设您有界限 'a: 'b
('a
必须比 'b
长)。为了使这个必然成立,任何 'b
是活的,'a
也必须是活的。这就是outlives的意思。另一种说法是:'a
lasts longer than 'b
。
然而,在这里我们还必须考虑位置感知的借用检查。这意味着 'a
only needs to outlive 'b
starting from the point their relation begins.
让我们看看我们的例子。对于像 r = Wrapper(&'x x)
这样的语句(我在其中添加了引用的生命周期),借用检查器必须确保引用 &'x x
的生命周期超过 r
的生命周期(即 'x: 'r
) 从 r = Wrapper(&x)
开始。查看上面的生命周期,我们可以看到任何地方 'x
都是活的,'r
也是活的。 'x
在范围结束时死亡,但幸运的是,'r
也是如此。所以这个检查出来了。冲洗并重复 'y
.
为了对此更有信心,让我们看一下示例的一些变体,看看我们如何预测编译器是接受还是拒绝它。
示例 1
我们将简单地删除 drop
调用以查看它是否有所作为:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x);
}
{
let y = 0;
r = Wrapper(&y);
}
}
这不会改变任何生命周期。请记住,生命周期与将来是否会使用值有关。由于我们没有添加 r
、x
或 y
的任何用途,因此生命周期没有变化。
正如预期的那样,it compiles just fine.
示例 2
如果 x
不在范围内怎么办:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
let x = 0;
r = Wrapper(&x);
{
let y = 0;
r = Wrapper(&y);
}
}
此处 'r
和 'x
已移入父范围,但除此之外它们都与原始示例非常相似。同样 'y
。因此所有的界限仍然成立,it compiles。 y
.
示例 3
所以让我们改变一些 r
的用法。如果我们不为 y
创建一个新的 Wrapper
,我们只是简单地覆盖引用:
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x);
}
{
let y = 0;
r.0 = &y;
}
}
这发生了实质性的变化 'r
,因为在第一个作用域之后,它将在第二个作用域中使用,以分配一个新的引用。因此,'r
现在从 r = Wrapper(&x)
跨越到 r.0 = &y
。然而,'x
不能匹配这个,因为 x
不会活得足够长(它超出范围)。因此,我们应该得到一个错误,说 x
的寿命不够长,which we do.
示例 4
如果我们只在两个作用域后使用 r
会怎么样:
#[derive(Debug)]
struct Wrapper<'a>(&'a i32);
fn foo() {
let mut r;
{
let x = 0;
r = Wrapper(&x);
}
{
let y = 0;
r = Wrapper(&y);
}
println!("{:?}", r)
}
此处,'r
现在比原始示例稍大,在 r = Wrapper(&y)
之后继续打印。但是,它仍然不存在于两个块之间,因为在第一个范围中分配的值从未在第二个范围中使用。因此,'x
比 'r
长。但是,'y
现在需要足够大才能打印出来(因为 'r
这样做了),但它不能,因为 y
会超出范围。因此,编译器应该抱怨 y
活得不够长,而不要说 x
。幸运的是,that is the case.
关于子类型化
问题的更新询问为什么上面的内容似乎与子类型化规则相矛盾。给定一个函数fn some_fn<'a>(a: &'a i32, b: &'a i32)
,为什么我们可以传递两个不同生命周期的引用给这个函数。在这两种情况下,函数声明似乎需要完全相同的生命周期。
答案是子类型有specific rules for lifetimes:
Whenever references are copied from one location to another, the Rust subtyping rules require that the lifetime of the source reference outlives the lifetime of the target location.
这意味着 rust 从不检查两个生命周期是否相等。它只检查它们是否以正确的方式比彼此长寿。在更新的示例中,'a
是对 bar
的调用的生命周期。将 r
和 r2
传递给它时,我们得到边界 'r: 'a
和 'r2: 'a
。这使得它变得非常容易,因为 'a
仅在调用期间处于活动状态,其他生命周期也处于活动状态。调用后,所有三个生命周期都已死亡,因此绑定平凡成立。
我希望我的想法不会与编译器内部实际发生的事情相差太多。
根据 rustc_middle (MIR) 的直接答案:
let mut r;
的类型推断为Wrapper<'free_region>
,其中'free_region
没有附加边界。- 粗略地说,每个表达式都有自己的区域实例,其边界根据它们所在的范围而定。在每个地方,MIR 都会进行关于是否满足特定区域边界的局部推理。对于这种情况:
r = Wrapper(&x);
它检查r'
类型中的'free_region
是否可以被'x
替代,并且因为前者没有边界(因为 [1])它成功了。请注意,它在本地进行替换,因此其他作用域看不到它,它们会看到它们的本地替换。
让我们将 'free_region
绑定到外部范围:
struct Wrapper<'a>(&'a i32);
fn foo<'a>() {
let mut r: Wrapper<'a>;
{
let x = 0;
r = Wrapper(&x); //~ ERROR `x` does not live long enough
drop(r);
}
}
有人可能会想 但是 r
被排除在 'x
的范围内! 原因再次是 局部推理.
Update:值得注意的是,r
只是一个绑定,而不是OOP理解中的对象。就是这样,一个名称的类型,您在其中两次分配了一个右值。 (rvalue表示表达式值)