字符串的切片操作与连接_Rust切片详解

slice也是另一个没有所有权的数据类型。切片使您可以引用集合中连续的元素序列,而不是整个集合。
这是一个小的编程问题:编写一个函数,该函数接受一个字符串并返回在该字符串中找到的第一个单词。如果函数在字符串中找不到空格,则整个字符串必须是一个单词,因此应返回整个字符串。
让我们考虑一下此函数的签名:
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()}

清单4-7:将first_word字节索引值返回到String参数的函数
因为我们需要String逐个元素地检查并检查一个值是否为空格,所以我们将String使用以下as_bytes方法将其转换为字节数组 :
let bytes = s.as_bytes();

接下来,我们使用以下iter方法在字节数组上创建一个迭代器:
for (i, &item) in bytes.iter().enumerate() {

我们将在第13章中更详细地讨论迭代器。到目前为止,我们知道这iter 是一种返回集合中每个元素并enumerate 包装结果iter并将其作为元组的一部分返回的方法。返回的元组的第一个元素enumerate是索引,第二个元素是对该元素的引用。这比自己计算索引要方便一些。
因为该enumerate方法返回一个元组,所以我们可以使用模式来破坏该元组,就像Rust中的其他地方一样。因此,在for 循环中,我们指定一种模式,该模式具有i元组中的索引和元组&item 中的单个字节。因为我们从中获得了对元素的引用,所以.iter().enumerate()我们&在模式中使用。
在for循环内部,我们使用字节文字语法搜索表示空间的字节。如果找到空间,则返回该位置。否则,我们使用以下命令返回字符串的长度s.len():
if item == b' ' {return i; }}s.len()

现在,我们有一种方法可以找出字符串中第一个单词的末尾的索引,但这是有问题的。我们将usize单独返回a ,但在的上下文中这只是一个有意义的数字&String。换句话说,由于它是与分开的值String,因此无法保证它将来仍然有效。考虑清单4-8中的程序,该程序使用first_word清单4-7中的函数。
文件名:src / main.rs
fn main() {let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5s.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!}

【字符串的切片操作与连接_Rust切片详解】清单4-8:存储调用 first_word函数然后更改String内容的结果
该程序的编译没有任何错误,如果word 在调用后使用,也可以这样做s.clear()。因为word根本没有连接到状态s ,所以word仍然包含value 5。我们可以将该值5与变量s一起使用,以尝试提取出第一个单词,但这将是一个错误,因为s自保存5到以来,的内容已更改word。
不必担心索引word与数据不同步 s是乏味且容易出错的!如果我们编写second_word函数,则管理这些索引的难度更大。其签名必须如下所示:
fn second_word(s: &String) -> (usize, usize) {

现在,我们正在跟踪起始索引结束索引,并且有更多的值是根据特定状态下的数据计算得出的,但根本没有与该状态绑定的值。现在,我们有三个不相关的变量需要保持同步。
幸运的是,Rust解决了这个问题:字符串切片。
切片 一个串片是给一部分的参考String,它看起来是这样的:
let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11];

这类似于对整体进行引用,String但要多加一 [0..5]点点。而不是整个参考String,它是对的一部分的参考String。
通过指定[starting_index..ending_index],我们可以使用方括号内的范围创建切片 ,其中,starting_index是切片中的第一个位置,比切片中ending_index的最后一个位置大。在内部,切片数据结构存储的起始位置及切片,其对应于长度ending_index减去 starting_index。因此,在的情况下let world = &s[6..11]; ,world将是一个切片,该切片包含一个指向第7个字节(从1开始计数)的指针,其s长度值为5。
字符串的切片操作与连接_Rust切片详解
文章图片
图4-6对此进行了显示。
使用Rust的..range语法,如果要从第一个索引(零)开始,则可以在两个句点之前删除该值。换句话说,这些是相等的:
let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2];

同样,如果您的分片包含的最后一个字节,则String可以删除尾随数字。这意味着这些是相等的:
let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..];

您还可以删除两个值以截取整个字符串。所以这些是相等的:
let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..];

注意:字符串切片范围索引必须出现在有效的UTF-8字符边界处。如果尝试在多字节字符的中间创建字符串切片,则程序将退出并出现错误。为了引入字符串片段,我们仅在本节中假定为ASCII;否则,将使用ASCII。第8章的“使用字符串存储UTF-8编码文本”一节对UTF-8处理进行了更全面的讨论。
考虑到所有这些信息,让我们重写first_word以返回切片。表示“字符串切片”的类型写为&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[..]}

通过查找空格的第一个匹配项,可以像清单4-7一样获得单词结尾的索引。当找到一个空格时,我们使用字符串的开头和空格的索引作为开始和结束索引来返回一个字符串切片。
现在,当调用时first_word,我们将获得与基础数据相关的单个值。该值由对切片起点的引用以及切片中元素的数量组成。
返回切片也可以用于以下second_word功能:
fn second_word(s: &String) -> &str {

现在,我们有了一个简单的API,很难将其弄乱,因为编译器将确保对引用的引用String保持有效。还记得清单4-8种程序中的错误吗?当我们将索引放在第一个单词的末尾但又清除了字符串,因此索引无效时?该代码在逻辑上是不正确的,但没有立即显示任何错误。如果我们继续尝试将第一个单词索引与空字符串一起使用,问题将在稍后出现。切片使此错误变得不可能,并且让我们知道我们的代码早期有问题。使用的分片版本first_word会引发编译时错误:
文件名:src / main.rs
fn main() {let mut s = String::from("hello world"); let word = first_word(&s); s.clear(); // error!println!("the first word is: {}", word); }

这是编译器错误:
$ cargo runCompiling 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 here17 | 18 |s.clear(); // error!|^^^^^^^^^ mutable borrow occurs here19 | 20 |println!("the first word is: {}", word); |---- immutable borrow later used hereerror: aborting due to previous errorFor more information about this error, try `rustc --explain E0502`.error: could not compile `ownership`.To learn more, run the command again with --verbose.

从借阅规则中回想起,如果我们对某事物具有不变的引用,那么我们也不能采用可变的引用。由于clear需要截断String,因此需要获取可变的引用。Rust不允许这样做,并且编译失败。Rust不仅使我们的API易于使用,而且还消除了编译时的一整类错误!
字符串文字是切片 回想一下,我们曾经讨论过将字符串文字存储在二进制文件中。现在我们了解了切片,我们可以正确理解字符串文字了:
let s = "Hello, world!";

s这里的类型是&str:它是指向二进制文件的特定点的切片。这也是字符串文字不可变的原因。&str是不可变的参考。
字符串切片作为参数 知道您可以使用一些文字和String值,这使我们在上有了另一个改进first_word,这就是它的签名:
fn first_word(s: &String) -> &str {

经验丰富的Rustacean会写清单4-9所示的签名,因为它使我们可以在&String值和&str值上使用相同的函数。
fn first_word(s: &str) -> &str {

清单4-9:first_word通过使用字符串切片作为s参数的类型来改进功能
如果我们有一个字符串切片,我们可以直接传递它。如果有一个String,我们可以传递整个切片String。定义一个函数以采用字符串切片而不是对a的引用String使我们的API更通用和有用,而不会丢失任何功能:
文件名:src / main.rs
fn main() {let my_string = String::from("hello world"); // first_word works on slices of `String`slet word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literalslet 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); }

其他切片 您可能会想到,字符串切片是特定于字符串的。但是,还有一种更通用的切片类型。考虑以下数组:
let a = [1, 2, 3, 4, 5];

就像我们可能要引用字符串的一部分一样,我们可能要引用数组的一部分。我们会这样:
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3];

该切片具有类型&[i32]。通过存储对第一个元素和长度的引用,它的工作方式与字符串切片相同。您将在所有其他集合中使用这种切片。在第8章中讨论向量时,我们将详细讨论这些集合。
总结 所有权,借用和切片的概念可确保在编译时Rust程序中的内存安全。Rust语言与其他系统编程语言一样,可让您控制内存使用情况,但是当数据所有者超出范围时,让数据所有者自动清除该数据意味着您不必编写和调试额外的代码获得此控制。
所有权会影响Rust的其他许多部分,因此在本书的其余部分中,我们将进一步讨论这些概念。让我们继续第5章,看看如何将数据分组在一起struct。

    推荐阅读