切片类型
Slice 允许您引用集合中的连续元素序列,而不是整个集合。一个 slice 是一种引用,因此它没有所有权。
这里有一个小编程问题:编写一个函数,它接受一个字符串 words 中用空格分隔,并返回在该字符串中找到的第一个单词。 如果函数在字符串中找不到空格,则整个字符串必须为 一个单词,因此应返回整个字符串。
让我们来看看如何在不使用 slices 来理解 slices 将解决的问题:
fn first_word(s: &String) -> ?
该函数具有 a as 参数。我们不想
所有权,所以这很好。但是我们应该返回什么呢?我们真的没有
来谈论字符串的一部分。但是,我们可以返回
单词的结尾,由空格表示。让我们试一试,如示例 4-7 所示。first_word
&String
因为我们需要逐个元素遍历并检查
一个值是一个空格,我们将使用该方法将 THE VALUE 转换为字节数组。String
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 章更详细地讨论迭代器。
现在,请知道这是一个返回集合中每个元素的方法
,它将 的结果包装并返回每个元素
是元组的一部分。返回的 Tuples 的第一个元素是索引,第二个元素是对元素的引用。
这比我们自己计算指数要方便一些。iter
enumerate
iter
enumerate
因为该方法返回一个元组,所以我们可以使用 patterns 来
解构该元组。我们将在 Chapter 中详细讨论模式
6. 在循环中,我们指定一个模式,该模式具有元组中的索引和元组中的单个字节。
因为我们从 中获取对元素的引用,所以我们在模式中使用。enumerate
for
i
&item
.iter().enumerate()
&
在循环中,我们搜索表示空格的字节
使用 byte literal 语法。如果我们找到一个空格,我们返回位置。
否则,我们使用 .for
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() {}
我们现在有办法找出
string,但有一个问题。我们要单独返回 a,但它是
在 .换句话说,
因为它是独立于 的值,所以不能保证它
将来仍然有效。考虑示例 4-8 中的程序,
使用示例 4-7 中的函数。usize
&String
String
first_word
该程序编译时没有任何错误,如果我们在调用 .因为 根本没有连接到状态,所以仍然包含值 。我们可以将该值与
该变量尝试提取第一个单词,但这将是一个错误
因为自从我们保存在 中以来,内容已经发生了变化。word
s.clear()
word
s
word
5
5
s
s
5
word
不得不担心索引与数据不同步是乏味且容易出错的!如果
我们编写一个函数。它的签名必须如下所示:word
s
second_word
fn second_word(s: &String) -> (usize, usize) {
现在,我们正在跟踪起始索引和结束索引,并且还有更多 根据特定状态中的数据计算但未绑定到 那个州。我们有三个不相关的变量围绕着这个需求 保持同步。
幸运的是,Rust 有一个解决这个问题的方法:字符串切片。
字符串切片
字符串 slice 是对 的一部分的引用,它看起来像这样:String
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
而不是对整个 的引用,而是对
部分。我们创建切片
通过指定 ,
其中 是切片中的第一个位置,并且 是
比切片中的最后一个位置多 1。在内部,切片数据
structure 存储切片的起始位置和长度,其中
对应于 minus 。因此,在 的情况下,将是一个包含指向
的字节,长度值为 。String
hello
String
[0..5]
[starting_index..ending_index]
starting_index
ending_index
ending_index
starting_index
let world = &s[6..11];
world
s
5
图 4-7 在图表中显示了这一点。
使用 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 字符处 边界。如果您尝试在 multibyte 字符,则程序将退出并显示错误。目的 在引入字符串切片时,我们只在本节中假设 ASCII;一个 对 UTF-8 处理的更深入讨论在“存储 UTF-8 编码的 Text with Strings“部分。
考虑到所有这些信息,让我们重写以返回一个
片。表示 “string slice” 的类型写为:first_word
&str
我们得到单词末尾的索引的方式与示例 4-7 中相同,通过 查找 Space 的第一个匹配项。当我们找到一个空格时,我们返回一个 string 切片,使用字符串的开头和空格的索引作为 起始索引和结束索引。
现在,当我们调用 时,我们会返回一个与
基础数据。该值由对
切片和切片中的元素数。first_word
返回 slice 也适用于函数:second_word
fn second_word(s: &String) -> &str {
我们现在有一个简单的 API,它更难搞砸,因为
编译器将确保对 的引用保持有效。记得
示例 4-8 中程序中的 bug,当我们获取索引到
第一个单词,但随后清除了字符串,所以我们的索引无效?该代码是
逻辑上不正确,但没有立即显示任何错误。问题会
如果我们一直尝试使用第一个单词索引和 emptyTed,则稍后显示
字符串。切片使这个错误变得不可能,并让我们知道我们有一个问题
我们的代码要快得多。使用 slice 版本的 将抛出一个
编译时错误:String
first_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
回想一下借用规则,如果我们有一个对
something,我们也不能采用可变引用。因为需要
truncate 的 ,它需要获取一个可变的引用。之后的 to 调用使用 中的 引用 ,因此不可变的
此时 reference 必须仍处于 active 状态。Rust 不允许 mutable
reference in 和不可变的 reference in from 存在于
同时,编译失败。Rust 不仅使我们的 API 更易于使用,
但它也消除了编译时的一整类错误!clear
String
println!
clear
word
clear
word
字符串文本作为切片
回想一下,我们讨论了存储在二进制文件中的字符串 Literals。现在 了解了 slices,我们可以正确理解字符串字面量:
#![allow(unused)] fn main() { let s = "Hello, world!"; }
这里的类型是 :它是一个指向 的特定点的切片
二进制。这也是字符串 Literals 不可变的原因; 是一个
immutable 引用。s
&str
&str
字符串切片作为参数
知道你可以获取 Literals 和 Value 的切片,我们就可以
对 的另一项改进,那就是它的签名:String
first_word
fn first_word(s: &String) -> &str {
更有经验的 Rustacean 会写示例 4-9 中所示的签名
相反,因为它允许我们对两个值使用相同的函数
和价值观。&String
&str
如果我们有一个字符串 slice,我们可以直接传递它。如果我们有一个 ,我们
可以将 的切片 或 引用传递给 。这
灵活性利用了 Deref 强制转换,我们将在“使用 Functions 和
方法“部分。String
String
String
定义一个函数来获取字符串 slice 而不是对 a 的引用,使我们的 API 更加通用和有用,而不会丢失任何功能:String
其他切片
正如您可能想象的那样,字符串切片是特定于字符串的。但是有一个 更通用的切片类型也是如此。考虑这个数组:
#![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]); }
此切片的类型为 .它的工作方式与字符串切片相同,通过
存储对第一个元素的引用和长度。您将使用这种
slice 用于各种其他集合。我们将在
细节。&[i32]
总结
所有权、借用和切片的概念确保了 Rust 中的内存安全 程序。Rust 语言让你控制你的内存 用法与其他 Systems 编程语言相同,但具有 数据所有者会在所有者超出范围时自动清理该数据 意味着您不必编写和调试额外的代码来获得此控制权。
所有权会影响 Rust 的许多其他部分的工作方式,因此我们将讨论
这些概念在本书的其余部分得到了进一步的阐述。让我们继续
第 5 章,了解如何将数据分组到 .struct
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准