二、生命周期
动态还是静态?
动态/静态生命周期定义与表示方式
- 静态生命周期: ’static str
- 如果一个值的生命周期贯穿整个进程的生命周期,那么我们就称这种生命周期为静态生命周期。
- 当值拥有静态生命周期,其引用也具有静态生命周期。
- 我们在表述这种引用的时候,可以用 ’static 来表示。比如: &’static str 代表这是一个具有静态生命周期的字符串引用。
- 一般来说,全局变量、静态变量、字符串字面量(string literal (字面) )等,都拥有静态生命周期。
- 堆内存,如果使用了 Box::leak 后,也具有静态生命周期。
- 动态生命周期: ’a 、’b 或者 ’hello 这样的小写字符或者字符串来表述
- 如果一个值是在某个作用域中定义的,也就是说它被创建在栈上或者堆上,那么其生命周期是动态的。
- 当这个值的作用域结束时,值的生命周期也随之结束。
- 对于动态生命周期,我们约定用 ’a 、’b 或者 ’hello 这样的小写字符或者字符串来表述。
- ’ 后面具体是什么名字不重要,它代表某一段动态的生命周期
- 其中, &’a str 和 &’b str 表示这两个字符串引用的生命周期可能不一致。
动静态生命周期示意图
- 分配在堆和栈上的内存有其各自的作用域,它们的生命周期是动态的。
- 全局变量、静态变量、字符串字面量、代码等内容,在编译时,会被编译到可执行文件中的 BSS/Data/RoData/Text 段,然后在加载时,装入内存。
- 因而,它们的生命周期和进程的生命周期一致,所以是静态的。
- 所以,函数指针的生命周期也是静态的,因为函数在 Text 段中,只要进程活着,其内存一直存在。
如何识别生命周期
其实生命周期参数主要用于帮助编译器识别引用的生命周期范围,对于明显不符合生命周期参数的变量,哪怕加了生命周期参数也不会通过。
只有传址的参数,且多于一个,才可能需要生命周期标注
两个小例子
两个小例子
- x 引用了在内层作用域中创建出来的变量 y。由于,变量从开始定义到其作用域结束的这段时间,是它的生命周期,所以 x 的生命周期 ’a 大于 y 的生命周期 ’b,当 x 引用 y 时,编译器报错。
- y 和 x 处在同一个作用域下, x 引用了 y,我们可以看到 x 的生命周期 ’a 和 y 的生命周期 ’b 几乎同时结束,或者说 ’a 小于等于 ’b,所以,x 引用 y 是可行的。
编译器其实会自动进行生命周期标注
编译器希望尽可能减轻开发者的负担,其实所有使用了引用的函数,都需要生命周期的标注,只不过编译器会自动做这件事,省却了开发者的麻烦
编译器自动进行生命周期标注
- 无标注版本
fn main() { let s1 = "Hello world"; println!("first word of s1: {}", first(s1)); } // 如果你用 clippy,多余的 lifetime 会提醒你不需要 // fn first<'a>(s: &'a str) -> &'a str { fn first(s: &str) -> &str { let trimmed = s.trim(); match trimmed.find(' ') { None => "", Some(pos) => &trimmed[..pos], } }
- 自动标注
fn main() { let s1 = "Lindsey"; let s2 = String::from("Rosie"); let result = max(s1, &s2); println!("bigger one: {}", result); } fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1 > s2 { s1 } else { s2 } }
返回值如何标注?是 ’a 还是’b 呢?这里的冲突,编译器无能为力。
自动标注规则
- 所有引用类型的参数都有独立的生命周期 ’a 、’b 等。
- 如果只有一个引用型输入,它的生命周期会赋给所有输出。
- 如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。
需要生命周期标注的情况
missing lifetime specifier
fn main() { let s1 = String::from("Lindsey"); let s2 = String::from("Rosie"); let result = max(&s1, &s2); println!("bigger one: {}", result); let result = get_max(s1); println!("bigger one: {}", result); } fn get_max(s1: &str) -> &str { // 字符串字面量的生命周期是静态的,而 s1 是动态的,它们的生命周期显然不一致 max(s1, "Cynthia") } // 这段代码无法编译通过 fn max(s1: &str, s2: &str) -> &str { if s1 > s2 { s1 } else { s2 } }
- 编译器在编译 max() 函数时,无法判断 s1、s2 和返回值的生命周期。
- 函数本身携带的信息,就是编译器在编译时使用的全部信息。
- 这里函数本身提供的信息就告诉编译期,生命周期不一致
添加生命周期标注即可编译通过
fn main() { let s1 = String::from("Lindsey"); let s2 = String::from("Rosie"); let result = max(&s1, &s2); println!("bigger one: {}", result); let result = get_max(&s1); println!("bigger one: {}", result); } fn get_max(s1: &str) -> &str { max(s1, "Cynthia") } fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str { if s1 > s2 { s1 } else { s2 } }
生命周期标注练习
标注练习题
pub fn strtok(s: &mut &str, delimiter: char) -> &str { if let Some(i) = s.find(delimiter) { let prefix = &s[..i]; // 由于 delimiter 可以是 utf8,所以我们需要获得其 utf8 长度, // 直接使用 len 返回的是字节长度,会有问题 let suffix = &s[(i + delimiter.len_utf8())..]; *s = suffix; prefix } else { // 如果没找到,返回整个字符串,把原字符串指针 s 指向空串 let prefix = *s; *s = ""; prefix } } fn main() { let s = "hello world".to_owned(); let mut s1 = s.as_str(); let hello = strtok(&mut s1, ' '); println!("hello is: {}, s1: {}, s: {}", hello, s1, s); }
- 按照编译器的规则, &mut &str 添加生命周期后变成 &’b mut &’a str
- 这将导致返回的 ’&str 无法选择一个合适的生命周期。
标注练习题参考
pub fn strtok<'a>(s: &mut &'a str, delimiter: char) -> &'a str { if let Some(i) = s.find(delimiter) { let prefix = &s[..i]; let suffix = &s[(i + delimiter.len_utf8())..]; *s = suffix; prefix } else { let prefix = *s; *s = ""; prefix } } fn main() { let s = "hello world".to_owned(); let mut s1 = s.as_str(); let hello = strtok(&mut s1, ' '); println!("hello is: {}, s1: {}, s: {}", hello, s1, s); }