走进Rust:切片

Rust大约 5674 字

切片

另一种没有所有权的数据类型是切片。切片使你可以引用集合中连续的元素序列,而不是整个集合。

这是一个小的编程问题:编写一个函数,该函数需要一个字符串并返回在该字符串中找到的第一个单词。如果函数在字符串中找不到空格,则整个字符串必须是一个单词,因此应返回整个字符串。

让我们考虑一下此函数的签名:

fn first_word(s: &String) -> ?

函数first_word&String作为参数。我们不需要所有权,所以这样传参很好。但是,我们应该返回什么呢?我们真的没有办法谈论字符串的一部分。但是,我们可以返回单词结尾的索引。

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()
}

因为我们需要逐个检查String元素,并检查值是否有空格,所以我们将使用as_bytes方法将String转换为字节数组:

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,但是只有在在&String的上下文中才是一个有意义的数字。换句话说,由于它是与字符串分离的值,因此无法保证该值将来仍然有效。考虑以下场景:

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word 变量的值是 5

    s.clear(); // 清空字符串,使变量 s 等于 ""

    // 这一步,word 变量仍然等于 5,但是没有字符串让我们可以使用这个 5 了。
    // word 变量现在等于说是完全无效了。
}

该程序的编译没有任何错误,如果在调用s.clear()之后使用word,也没有问题。由于words的状态没有关联,因此word仍然包含值5。我们可以将值5与变量s一起使用以尝试提取第一个单词,但这将是一个错误,因为自从我们在单词中保存5以来s的内容已更改。

不得不担心word中的索引值与s中的数据不同步,这既繁琐又容易出错!如果我们编写second_word函数,则管理这些索引的难度更大。其签名必须如下所示:

fn second_word(s: &String) -> (usize, usize) {

现在,我们正在跟踪起始索引和结束索引,并且我们有更多的值是根据特定状态下的数据计算得出的,但与该状态完全无关。现在,我们有三个不相关的变量需要保持同步。

幸运的是,Rust解决了这个问题:字符串切片。

字符串切片

字符串切片是对字符串一部分的引用,它看起来像这样:

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];
    let world = &s[6..11];
}

这类似于引用整个String,但具有额外的[0..5]位。它不是对整个String的引用,而是对String的一部分的引用。

通过指定[开始索引..结束索引],我们可以使用方括号内的范围来创建切片,其中,开始索引是切片中的第一个位置,而结束索引比切片中的最后一个位置大1。在内部,切片数据结构存储切片的起始位置和长度,它对应于结束索引减去开始索引。因此,在let world =&s [6..11];的情况下,world将是一个切片,其中包含指向s的第7个字节(从1开始)的指针,其长度值为5。

trpl0406.png

使用Rust..范围语法,如果要从第一个索引(零)开始,则可以省略两个句点之前的值。换句话说,这些是相等的:

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

同样,如果你的分片包含字符串的最后一个字节,则可以省略两个句点之后的值。这意味着这些是相等的:

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码。第8章的“存储带字符串的UTF-8编码文本”部分对UTF-8处理进行了更全面的讨论。

记住这些信息后,让我们重写first_word以返回切片。 表示“字符串切片”的类型写为&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[..]
}

我们像之前代码那样,判断空格出现的索引位置。找到空格后,我们将以字符串的开头和空格的索引作为开始索引和结束索引来返回字符串切片。

现在,当我们调用first_word时,我们将获得与基础数据相关的单个值。该值由对切片起点的引用以及切片中元素的数量组成。

返回切片也可以用于second_word函数:

fn second_word(s: &String) -> &str {

现在,我们有了一个简单的API,很难将其弄乱,因为编译器将确保对String的引用仍然有效。还记得之前程序中的错误吗?当我们将索引移到第一个单词的末尾但又清除了字符串,使得我们的索引无效?该代码在逻辑上不正确,但没有立即显示任何错误。如果我们继续尝试将第一个单词索引与空字符串一起使用,则问题将在稍后出现。切片使此错误变得不可能,并且让我们提早知道我们的代码有问题。使用first_word的切片版本将引发编译时错误:

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

error: aborting due to previous error

For 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是不可变的引用。

字符串切片作为参数

知道你可以采用文字和字符串值的切片,这使我们对first_word进行了另一项改进,以下是原先的签名:

fn first_word(s: &String) -> &str {

经验丰富的Rustacean会写以下的签名,因为它允许我们在&String值和&str值上使用相同的函数。

fn first_word(s: &str) -> &str {

如果我们有一个字符串切片,我们可以直接传递它。如果我们有一个String,我们可以传递整个String的一部分。定义函数以采用字符串切片而不是对String的引用使我们的API更通用和有用,而不会丢失任何功能:

fn main() {
    let my_string = String::from("hello world");

    // first_word 传入 `String` s 的切片
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word 传入字符串文字的切片
    let word = first_word(&my_string_literal[..]);

    // 因为字符串文字已经是字符串切片了,所以不需要切片语法了
    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章中讨论vectors时,将详细讨论这些集合。

总结

所有权,借用和切片的概念可确保在编译时Rust程序中的内存安全。Rust语言与其他系统编程语言一样,可让你控制内存使用情况,但是当数据所有者超出作用域时,让数据所有者自动清除该数据意味着你不必编写和调试额外的代码 获得此控制。

所有权会影响Rust的其他许多部分,因此在本书的其余部分中,我们将进一步讨论这些概念。

阅读 164 · 发布于 2020-07-07

————        END        ————

扫描下方二维码关注公众号和小程序↓↓↓

昵称:
随便看看换一批