在本文中,我们将围绕着字符串分割的实例,讲解 Rust 中的生命周期。首先我们会剖析为什么需要生命周期、什么是生命周期、以及如何标注生命周期;接下来引入多生命周期标注,并阐述什么时候需要标注多个生命周期。在此基础上,我们向前多迈一步,使用自定义的 trait 来取代分隔符的定义,让实现更加通用。最后通过查看标准库字符串分割的实现,综合理解本文中所有的知识点。
前置要求
至少看过 Rust The Book 前 8 章的内容。推荐的学习资料:
Take your first steps with Rust 微软推出的 Rust 培训课程,可以配合视频一起使用 Rust for Beginners
Rust The Book —— 第 4 章和第 10 章的内容与本文密切相关,建议重新阅读一遍
极客时间专栏 陈天 · Rust 编程第一课 —— 第 7 讲 - 第 11 讲
Jon Gjengset 的 YouTube 频道,本文就是 Crust of Rust 系列 Lifetime Annotations 的学习笔记
快速开始
确定目标,实现字符串分割
input: "a b c d e" -- &str
output: "a" "b" "c" "d" "e" -- 分隔符指定为 " ",每次 next 得到一个 &str
开始一个 Rust 项目:
cargo new --lib strsplit
同时,我们可以使用 Rust Playground 进行练习,文中展示的所有代码都提供了 playground 链接,可以点击跳转过去,Run 起来测试一下。
搭建骨架
定义数据结构和方法,添加单元测试,搭建好骨架:
pub struct StrSplit { remainder: &str, delimiter: &str,}impl StrSplit { pub fn new(haystack: &str, delimiter: &str) -> Self { // ....
}}impl Iterator for StrSplit { type Item = &str; fn next(&mut self) -> Option<Self::Item> { // ...
}}#[test]fn it_works() { let haystack = "a b c d e"; let letters: Vec<_> = StrSplit::new(haystack, " ").collect(); assert_eq!(letters, vec!["a", "b", "c", "d", "e"]);}
实现 Iterator Trait 之后,就可以使用 for
循环遍历对应的 struct。
为什么使用 &str,而不是 String?
当我们对一个知识点不熟悉时,打开 playground,写一段代码测试一下
为了方便解释,我们写一段简单的代码(代码 0,String, str and &str)
fn main() { let noodles: &'static str = "noodles"; // let poodles: String = String::from(noodles);
// https://doc.rust-lang.org/std/primitive.slice.html#method.to_vec
let poodles: String = noodles.to_string(); // 底层调用的就是 String::from(noodles);
let oodles: &str = &poodles[1..]; println!("addr of {:?}: {:p}", "noodles", &"noodles"); println!("addr of noodles: {:p}, len: {}, size: {}", &noodles, noodles.len(), std::mem::size_of_val(&noodles)); println!("addr of poodles: {:p}, len: {}, capacity: {}, size: {}", &poodles, poodles.len(), poodles.capacity(), std::mem::size_of_val(&poodles)); println!("addr of oodles: {:p}, len: {}, size: {}", &oodles, oodles.len(), std::mem::size_of_val(&oodles));}
"noodles"
作为字符串常量(string literal),编译时存入可执行文件的 .RODATA 段,在程序加载时,获得一个固定的内存地址。作为一个字符串切片赋值给栈上变量 noodles
,拥有静态生命周期(static lifetime),在程序运行期间一直有效。
当执行 noodles.to_string()
时,跟踪标准库实现,最后调用 [u8]::to_vec_in()
,在堆上分配一块新的内存,将 "noodles"
逐字节拷贝过去。
当把堆上的数据赋值给 poodles
时,poodles
作为分配在栈上的一个变量,其拥有(owns)堆上数据的所有权,使用胖指针(fat pointer)进行表示:ptr
指向字符串堆内存的首地址、length
表示字符串当前长度、capacity
表示分配的堆内存总容量。
oodles
为字符串切片,表示对字符串某一部分(包含全部字符串)的引用的(A string slice is a reference to part of a String),包含两部分内容:ptr
指向字符串切片首地址(可以为堆内存和 static 静态内存)、length
表示切片长度。
下图清晰展示了这里的关系:
str
——[char]
,表示为一串字符序列(a sequence of characters),编译期无法确定其长度(dynamically sized);&str
——&[T]
,表示为一个胖指针(fat pointer),ptr
指向切片首地址、length
表示切片长度,编译期可以确定其长度为 16 字节;String
——Vec<char>
,表示为一个胖指针(fat pointer),ptr
指向字符串堆内存的首地址、length
表示字符串当前长度、capacity
表示分配的堆内存的总容量。堆内存支持动态扩展和收缩。编译期可以确定其长度为 24 字节。
在这里,针对分隔符 delimiter
,使用 String 会存在两个问题:
1、涉及堆内存分配,开销大;
2、需要进行堆内存分配,而在嵌入式系统中是没有堆内存的,会有兼容性问题。
因此使用 &str
类型。
Iterator trait
查看标准文档 Iterator trait
pub trait Iterator { /// The type of the elements being iterated over.
type Item; // 必须实现的关联方法,被其他关联方法的缺省实现所依赖
/// Advances the iterator and returns the next value.
///
/// Returns [`None`] when iteration is finished. Individual iterator
/// implementations may choose to resume iteration, and so calling `next()`
/// again may or may not eventually start returning [`Some(Item)`] again at some
/// point.
fn next(&mut self) -> Option<Self::Item>; // 其他的一些关联方法,有缺省实现
fn collect<B>(self) -> B where B: FromIterator<Self::Item>, { ... } // ...
}
关联类型(associated types)—— 例如
type Item;
为迭代遍历的类型,只有在用户实现 Iterator trait 时才能够确定遍历的值的类型,延迟绑定。方法(methods),也称为关联函数(associated functions)—— 对于 Iterator trait,
next()
是必须实现的(Request methods),在值存在时,返回Some(item)
;值不存在时,返回None
。trait 中的其他方法有缺省的实现。也就是说,只要用户实现了 Iterator trait 的next()
方法,该 trait 中的其他方法就有了默认实现,可以直接进行使用。
什么时候用 Self,什么时候用 self?
Self
代表当前的类型,比如StrSplit
类型实现Iterator
,那么实现过程中使用到的Self
就指代StrSplit
;self
在用作方法的第一个参数时,实际上就是self: Self
(参数名: 参数类型)的简写,所以&self
是self: &Self
,而&mut self
是self: &mut Self
。
因此 Iterator trait 的 next()
签名展开为:
pub trait Iterator { type Item; // fn next(&mut self) -> Option<Self::Item>;
fn next(self: &mut Self) -> Option<Self::Item>;}
version #1: hands on
让我们直接开始吧(代码 1,version #1: hands on)
pub struct StrSplit { remainder: &str, delimiter: &str,}impl StrSplit { pub fn new(haystack: &str, delimiter: &str) -> Self { Self { remainder: haystack, delimiter, } }}impl Iterator for StrSplit { type Item = &str; fn next(&mut self) -> Option<Self::Item> { if let Some(next_delim) = self.remainder.find(self.delimiter) { let until_delimiter = &self.remainder[..next_delim]; self.remainder = &self.remainder[(next_delim + self.delimiter.len())..]; Some(until_delimiter) } else if self.remainder.is_empty() { None } else { let rest = self.remainder; self.remainder = ""; Some(rest) } }}#[test]fn it_works() { let haystack = "a b c d e"; let letters: Vec<_> = StrSplit::new(haystack, " ").collect(); assert_eq!(letters, vec!["a", "b", "c", "d", "e"]);}
next()
的实现很简单:
1、在字符串中查找分隔符第一次出现的位置,如果找到返回索引值 Some(usize)
,未找到返回 None
;
2、根据索引值将字符串分为三个部分,第一部分为 next()
的返回值,第二部分为分隔符,第三部分为剩余待处理的字符串,在下一次调用 next()
作为原始字符串。
执行编译,报错信息如下:
Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
--> src/lib.rs:2:16
|
2 | remainder: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ pub struct StrSplit<'a> {
2 ~ remainder: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/lib.rs:3:16
|
3 | delimiter: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ pub struct StrSplit<'a> {
2 | remainder: &str,
3 ~ delimiter: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/lib.rs:16:17
|
16 | type Item = &str;
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
16 | type Item<'a> = &'a str;
| ++++ ++
For more information about this error, try `rustc --explain E0106`.
三个错误信息都提示缺少生命周期标注(lifetime specifier),编译器建议添加生命周期参数(lifetime parameter),因此我们在 versi