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