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

2021-09-13 陈天
《陈天 · Rust 编程第一课》
课程介绍


讲述:陈天

时长:大小12.94M

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

值的生命周期

在进一步讨论之前,我们先给值可能的生命周期下个定义。
如果一个值的生命周期贯穿整个进程的生命周期,那么我们就称这种生命周期为静态生命周期<...

展开全文
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。

精选留言

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

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

    
    15
  • dotfiles
    2021-10-12
    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. 对于生命周期检查的一点思考 在含有引用的函数调用中,编译器会尝试根据规则进行补齐,如果无法自动补齐,就会要求开发者进行标注. 开发者标注的生命周期会在两个地方生效,一是函数的实现中,会去校验标注的正确性, 另一个是在函数的调用点也会根据函数声明中的标注,对入参和返回值进行检查. 那么函数声明中的生命周期标注,其实就是同时约束实现方和调用方的约定.在标注的约束关系中,如果检查发现调用方和实现方都满足约束,则编译通过.
    展开

    作者回复: 👍

    共 2 条评论
    11
  • Marvichov
    2021-09-13
    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

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

    作者回复: 👍

    
    6
  • Arthur
    2021-09-15
    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。

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

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

    共 3 条评论
    2
  • CR
    2021-09-13
    想成为今天第一个打卡的男孩 LOL🤣

    作者回复: 👍

    
    2
  • 朽木
    2022-01-28
    struct 的字段为什么需要标注生命周期呢? 函数需要标注是因为输出和输入的引用关系不确定,需要人为标注,可 struct 和它字段的依赖关系是确定的,struct 当然依赖每个字段,字段的声明周期不得小于 struct 本身,这不需要标注也知道吧?为何还需要标注呢?
    
    1
  • 人世间
    2021-12-17
    对于 &mut &'a str, 我理解类似下面的过程: ``` fn main() { let s = "hello world".to_owned(); let mut s1 = s.as_str(); let hello: &str; { let ss = &mut s1; hello = &ss[..]; // hello 的 lifetime 与 s1 一致, s1 的可变借用 ss 只存在 花括号的作用域内 } println!("s1={:?}", s1); println!("hello={:?}", hello); } ``` 对于 &'a mut &str : ``` fn main() { let s = "hello world".to_owned(); let mut s1 = s.as_str(); let hello: &mut &str; { let ss = &mut s1; hello = ss; // s1 可变引用 ss 绑定到 hello 中了 } println!("s1={:?}", s1); // s1 不可变借用 println!("hello={:?}", hello); // 活跃的 s1 的可变借用 // println!("s1={:?}", s1); // 调换 打印 s1,不报错 ,s1 的活跃的可变借用在 s1 的不可变借用之间。 } ```
    展开

    作者回复: 👍

    
    1
  • Ignis
    2021-11-13
    按照本节的解释,编译器会给每一个引用参数都生成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有可变引用。 不知道这么理解对不对
    展开

    作者回复: 对

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

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

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

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

    
    2
  • Roy Liang
    2021-09-26
    从前面文章学会match代替if会更简洁,谢谢老师 ```rust fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str { match s1 > s2 { true => s1, false => s2, } } ```
    展开

    作者回复: 👍

    共 2 条评论
    1
  • 老裴
    2021-09-19
    用其他语言写函数也会遇到生命周期的事,比如某某引用已经失效还是未初始化什么,标出来就清楚了,但是有没这种情况,某个函数的参数生命周期是变化的,可能是<'a,'a>也可能是<'a,'b>,这样的话函数要实现2遍吗

    作者回复: 'a 代表的是一个泛化的生命周期,表示我支持任何长度的生命周期。比如: fn max<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str {} 表明 'a 和 'b 可能是不同的生命周期,'b 生命周期大于等于 'a。返回值需要满足 'a 的生命周期。整个函数相当于在说,给定两个参数,返回值的生命周期要大于这两个参数中小的那个。 所以函数参数的生命周期是一种约束,而不是一个具体的值。它本身就可以随着传入的生命周期而自适应,只要它们满足约束就可以。你可以好好理解一下下面的代码: ```rust fn max<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str { s1 } fn main() { let s1 = String::from("hello"); let s2 = String::from("world"); // 静态 let s3 = "hello"; let s4 = "world"; max(&s1, &s2); max(&s1, s3); max(s3, s4); max(&s1, s3); // 这里 s3 是 'static,s1 是 'b,s3 传入后会降级成 'a,它依旧满足 'b: 'a 的约束 max(s3, &s1); } ``` playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=0080b95b36db1674b5d4d4c29793b1ab

    
    1
  • 核桃
    2021-09-17
    老师你好,这里有一些小疑惑没有搞懂。 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 是指向这个字符串切片的一个可变的引用。所以我们的问题是:返回值的生命周期究竟应该是和字符串切片有关?还是跟对这个字符串切片的可变引用有关?

    
    1
  • 芥末小龙
    2021-09-13
    陈老师您好! fn max<'a,'b:'a>(s1:&'a str, s2:&'b str) ->&'a str { if s1 > s2 { s1 } else { s2 } } 我记得汉东老师的课中讲过,max这样也能编译过去 但是没有明白啥意思。您能详细的说一下吗?
    展开

    作者回复: 当 Rust 编译器不能标注生命周期需要开发者进行标注时,一定是编译器无法从已知的逻辑中找到参数和返回值之间的关系。所以我们要说明这种关系。 我们可以说 s1, s2 生命周期一致,返回值和他们的一致。这是我文中代码的表述。 也可以说 s1, s2 生命周期不一致,我们返回值的生命周期是比较小的那个。这是这段代码的表述。

    共 7 条评论
    2
  • NorthWind
    2022-06-16
    let mut map = HashMap::new(); map.insert("hello", "world"); let key = "hello1"; 这里的 "hello" 和 "world" 是静态生命周期么?
    
    
  • 太南喬 Serendipity...
    2022-05-12
    請教下為啥 delimiter 不需要標註生命週期? 舉例來說我把 strtok 的簽名改成 pub fn strtok<'a, 'c>(s: &mut &'a str, delimiter: 'c char) 然後編譯器就不高興了
    共 1 条评论
    
  • 绝世珍奇
    2022-04-23
    老师,请教一个问题: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6027ba5bae514e563ff257100bedfd23 在不修改Scheme, Filter 等实现的情况下,Policy 需要怎样改呢。 对rust的生命周期了解不够深,试了多种方式都不行
    展开
    
    
  • GE
    2022-04-18
    1. 将&mut标记为'a之后 只要返回值hello没有出作用域 &mut就会存在 因为会有“同时”存在mutable ref & immutable ref的情况 如果只是为了绕开检查 可以稍加修改 ``` fn main() { let s = "hello world".to_owned(); let mut s1 = s.as_str(); { let hello = strtok(&mut s1, ' '); println!("hello is: {}", hello); } println!("s1: {}, s: {}", s1, s); } ```
    展开
    
    