陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
23195 人已学习
新⼈⾸单¥68
登录后,你可以任选4讲全文学习
课程目录
已完结/共 65 讲
基础篇 (21讲)
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

10|生命周期:你创建的值究竟能活多久?

'a'b等表示
在某个作用域中定义
'static表示
贯穿整个进程的生命周期
'a'b生命周期标注问题
内部有引用时需要标注
需要加引用标注
无法自动添加时,需要手工标注
通过一些简单的规则
生命周期参数描述参数和返回值之间的关系
静态的
生命周期和进程的生命周期一致
生命周期是动态的
有各自的作用域
动态生命周期
静态生命周期
Rust的生命周期管理进化
Rust的I/O安全性
栈上的内存不必特意释放
对代码中的生命周期标注的理解
strtok()函数签名问题
数据结构的生命周期标注
实现字符串分割函数strtok()
编译器自动添加生命周期的标注
需要生命周期标注
通过函数签名确定参数和返回值的生命周期约束
识别值和引用的生命周期
全局变量、静态变量、字符串字面量等
堆和栈上的内存
值的生命周期
参考资料
思考题
引用标注小练习
编译器如何识别生命周期
静态生命周期和动态生命周期
Rust生命周期管理

该思维导图由 AI 生成,仅供参考

你好,我是陈天。
之前提到过,在任何语言里,栈上的值都有自己的生命周期,它和帧的生命周期一致,而 Rust,进一步明确这个概念,并且为堆上的内存也引入了生命周期。
我们知道,在其它语言中,堆内存的生命周期是不确定的,或者是未定义的。因此,要么开发者手工维护,要么语言在运行时做额外的检查。而在 Rust 中,除非显式地做 Box::leak() / Box::into_raw() / ManualDrop 等动作,一般来说,堆内存的生命周期,会默认和其栈内存的生命周期绑定在一起
所以在这种默认情况下,在每个函数的作用域中,编译器就可以对比值和其引用的生命周期,来确保“引用的生命周期不超出值的生命周期”。
那你有没有想过,Rust 编译器是如何做到这一点的呢?

值的生命周期

在进一步讨论之前,我们先给值可能的生命周期下个定义。
如果一个值的生命周期贯穿整个进程的生命周期,那么我们就称这种生命周期为静态生命周期
当值拥有静态生命周期,其引用也具有静态生命周期。我们在表述这种引用的时候,可以用 'static 来表示。比如: &'static str 代表这是一个具有静态生命周期的字符串引用。
一般来说,全局变量、静态变量、字符串字面量(string literal)等,都拥有静态生命周期。我们上文中提到的堆内存,如果使用了 Box::leak 后,也具有静态生命周期。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Rust语言中的生命周期概念对于值和引用的管理至关重要。文章首先介绍了值的生命周期,包括静态生命周期和动态生命周期的定义和区分。接着讨论了编译器如何识别生命周期,通过示例代码展示了编译器在处理引用生命周期时的困惑和需要生命周期标注的情况。文章还解释了为什么在一些函数中使用引用时编译器没有提示额外标注生命周期的原因,以及编译器自动添加标注的规则。最后,强调了理解代码逻辑对于正确标注参数和返回值的约束关系的重要性。整体来说,文章深入浅出地介绍了Rust语言中的生命周期概念及其在编译器处理引用时的重要性。文章还通过一个字符串分割函数的实现示例,帮助读者更好地理解生命周期标注的重要性和使用方法。文章内容深入浅出,适合Rust语言初学者快速了解生命周期概念及其在编译器处理引用时的重要性。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《陈天 · Rust 编程第一课》
新⼈⾸单¥68
立即购买
登录 后留言

全部留言(35)

  • 最新
  • 精选
  • 记事本
    置顶
    &str 是静态区域内存的一个指针 String 是把字符分配在堆上,但是也可以通过as_str以指针形式返回,就是&str类型了,数据类型相同,分配的区域不同,生命周期不同,这样说法对吗?

    作者回复: &str 不是静态区域内存的指针。&str 是一个字符串切片,一个带有长度的胖指针,指向字符串实际的位置。它可以指向 "hello world",此时指针指到了 RODATA/STRING section 里 "hello" 的地址,它的生命周期是 'static;也可以指向 "hello world".to_string(),此时指针指向了这个字符串的堆地址,生命周期是 'a。

    2021-09-14
    23
  • dotfiles
    1. &str是一个在栈上分配的胖指针,而且实现了copy trait ``` let a = "hello"; let b = a; println!("a: {}, b: {}", a, b); println!("&a: {:p}, &b: {:p}", &a, &b); ``` 输出如下: ``` a: hello, b: hello &a: 0x7ffe7f4f3788, &b: 0x7ffe7f4f3798 ``` 首先,a赋给b之后,a依然可以读取.因此没有转移所有权,进一步地,a其实是str的一个不可变引用.str的不可变引用允许有多份.这些不可变引用需要满足其生命周期小于str即可. 然后,&a和&b之间在64位系统上,相差16个字节.刚好是一个ptr和一个size的大小. 2. 思考题一 直接运行代码,在编译器的提示下,就会添加标注如下: ``` pub fn strtok<'a>(s: &'a mut &str, delimiter: char) -> &'a str {...} ``` 再运行,就会提示s1,同时存在可变和不可变引用. 一开始没想明白,原来是s1的可变引用的周期和返回值绑定了.在hello使用结束前,编译器认为s1的可变引用一直存在. 那么根据这个理解,其实这样标注也可以.只要把打印拆开就行了. ``` fn main() { ... println!("hello is: {}, s: {}", hello, s); println!("s1: {}", s1); } ``` 运行一下,果然可以通过. 3. 对于生命周期检查的一点思考 在含有引用的函数调用中,编译器会尝试根据规则进行补齐,如果无法自动补齐,就会要求开发者进行标注. 开发者标注的生命周期会在两个地方生效,一是函数的实现中,会去校验标注的正确性, 另一个是在函数的调用点也会根据函数声明中的标注,对入参和返回值进行检查. 那么函数声明中的生命周期标注,其实就是同时约束实现方和调用方的约定.在标注的约束关系中,如果检查发现调用方和实现方都满足约束,则编译通过.

    作者回复: 👍

    2021-10-12
    3
    24
  • Marvichov
    1. 入参 s: &'a mut &str 变成了 &'a mut &'a str; 因为outer ref must outlive inter ref; 相当于把mutable borrow的lifetime extend成了&str的lifetime; 很奇怪, 就算如此, 入参`s` 在函数call结束之后就end its life; 为啥还能extend it life到函数invocation后呢? 希望老师解答一下 ``` 17 | let hello = strtok(&mut s1, ' '); | ------- mutable borrow occurs here 18 | println!("hello is: {}, s1: {}, s: {}", hello, s1, s); | -----------------------------------------------^^----- | | | | | immutable borrow occurs here ``` https://doc.rust-lang.org/nomicon/lifetime-elision.html; ``` pub fn strtok<'a>(s: &'a mut &str, delimiter: char) -> &'a str { ``` 本以为output param会被elide; 看来有显示标注, 编译器不会自动elide 2. 在实践中慢慢有所领悟: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotations-in-function-signatures > One lifetime annotation by itself doesn’t have much meaning, because the annotations are meant to tell Rust how generic lifetime parameters of multiple references relate to each other. 3. 死灵书比the book讲得好: https://doc.rust-lang.org/nomicon/lifetimes.html; 从编译器的角度去minimize the scope of a borrow能更好理解lifetime 4. 以前看到有人评论, 说lifetime翻译成生命**周**期很误导大家: 死了就死了, 不会周而复始地复活; 更好的翻译是生命期. 我表示赞同; 不知老师如何看? 5. lifetime是泛型的一部分; 对不同的'a, 是不是strtok<'a>都要生成不同的单体化呢? 编译生成后的代码, 还有'a的信息吗?

    作者回复: 1. 好问题。还是要从生命周期中参数和返回值的关系来看,因为现在我们把对字符串的可变引用的生命周期和返回值的生命周期关联起来了,hello 得到了 strtok 的返回值,所以从编译器看来,&mut s1 的生命周期并没有结束,所以发生了 &mut 和 & 同时存在的局面。 2. 👍 3. 赞 4. 我觉得这是个翻译风格问题,从字面意思上看生命期更好,但大家最惯用的说法已经约定俗称,而且不光 Rust,其它语言在谈到 lifetime 时也是如此(比如 react),所以生命周期更好。它起码比福尔摩斯的译名要好吧 lol。 5. 不会。生命周期在单体化之前就会被处理掉。它更多以编译器的错误呈现出来。对编译器来说,只要满足约束,更长的生命周期的变量也可以通过编译(比如 &'static str 至于 &'a str)。如果生命周期做单体化,下面的代码就可以编译,然而不行: ```rust trait Print { fn print(self); } // 生命周期无法像泛型那样有多个实现 // impl<'a> Print for &'a str { // fn print(self) { // println!("Arbitrary str: {}", self); // } // } impl Print for &'static str { fn print(self) { println!("'static str: {}", self); } } // 也无法单体化 fn print_str<'a>(s: &'a str) { s.print() } fn main() { let s = "hello, world!"; s.print(); print_str(s); } ``` playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b767ef1a9d9e265252c1ad29d31176b0

    2021-09-13
    5
    15
  • 亚伦碎语
    会发生所有权的冲突,不满足一个值可以有唯一一个活跃的可变引用。可变引用(写)和只读引用(读)是互斥的关系。 原因是因为让 s1的mut引用和只读引用有一样的scope,就会违反上述的规则,&mut &'a str则是s1的可变引用的scope只在strtok内。所以不违法上边的规则

    作者回复: 👍

    2021-09-13
    12
  • Ignis
    按照本节的解释,编译器会给每一个引用参数都生成lifetime标记,那么strtok这个例子里面,如果我们标记了一部分,编译器是不是会自动把省略的标记补上,也就是这样: pub fn strtok<'a>(s: &mut &'a str, delimiter: char) -> &'a str ==> pub fn strtok<'a, 'b>(s: &'b mut &'a str, delimiter: char) -> &'a str 按照这个思路,如果我们把标记改为: pub fn strtok<'a>(s: &'a mut &str, delimiter: char) -> &'a str 编译器会处理成: pub fn strtok<'a, 'b>(s: &'a mut &'b str, delimiter: char) -> &'a str 这种情况下s1的生命周期被s参数传递到返回值到hello,所以编译器会提示s1有可变引用。 不知道这么理解对不对

    作者回复: 对

    2021-11-13
    6
  • Geek_1b11b3
    陈老师你好: ”&mut &str 添加生命周期后变成 &'b mut &'a str“,为什么编译器会自动标注成这样?不是一个参数一个生命周期吗?

    作者回复: 一个引用一个生命周期

    2021-10-02
    6
  • 核桃
    老师你好,这里有一些小疑惑没有搞懂。 1.&mut &str 添加生命周期后变成 &'b mut &'a str,这将导致返回的 '&str 无法选择一个合适的生命周期。 这句话里面,为什么mut前面还会需要标注生命周期的,这不是变量吧,因为在我故个人理解中,这是一个关键词和符号,为什么会需要生命周期标记? 2. pub fn strtok<'b, 'a>(s: &'b mut &'a str, delimiter: char) -> &'a str {...} 这个里面,首先<>中的表示什么意思? 然后这里最后的 -> &'a str表示返回值的生命周期是'a,和入参的str相同吗? 噢,刚刚手快弄错了一条留言,没有写完,请忽略,多谢了

    作者回复: 对于 1,你可以再看看我文中画的图,好好感受一下 &mut &str 表示什么。&str 是一个字符串切片,&mut &str 是指向这个字符串切片的一个可变的引用。所以我们的问题是:返回值的生命周期究竟应该是和字符串切片有关?还是跟对这个字符串切片的可变引用有关?

    2021-09-17
    6
  • Arthur
    1. - 生命周期标注为`pub fn strtok<'a>(s: &'a mut &str, delimiter: char) -> &'a str ` - 错误信息为 ``` error[E0502]: cannot borrow `s1` as immutable because it is also borrowed as mutable --> src/main.rs:18:52 | 17 | let hello = strtok(&mut s1, ' '); | ------- mutable borrow occurs here 18 | println!("hello is: {}, s1: {}, s: {}", hello, s1, s); | -----------------------------------------------^^----- | | | | | immutable borrow occurs here | mutable borrow later used here error: aborting due to previous error ``` - 原因是,在这样手工标注生命周期以后,可变引用s1的声明周期就和返回值hello一样了,都存活于main函数作用域中。但是同时s1在main函数的作用域中,在18行又作为只读引用在println!中使用。违背规则:在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在。因此编译器会报错。 2. 在06讲中, ```rust struct UrlFetcher<'a>(pub(crate) &'a str); ``` 这个struct只有一个成员,为什么也需要手动标注生命周期呢?编译器不能像为只有一个引用参数的函数那样自动生成生命周期的标记吗?

    作者回复: 1. 正确! 2. 好问题,理论上可以,但编译器目前没有做这样的事情,主要是代码可读性的担忧。可以看:https://github.com/rust-lang/rfcs/blob/master/text/0141-lifetime-elision.md#lifetime-elision-in-structs。

    2021-09-15
    5
  • 周烨
    1. 错误是 cannot borrow `s1` as immutable because it is also borrowed as mutable。感觉应该是这样写,就变成了可变借用&mut的生命周期和返回值hello的生命周期一样了,所以这个引用的生命周期在函数结束后仍没有结束,所以产生了作用域冲突。我之前加生命周期的时候第一直觉也是加在这个地方,感觉真的很容易出错。

    作者回复: 对! 的确,在不是特别理解 &mut &str 在内存中的意义时容易出错,我们需要知道它是一个指向字符串引用的可变指针。这样,返回值和谁产生约束就一目了然了。

    2021-09-13
    3
    3
  • pk
    strtok 可以使用 &mut str 做第一个参数而不是 &mut &str 吗?fn strtok(s: &mut str, delimiter: char) -> &str

    作者回复: 这样并不理想。因为 strtok 并不想改变原有的字符串,只是改变指向这个字符串的可变引用。

    2021-10-03
    2
收起评论
显示
设置
留言
35
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部