切片类型
切片允许您在
集合,而不是整个集合。切片是一种引用,因此它没有所有权。
这里有一个小的编程问题:编写一个函数,该函数接受一串用空格分隔的单词,并返回在该字符串中找到的第一个单词。如果函数在字符串中找不到空格,则整个字符串必须是一个单词,因此应返回整个字符串。
让我们来看看如何在不使用 slices 的情况下编写这个函数的签名,以了解 slices 将解决的问题:
fn first_word(s: &String) -> ?first_word 函数具有 &String 作为参数。我们不想要所有权,所以这很好。但是我们应该返回什么呢?我们真的没有办法谈论字符串的一部分。但是,我们可以返回单词末尾的索引,由空格表示。让我们试一试,如示例 4-7 所示。
文件名: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
示例 4-7:将字节索引值返回到
String 参数中的 first_word 函数
因为我们需要逐个元素遍历 String 并检查值是否为空格,所以我们将使用
as_bytes方法。
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}
fn main() {}
接下来,我们使用 iter 方法在字节数组上创建一个迭代器:
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}
fn main() {}
我们将在第 13 章更详细地讨论迭代器。现在,要知道 iter 是一个返回集合中每个元素的方法,而 enumerate 包装 iter 的结果并将每个元素作为
是元组的一部分。从
enumerate 是索引,第二个元素是对元素的引用。这比我们自己计算指数要方便一些。
因为 enumerate 方法返回一个元组,所以我们可以使用 patterns 来解构该元组。我们将在 Chapter 中详细讨论模式
6. 在 for 循环中,我们指定一个具有 i 的模式
用于元组中的索引,&item 用于元组中的单个字节。因为我们从 .iter().enumerate() 获取对元素的引用,所以我们使用
& 在图案中。
在 for 循环中,我们使用 byte literal 语法搜索表示空格的字节。如果我们找到一个空格,我们返回位置。否则,我们使用 s.len() 返回字符串的长度。
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}
fn main() {}
我们现在有办法找出字符串中第一个单词的结尾的索引,但有一个问题。我们返回一个 usize 本身,但它在 &String 的上下文中只是一个有意义的数字。换句话说,因为它是独立于 String 的值,所以不能保证它在将来仍然有效。考虑示例 4-8 中的程序,它使用示例 4-7 中的 first_word 函数。
文件名: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 s.clear(); // this empties the String, making it equal to "" // word still has the value 5 here, but there's no more string that // we could meaningfully use the value 5 with. word is now totally invalid! }
示例 4-8:存储调用
first_word 函数的结果,然后更改 String 内容
该程序编译时没有任何错误,如果我们使用 word,也会这样做
在调用 s.clear() 之后。因为 word 与 s 的状态无关
毕竟,Word 仍然包含值 5。我们可以将该值 5 与变量 s 一起使用,以尝试提取第一个单词,但这将是一个错误,因为自从我们在 word 中保存 5 以来,s 的内容已经发生了变化。
不得不担心 word 中的索引与
s 很乏味且容易出错!如果我们编写一个 second_word 函数,管理这些索引就更加脆弱了。它的签名必须如下所示:
fn second_word(s: &String) -> (usize, usize) {
现在,我们正在跟踪起始索引和结束索引,并且有更多的值是根据特定状态的数据计算得出的,但与该状态完全无关。我们有三个不相关的变量需要保持同步。
幸运的是,Rust 有一个解决这个问题的方法:字符串切片。
字符串切片
String slice 是对 String 的一部分的引用,它看起来像这样:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
hello 不是对整个 String 的引用,而是对 String 的一部分的引用,在额外的 [0..5] 位中指定。我们通过指定 [starting_index..ending_index] ,使用括号内的范围创建切片,其中 starting_index 是切片中的第一个位置,ending_index 是切片中的最后一个位置多 1。在内部,切片数据结构存储切片的起始位置和长度,对应于ending_index减 starting_index。因此,在 let world = &s[6..11];的情况下,world 将是一个切片,其中包含一个指向 s 索引 6 处的字节的指针,长度值为 5。
图 4-7 在图表中显示了这一点。
图 4-7:引用 a 的一部分的字符串 slice
字符串
使用 Rust 的 .. range 语法,如果要从索引 0 开始,可以删除两个句点之前的值。换句话说,这些是相等的:
#![allow(unused)] fn main() { let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; }
同样,如果您的切片包含 String 的最后一个字节,则可以删除尾随数字。这意味着它们是相等的:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; }
您还可以删除这两个值以获取整个字符串的切片。所以这些是相等的:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..]; }
注意:字符串切片范围索引必须位于有效的 UTF-8 字符边界处。如果尝试在多字节字符的中间创建字符串切片,则程序将退出并显示错误。为了介绍字符串切片,我们在本节中仅假设 ASCII;对 UTF-8 处理的更深入讨论在“存储 UTF-8 编码的
Text with Strings“部分。
记住所有这些信息后,让我们重写 first_word 以返回一个 slice。表示 “string slice” 的类型写为 &str:
文件名: src/main.rs
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
我们得到单词末尾的索引的方式与示例 4-7 中的相同,通过查找第一次出现的空格。当我们找到一个空格时,我们使用字符串的开头和空格的索引作为开始和结束索引返回一个字符串切片。
现在,当我们调用 first_word 时,我们会返回一个与基础数据相关的值。该值由对切片起点的引用和切片中的元素数组成。
返回 slice 也适用于 second_word 函数:
fn second_word(s: &String) -> &str {
我们现在有一个简单的 API,它更难搞砸,因为编译器将确保对 String 的引用保持有效。还记得示例 4-8 中程序中的错误吗,当我们把索引弄到第一个单词的末尾,但随后清除了字符串,所以我们的索引是无效的吗?该代码在逻辑上是错误的,但没有立即显示任何错误。如果我们一直尝试将第一个单词索引与空字符串一起使用,问题稍后就会出现。切片使这个错误变得不可能,并让我们更快地知道我们的代码有问题。使用 first_word 的切片版本将引发编译时错误:
文件名: src/main.rs
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    s.clear(); // error!
    println!("the first word is: {word}");
}
这是编译器错误:
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                  ------ immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
回想一下借用规则,如果我们对某物有一个不可变的引用,我们就不能也采用一个可变的引用。因为 clear 需要截断 String,所以它需要获取一个可变的引用。println!
在调用 clear 后使用 Word 中的引用,因此不可变引用在该点必须仍处于活动状态。Rust 不允许 clear 中的可变引用和 word 中的不可变引用同时存在,编译失败。Rust 不仅使我们的 API 更易于使用,而且还消除了编译时的整个错误类别!
字符串文本作为切片
回想一下,我们讨论了存储在二进制文件中的字符串 Literals。现在我们已经了解了 slices,我们可以正确理解字符串 Literals:
#![allow(unused)] fn main() { let s = "Hello, world!"; }
这里的 s 类型是 &str:它是一个指向二进制特定点的切片。这也是字符串 Literals 不可变的原因;&str 是一个不可变的引用。
字符串切片作为参数
知道你可以获取 Literals 和 String 值的切片,这让我们对 first_word 进行了另一项改进,这就是它的标志:
fn first_word(s: &String) -> &str {
更有经验的 Rustacean 会编写示例 4-9 所示的签名,因为它允许我们对 &String 值和 &str 值使用相同的函数。
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}
fn main() {
    let my_string = String::from("hello world");
    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);
    let my_string_literal = "hello world";
    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);
    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}示例 4-9:通过使用字符串 slice 作为
s 参数的类型来改进 first_word 函数
如果我们有一个字符串 slice,我们可以直接传递它。如果我们有一个 String,我们可以传递 String 的切片或对 String 的引用。这种灵活性利用了 deref 强制转换,我们将在
“使用 Functions 和 的隐式 Deref 强制转换
方法“部分。
定义一个函数来获取字符串切片而不是对 String 的引用
使我们的 API 更通用、更有用,而不会丢失任何功能:
文件名: src/main.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // `first_word` works on slices of `String`s, whether partial or whole let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // `first_word` also works on references to `String`s, which are equivalent // to whole slices of `String`s let word = first_word(&my_string); let my_string_literal = "hello world"; // `first_word` works on slices of string literals, whether partial or whole let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
其他切片
正如您可能想象的那样,字符串切片是特定于字符串的。但还有一个更通用的 slice 类型。考虑这个数组:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; }
正如我们可能想要引用字符串的一部分一样,我们可能想要引用数组的一部分。我们会这样做:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }
这个切片的类型是 &[i32]。它的工作方式与字符串切片相同,通过存储对第一个元素的引用和长度。您将对各种其他集合使用这种 slice。当我们在 Chapter 8 讨论 vector 时,我们将详细讨论这些集合。
总结
所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言使您可以像其他系统编程语言一样控制内存使用情况,但是当所有者超出范围时,让数据所有者自动清理该数据意味着您不必编写和调试额外的代码来获得此控制权。
所有权会影响 Rust 的许多其他部分的工作方式,因此我们将在本书的其余部分进一步讨论这些概念。让我们继续第 5 章,看看如何将数据片段分组到一个结构体中。