【投稿】Rust 中的生命周期 —— 从 StrSplit 实例说开去

本文通过分析 Rust 中的字符串分割实例,深入探讨了生命周期的概念、标注方法以及多生命周期的处理。通过逐步改进代码,引入自定义的 trait 并实现通用的分隔符处理,最后对比标准库的 `str::split` 实现,加深了对 Rust 生命周期的理解。
摘要由CSDN通过智能技术生成

在本文中,我们将围绕着字符串分割的实例,讲解 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 表示切片长度。

下图清晰展示了这里的关系:

6c1c907e9d6e1be8bb79d3f461dbf390.png
  • 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值