CRust 學習筆記:生命週期 - 2
本系列文章是 Jon Gjengset 發佈的 CRust of Rust 系列視頻的學習筆記,CRust of Rust 是一系列持續更新的 Rust 中級教程。
讓我們接着上一篇文章繼續學習 Rust 的生命週期。在上一篇文章中的代碼基礎上,加入如下的函數和測試用例:
fn until_char(s: &str, c: char) -> &str {
StrSplit::new(s, &format!("{}", c))
.next()
.expect("StrSplit always gives at least one result")
}
#[test]
fn until_char_test() {
assert_eq!(until_char("hello world", 'o'), "hell");
}
編譯器會報如下錯誤:
error[E0515]: cannot return value referencing temporary value
這裏的臨時值是 & format!("{}",c),從代碼中可以看出,參數 s、c 和 next() 之後的值要擁有相同的生命週期,因此返回值與臨時值的生命週期相同。但是這個臨時值的生命週期在函數執行完後就結束了,所以編譯不通過。
有一種解決辦法是使用 String
#[derive(Debug)]
pub struct StrSplit<'a> {
// 使用Option
remainder: Option<&'a str>,
delimiter: String,
}
但是使用 String 是有兩個問題的,我們先來比較一下 str,&str,String 之間的區別:
-
str -> [char]: 相當於 char 類型的切片,既可以分配在棧上,也可以分配在堆上,還可以分配在 static 區。
-
&str -> &[char]: 相當於胖指針,包含指向 str 的指針和字符串的長度。
-
String -> Vec: 分配在堆上的字符向量,在棧上有一個胖指針指向這個堆上的字符向量。
String 轉換 & str 相對容易一些,因爲已知字符串的起始位置及長度。而 & str 轉換成 String 就複雜一些,需要先在堆上分配一段空間,然後再通過內存拷貝 (memcpy) 把字符 copy 到堆上。
因此使用 String 的第一個問題是性能問題;第二個問題是不能兼容嵌入式系統,大多數嵌入式系統沒有堆內存。
我們選擇更好的解決方案,定義多個生命週期
#[derive(Debug)]
pub struct StrSplit<'haystack, 'delimiter> {
// 使用Option
remainder: Option<&'haystack str>,
delimiter: &'delimiter str,
}
impl<'haystack, 'delimiter> StrSplit<'haystack, 'delimiter> {
/**
* 新構建的StrSplit與傳入的參數haystack,delimiter 擁有相同的生命週期
*/
pub fn new(haystack: &'haystack str, delimiter: &'delimiter str) -> Self {
Self {
remainder: Some(haystack),
delimiter,
}
}
}
impl<'haystack> Iterator for StrSplit<'haystack, '_> {
// 迭代的結果也要與StrSplit擁有相同的生命週期,是因爲要在StrSplit的成員remainder上做迭代。
type Item = &'haystack str;
fn next(&mut self) -> Option<Self::Item> {
let remainder = self.remainder.as_mut()?;
if let Some(next_delim) = remainder.find(self.delimiter) {
let until_remainder = &remainder[..next_delim];
*remainder = &remainder[next_delim + self.delimiter.len()..];
Some(until_remainder)
} else {
self.remainder.take()
}
}
}
執行 cargo test,測試通過。
泛型化 Delimiter
在這裏我們將分隔符進行泛型化,使得 StrSplit 更加通用。
pub struct StrSplit<'haystack, D> {
// 使用Option
remainder: Option<&'haystack str>,
delimiter: D,
}
impl<'haystack, D> StrSplit<'haystack, D> {
pub fn new(haystack: &'haystack str, delimiter: D) -> Self {
Self {
remainder: Some(haystack),
delimiter,
}
}
}
定義一個 trait,包含一個 find_next() 方法,用於返回分隔符在字符串中的起始位置和結束位置
pub trait Delimiter {
// 返回分隔符在字符串中的起始位置和結束位置
fn find_next(&self, s: &str) -> Option<(usize, usize)>;
}
迭代器修改如下:
impl<'haystack, D> Iterator for StrSplit<'haystack, D>
where
D: Delimiter
{
// 迭代的結果也要與StrSplit擁有相同的生命週期,是因爲要在StrSplit的成員remainder上做迭代。
type Item = &'haystack str;
fn next(&mut self) -> Option<Self::Item> {
let remainder = self.remainder.as_mut()?;
if let Some((delim_start, delim_end)) = self.delimiter.find_next(remainder) {
let until_remainder = &remainder[..delim_start];
*remainder = &remainder[delim_end..];
Some(until_remainder)
} else {
self.remainder.take()
}
}
}
然後爲 & str 和 char 分別實現 Delimiter trait:
impl Delimiter for &str {
fn find_next(&self, s: &str) -> Option<(usize, usize)> {
s.find(self).map(|start| (start, start + self.len()))
}
}
impl Delimiter for char {
fn find_next(&self, s: &str) -> Option<(usize, usize)> {
s.char_indices()
.find(|(_, c)| c == self)
.map(|(start, _)| (start, start + self.len_utf8()))
}
}
函數 until_char() 修改爲:
pub fn until_char(s: &str, c: char) -> &str {
StrSplit::new(s, c)
.next()
.expect("StrSplit always gives at least one result")
}
執行 cargo test,測試通過。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Air9iZvHV3vhGFezMfLZSA