Reference 为什么Rust中需要显式寿命?
我在读《铁锈书》的第二部分,在一个命名/显式的生命周期中,我遇到了这个例子:Reference 为什么Rust中需要显式寿命?,reference,rust,static-analysis,lifetime,Reference,Rust,Static Analysis,Lifetime,我在读《铁锈书》的第二部分,在一个命名/显式的生命周期中,我遇到了这个例子: struct Foo<'a> { x: &'a i32, } fn main() { let x; // -+ x goes into scope // | { // | let y = &5;
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ x goes into scope
// |
{ // |
let y = &5; // ---+ y goes into scope
let f = Foo { x: y }; // ---+ f goes into scope
x = &f.x; // | | error here
} // ---+ f and y go out of scope
// |
println!("{}", x); // |
} // -+ x goes out of scope
structfoo请注意,除了结构定义之外,该代码段中没有显式的生命周期。编译器完全能够推断main()
中的生存期
然而,在类型定义中,显式生存期是不可避免的。例如,此处存在歧义:
struct RefPair(&u32, &u32);
这些生命周期是不同的还是相同的?从使用角度来看,它确实很重要,struct-RefPair(&'a-u32,&'b-u32)
与struct-RefPair有很大的不同,让我们看看下面的示例
fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
x
}
fn main() {
let x = 12;
let z: &u32 = {
let y = 42;
foo(&x, &y)
};
}
书中的箱子设计得很简单。生命周期这个话题被认为是复杂的
编译器无法轻松推断具有多个参数的函数的生存期
另外,我自己的板条箱有一个OptionBool
类型,带有as_slice
方法,其签名实际上是:
fn as_slice(&self) -> &'static [bool] { ... }
编译器根本不可能找到答案。其他答案都有显著的要点(),但缺少一个关键点:为什么编译器会告诉您错误时需要显式的生存期
这实际上与“当编译器可以推断出显式类型时,为什么需要显式类型”相同。一个假设的例子:
fn foo() -> _ {
""
}
当然,编译器可以看到我返回了一个&'static str
,那么程序员为什么要键入它呢
主要原因是,虽然编译器可以看到您的代码做了什么,但它不知道您的意图是什么
函数是防止代码更改影响的自然边界。如果我们允许从代码中完全检查生命周期,那么一个看似无辜的更改可能会影响生命周期,这可能会在遥远的函数中导致错误。这不是一个假设的例子。据我所知,Haskell在顶级函数依赖类型推断时有这个问题。锈把那个问题扼杀在萌芽状态
编译器还有一个效率优势——只需解析函数签名,即可验证类型和生命周期。更重要的是,它对程序员有效率上的好处。如果我们没有明确的生命周期,这个函数做什么:
fn foo(a: &u8, b: &u8) -> &u8
如果不检查源代码就无法判断,这与大量编码最佳实践背道而驰
通过推断引用非法分配到更大范围
作用域本质上是生命周期。更清楚一点,lifetime'A
是一个通用的lifetime参数,可以在编译时根据调用站点使用特定的作用域进行专门化
是否确实需要显式生命周期来防止[…]错误
一点也不。需要使用生命周期来防止错误,但需要显式的生命周期来保护几乎没有理智的程序员所拥有的东西。以下结构中的生命周期注释:
struct Foo<'a> {
x: &'a i32,
}
现在,f
确实比f.x
所指的变量寿命长,我在这里找到了另一个很好的解释:
一般来说,只有在引用正确的情况下才可能返回引用
从过程的参数派生。在这种情况下,指针
结果将始终与其中一个参数具有相同的寿命;
命名的生存期指示所使用的参数
如果函数接收两个引用作为参数并返回一个引用,那么函数的实现有时可能返回第一个引用,有时返回第二个引用。无法预测给定调用将返回哪个引用。在这种情况下,无法推断返回引用的生存期,因为每个参数引用可能引用具有不同生存期的不同变量绑定。明确的生命周期有助于避免或澄清这种情况
同样,如果一个结构包含两个引用(作为两个成员字段),那么该结构的成员函数有时可能返回第一个引用,有时返回第二个引用。再次明确的生命周期防止了这种歧义
在一些简单的情况下,编译器可以推断生存期。示例不起作用的原因很简单,因为Rust只具有局部生存期和类型推断。你的建议需要全局推断。每当你有一个生命周期无法省略的引用时,就必须对它进行注释。作为一个生锈的新手,我的理解是,明确的生命周期有两个目的
在函数上放置显式生存期注释会限制该函数中可能出现的代码类型。显式生命周期允许编译器确保您的程序按预期执行
如果您(编译器)想要检查一段代码是否有效,那么您(编译器)不必迭代地查看每个调用的函数。只要看一看由该段代码直接调用的函数的注释就足够了。这使您的程序(编译器)更易于推理,并使编译时间可管理
第1点,考虑Python中编写的以下程序:
将熊猫作为pd导入
将numpy作为np导入
def第二排(ar):
返回ar[0]
def工作(秒):
df=pd.DataFrame(数据=秒)
df.loc[0,0]=1
def main():
# .. 加载数据。。
ar=np.array([[0,0],[0,0]])
# .. 在第二排做些工作。。
第二行=第二行(ar)
工作(第二)
# .. 很久以后。。
打印(报告(ar))
如果名称=“\uuuuu main\uuuuuuuu”:
main()
哪个会打印
数组([[1,0],
[0, 0]])
这种行为总是让我感到惊讶。发生的情况是,df
与ar
共享内存,因此当df
的某些内容发生变化时
fn main() {
let f : Foo;
{
let n = 5; // variable that is invalid outside this block
let y = &n;
f = Foo { x: y };
};
println!("{}", f.x);
}
#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);
impl<'a, 'b> Array<'a, 'b> {
fn second_row(&mut self) -> &mut &'b mut [i32] {
&mut self.0
}
}
fn work(second: &mut [i32]) {
second[0] = 1;
}
fn main() {
// .. load data ..
let ar1 = &mut [0, 0][..];
let ar2 = &mut [0, 0][..];
let mut ar = Array(ar1, ar2);
// .. do some work on second row ..
{
let second = ar.second_row();
work(second);
}
// .. much later ..
println!("{:?}", ar);
}