Rust 生命周期的学习过程中,感觉总是道理讲得很多,但例子很少

2022-05-02 07:42:55 +08:00
 Richard14

网上有很多教学,包括 rust course 里对引用生命周期的各种自动添加规则讲了很多,但是看起来总让人感觉云里雾里,感觉总是告诉你说某某情况会编译报错,但只知道会错,教学也并未提供一种推荐的解决方案,导致最终解决问题的时候要不然是无脑注释'a 让编译器过了,要不然就是到处 copy(),重复着低水平代码。

像以下描述一个最简场景

use std::collections::HashMap;

fn index_t_words(sentence: &String) -> HashMap<&str, usize> {
    let mut result: HashMap<&str, usize> = HashMap::new();
    for (idx, word) in sentence.split(' ').enumerate() {
        if word.chars().nth(0).unwrap() == 't' {
            result.insert(word, idx);
        }
    }; result
}

fn main() {
    let sentence: String = String::from("magic happens in the test statement");
    println!("{:?}", index_t_words(&sentence))
}

它实现一个非常简单的需求,输入字符串"magic happens in the test statement",查找其中以 t 开头的单词的位置(理论返回结果 {"the": 3, "test": 4}),上述代码编译器能通过,倒是没什么难理解的地方。但是如果再加入一个引用作为函数的参数,不参与任何逻辑执行,仅改变函数签名,此时按照经验编译器就会报错了,因为无法应用自动生命周期标注的规则,提示你需要人工解决。

实际生产中总是不可避免地出现这种情况,我们不可能总是保证函数或方法只有单一输入。

use std::collections::HashMap;

fn index_t_words(sentence: &String, bias: &str) -> HashMap<&str, usize> {
    let mut result: HashMap<&str, usize> = HashMap::new();
    for (idx, word) in sentence.split(' ').enumerate() {
        if word.chars().nth(0).unwrap() == 't' {
            result.insert(word, idx);
        }
    }; result
}

fn main() {
    let sentence: String = String::from("magic happens in the test statement");
    let bias = "spoiler";
    println!("{:?}", index_t_words(&sentence, &bias))
}

此时如果给函数签名无脑全标'a 就又能编译通过,

fn index_t_words<'a>(sentence: &'a String, bias: &'a str) -> HashMap<&'a str, usize> {}

说实话看完 rust course 也搞不太清楚发生了什么,理论上标注和资源释放不发生任何联系,为什么这么手动标了一下编译器就过了?

以至于再进阶一种情况,如果需要返回的变量所有权在函数内部,比如下面这种情况,在生产中也不少见,有时不可避免地资源的所有权在被调用的函数内部产生。因为函数结束时要释放资源,引用当然是无法带到函数外面的,会报错可以理解,一般来说解决方案也是通过 copy 把所有权转移出去。但是如果我们需要面临输出到多个位置(比如函数签名不光输出一个 HashMap ,还有另一个 Vec ,也同样引用&str ),如果不想每输出到一个结构就新拷贝一份的话,应该采用什么样的解决方案呢?

fn index_t_words() -> HashMap<&str, usize> {
    let sentence: String = String::from("magic happens in the test statement");
    let mut result: HashMap<&str, usize> = HashMap::new();
    for (idx, word) in sentence.split(' ').enumerate() {
        if word.chars().nth(0).unwrap() == 't' {
            result.insert(word, idx);
        }
    }; result
}

fn main() { println!("{:?}", index_t_words()) }

最后吐槽一下都说 rust 学习曲线比较陡峭,刚开始特别难,后面慢慢变简单。我个人体验上倒是感觉刚开始特别简单,越看感觉越难。。

2583 次点击
所在节点    Rust
12 条回复
GTim
2022-05-02 09:56:50 +08:00
Rust 的生命周期,只要牢记以下几点就可以了:

0. 重要的事情说三遍:生命周期标注只是帮助编译器检查,而不影响实际的程序运行。
1. 所有引用类型的参数都有独立的生命周期 'a 、'b 等。比如方法 /函数的参数,比如结构体的引用参数
2. 入参的生命周期决定了出参的生命周期,这是编辑器自动推测生命周期的基础。
2. 自动推测规则 1: 如果只有一个引用型输入,它的生命周期会赋给所有输出。
3. 自动推测规则 2: 如果有多个引用类型的参数,其中一个是 self ,那么它的生命周期会赋给所有输出。
4. 自动推测规则 3: 如果有多个引用,编译器无法知道开发者到底怎么使用出参,也就无法为出参指定生命周期。其实编译器能做到,如果那样,还不如上自动 GC.


第一条规则有一个重要的说明,就是引用类型的入参决定了整体的函数或结构体的生命周期。rust 结构体如果有构造函数,那么其实就是构造函数的入参的生命周期决定了结构体里引用成员的的生命周期,可惜的是,rust 没有,那么只能标注在结构体成员变量上
stein42
2022-05-02 10:15:15 +08:00
result 的 key 指向 sentence 的子字符串,所以 result 的生命周期不长于 sentence 。

result 的生命周期与 bias 无关,无脑加 'a 是有问题的,例如:

'''
use std::collections::HashMap;

fn index_t_words<'a>(sentence: &'a String, bias: &'a str) -> HashMap<&'a str, usize> {
let mut result: HashMap<&str, usize> = HashMap::new();
for (idx, word) in sentence.split(' ').enumerate() {
if word.chars().nth(0).unwrap() == 't' {
result.insert(word, idx);
}
};
result
}

fn main() {
let sentence: String = String::from("magic happens in the test statement");
let result = {
let bias: String = String::from("spoiler");
index_t_words(&sentence, &bias) // 错误!
};
println!("{:?}", result);
}
'''

正确的做法是:

'''
use std::collections::HashMap;

fn index_t_words<'a>(sentence: &'a String, bias: &str) -> HashMap<&'a str, usize> {
let mut result: HashMap<&str, usize> = HashMap::new();
for (idx, word) in sentence.split(' ').enumerate() {
if word.chars().nth(0).unwrap() == 't' {
result.insert(word, idx);
}
};
result
}

fn main() {
let sentence: String = String::from("magic happens in the test statement");
let result = {
let bias: String = String::from("spoiler");
index_t_words(&sentence, &bias)
};
println!("{:?}", result);
}
'''

只有一个参数时可以省略生命周期标注。

如果没有搞懂所有权、借用、生命周期这些概念,那还处于开始特别难的阶段。
GTim
2022-05-02 10:26:05 +08:00
@stein42 是的,现在市面上的教程都是无脑加,即使有 2 个不同生命周期 'a , 'b 人家也不告诉你为什么要加一个 'b 。
misdake
2022-05-02 10:29:29 +08:00
fn index_t_words(sentence: &String) -> HashMap<&str, usize>,其实隐含了输入和输出各自唯一的引用都是'a 。
认为唯一的输出引用的生命周期和唯一的输入引用的生命周期一致,绝大多数情况下是正确的。

fn index_t_words(sentence: &String, bias: &str) -> HashMap<&str, usize>,不能直接确定 sentence 和 bias 的生命周期是一致的,也不知道返回值的生命周期是什么样子:返回值引用了 sentence 还是 bias 还是'static ?,函数返回的时候,sentence 和 bias 能不能释放输入的引用?如果返回的引用还没释放时,bias 被修改了,允不允许?
在代码稍微复杂一点的情况下,假定所有引用的生命周期相同是非常危险的。
misdake
2022-05-02 10:37:33 +08:00
举两个例子,为什么有时候需要加标注,可以多想想为什么不允许修改,为什么只读引用没还回去而影响了修改。

第一个例子是一个很常见的情况,这个就是标注的意义
第二个例子举了一个编译器自动加 'a 会导致无法编译,必须手动加标注的例子

https://gist.github.com/misdake/364f00d6d7c43d88b5a5d2e7ba1144b9
PeterD
2022-05-02 10:39:20 +08:00
之前我对生命周期也不太理解,直到读完这本小书,大部分的疑惑都解决的差不多了。

https://okryvyts.github.io/lifetimes-book/introduction.html
Richard14
2022-05-02 11:09:46 +08:00
@stein42 我知道 1L 场景中正确的做法应该是加只给 sentence 加&'a ,但我无法理解这么做的含义。按 1L 例子中的场景,应该可以简述为,出参与哪个入参相关,就往哪个身上标记。

这个理解应该是简单的,只是感觉似乎不能解决所有疑问。也许不适用这条用法的情况下就不应该用引用的参数解决,而是应该引入智能指针一类东西?
stein42
2022-05-02 11:47:49 +08:00
Rust 的定位是和 C/C++ 一样运行效率高、没有垃圾回收,同时避免了 C/C++ 的很多错误,例如访问已释放的内存。

result 的 key 指向 sentence 的子字符串,所以 result 的生命周期应该不长于 sentence 。
否则先释放 sentence ,再访问 result ,就访问了已释放的内存,而这部分内存可能已经写入了其它数据。
C/C++ 这样做的话,可以通过编译,但运行时可能会出现莫名其妙的错误。
Rust 通过生命周期检查,可以避免这种错误。

'''
use std::collections::HashMap;

fn index_t_words<'a>(sentence: &'a String, bias: &str) -> HashMap<&'a str, usize> {
let mut result: HashMap<&str, usize> = HashMap::new();
for (idx, word) in sentence.split(' ').enumerate() {
if word.chars().nth(0).unwrap() == 't' {
result.insert(word, idx);
}
};
result
}

fn main() {
{
// 正确
let sentence: String = String::from("magic happens in the test statement");
let bias: String = String::from("spoiler");
let result = index_t_words(&sentence, &bias);
println!("{:?}", result);
}

{
// 错误,sentence 已经提前释放
let result = {
let sentence: String = String::from("magic happens in the test statement");
let bias: String = String::from("spoiler");
index_t_words(&sentence, &bias)
};
println!("{:?}", result);
}
}
'''
greygoo
2022-05-02 12:26:20 +08:00
greygoo
2022-05-02 12:37:05 +08:00
对于最后一个问题:
这样就可以了
fn index_t_words(sentence: &str) -> HashMap<&str, usize> {
let mut result: HashMap<&str, usize> = HashMap::new();
for (idx, word) in sentence.split(' ').enumerate() {
if word.chars().nth(0).unwrap() == 't' {
result.insert(word, idx);
}
}
result
}

fn main() {
let sentence: String = String::from("magic happens in the test statement");
println!("{:?}", index_t_words(&sentence))
}
Buges
2022-05-02 14:56:07 +08:00
你要知道生命周期是泛型参数,&'a T 只是 Ref<A,T>的语法糖。既然是类型参数,那当然有时候能自动推导,有时候需要手动标记。
maotao456
2022-06-23 17:29:57 +08:00
@misdake 猫鼻子?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/850454

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX