• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

【译】深入理解Rust中的生命周期

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

原文标题:Understanding Rust Lifetimes
原文链接:https://medium.com/nearprotocol/understanding-rust-lifetimes-e813bcd405fa
公众号: Rust 碎碎念
翻译 by: Praying


从 C++来到 Rust 并需要学习生命周期,非常类似于从 Java 来到 C++并需要学习指针。起初,它看起来是一个不必要的概念,是编译器应该处理好的东西。后来,当你意识到它赋予你更多的力量——在 Rust 中,它带来了更多的安全性和更好的优化--你感到兴奋,想掌握它,但却失败了,因为它并不直观,很难找到形式化的规则。可以说,C++指针比 Rust 生命周期更容易沉浸其中,因为 C++指针在代码中随处可见,而 Rust 生命周期通常隐藏在大量的语法糖背后。所以你最终会在语法糖不适用的时候接触生命周期,这通常是一些复杂的情况。当你面临的只有这些复杂情况时,你很难内化这个概念。

引言

对于生命周期,需要记住的第一件事就是,它们全都是关于引用(references)的,与其他东西无关。例如,当我们看到一个带有生命周期(lifetime)类型参数的结构体时,它指的是这个结构体所拥有的引用的生命周期,再无其他。不存在结构体的生命周期或者闭包的生命周期,只有结构体或闭包内部引用的生命周期。因此,我们对生命周期的讨论会不可避免地涉及到 Rust 引用。

生命周期背后的动机

要理解生命周期,我们首先需要理解其背后的动机,这就要求我们先理解借用规则背后的动机。借用规则中指出:

在代码中,存在对重叠内存的引用,也称为别名(aliasing),它们中至少有一个会变更(mutate)内存中的内容。

同时变更是不允许的,因为这样是不安全的,并且它阻碍编译器进行各种优化。

示例

假定我们现在想要写一个函数,该函数将一个坐标沿着 x 轴在给定方向上移动两倍的距离。

struct Coords {
  pub x: i64,
  pub y: i64,
}
fn shift_x_twice(coords: &mut Coords, delta: &i64) {
  coords.x += *delta;
  coords.x += *delta;
}

fn main() {
  let mut a = Coords{x: 10, y: 10};
  let delta_a = 10;
  shift_x_twice(&mut a, &delta_a);  // All good.

  let mut b = Coords{x: 10, y: 10};
  let delta_b = &b.x;
  // shift_x_twice(&mut b, delta_b);  // Compilation failure.
}

最后一条语句会把坐标移动三倍距离而不是两倍,这可能会在生产系统中引发各种 bug。关键问题在于,delta_b&mut b指向一块重叠的内存,而这在 Rust 中是被生命周期和借用规则所阻止的。尤其是,Rust 编译器会提醒,delta_b要求持有一个b的不可变引用直到main()结束,但是在那个作用域内,我们还试图创建一个b的可变引用,这是被禁止的。

为了能够进行借用规则检查,编译器需要知道所有引用的生命周期。在很多情况下,编译器能够自己推导出生命周期,但是有些情况它无法完成,这就需要开发者手动的对生命周期进行标注。此外,编译器还给开发者提供了工具,例如,我们可以要求所有实现了某个特定 trait 的结构体,其所有引用至少在给定的时间段内都是有效的。

对比 Rust 的引用和 C++中的引用,在 C++中,我们也可以有常量(const)和非常量(non-const)引用,类似于 Rust 中的&x&mut x。但是,C++中没有生命周期。常量引用(const reference)能够帮助 C++编译器进行优化,但是它们不能给出完整的安全性保证。所以,上面的示例如果用 C++来写是可以编译通过的。

脱糖(Desugaring)

在我们深入理解生命周期之前,我们需要弄清生命周期是什么,因为各种 Rust 文档用生命周期这个词既指代作用域(scope)也指代类型参数(type-parameter)。在这里,我们用生命周期(lifetime ) 表示一个作用域,用生命周期参数(lifetime-parameter ) 来表示一个参数,编译器会用一个真正的生命周期来替换这个参数,就像它在推导泛型时那样。

示例

为了让解释更加清晰,我们将会对一些 Rust 代码进行脱糖(译注:指脱去语法糖)。考虑下面的代码:

fn announce(value: &impl Display) {
  println!("Behold! {}!", value);
}

fn main() {
 let num = 42;
 let num_ref = #
 announce(num_ref);
}

下面是脱糖的版本:

fn announce<'a, T>(value: &'a T) where T: Display {
    println!("Behold! {}!", value);
}
fn main() {
'x: {
    let num = 42;
    'y: {
        let num_ref = &'y num;
        'z: {
            announce(num_ref);
        }
    }
}
}

后面脱糖的代码使用生命周期参数'a和生命周期/作用域'x'y进行了显式的标注。

我们还使用impl Display来比较生命周期参数和一般的类型参数。注意这里语法糖是如何把生命周期参数'a和类型参数T都隐藏起来的。注意,作用域并不是 Rust 语法的一部分,我们只是用它来标注,所以脱糖后的代码是无法编译的。而且,在这个以及后面的示例中,我们忽略了在 Rust 2018 中加入的非词法生命周期(non-lexical lifetimes)以简化我们的解释。

子类型

从技术角度看,生命周期不是一个类型,因为我们无法像u64或者Vec<T>这样的普通的类型一样构建一个生命周期的实例。然而,当我们对函数或结构进行参数化时,生命周期参数就像类型参数一样被使用,请看上面的announce示例。另外,我们后面会看到的变型规则(Variance Rule)也会像使用类型一样使用生命周期,所以我们在本文中也会称之为类型。

比较生命周期和普通类型、生命周期参数和普通类型参数是有用的:

  • 当编译器为一个普通类型参数推导类型时,如果有多个类型可以满足类型参数,编译器就会报错。而在生命周期的情况下,如果有多个生命周期可以满足给定的生命周期参数,编译器将会使用最小的那个生命周期。


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
Matlab watershed函数学习(1)发布时间:2022-07-18
下一篇:
matlab绘图的坐标轴数字、范围、间隔控制 .发布时间:2022-07-18
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap