切片类型

Slice 允许您引用集合中的连续元素序列,而不是整个集合。一个 slice 是一种引用,因此它没有所有权。

这里有一个小编程问题:编写一个函数,它接受一个字符串 words 中用空格分隔,并返回在该字符串中找到的第一个单词。 如果函数在字符串中找不到空格,则整个字符串必须为 一个单词,因此应返回整个字符串。

让我们来看看如何在不使用 slices 来理解 slices 将解决的问题:

fn first_word(s: &String) -> ?

该函数具有 a as 参数。我们不想 所有权,所以这很好。但是我们应该返回什么呢?我们真的没有 来谈论字符串的一部分。但是,我们可以返回 单词的结尾,由空格表示。让我们试一试,如示例 4-7 所示。first_word&String

文件名: 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:将字节索引值返回到参数中的函数first_wordString

因为我们需要逐个元素遍历并检查 一个值是一个空格,我们将使用该方法将 THE VALUE 转换为字节数组。StringStringas_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 的第一个元素是索引,第二个元素是对元素的引用。 这比我们自己计算指数要方便一些。iterenumerateiterenumerate

因为该方法返回一个元组,所以我们可以使用 patterns 来 解构该元组。我们将在 Chapter 中详细讨论模式 6. 在循环中,我们指定一个模式,该模式具有元组中的索引和元组中的单个字节。 因为我们从 中获取对元素的引用,所以我们在模式中使用。enumeratefori&item.iter().enumerate()&

在循环中,我们搜索表示空格的字节 使用 byte literal 语法。如果我们找到一个空格,我们返回位置。 否则,我们使用 .fors.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&StringStringfirst_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_wordString

该程序编译时没有任何错误,如果我们在调用 .因为 根本没有连接到状态,所以仍然包含值 。我们可以将该值与 该变量尝试提取第一个单词,但这将是一个错误 因为自从我们保存在 中以来,内容已经发生了变化。words.clear()wordsword55ss5word

不得不担心索引与数据不同步是乏味且容易出错的!如果 我们编写一个函数。它的签名必须如下所示:wordssecond_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 。因此,在 的情况下,将是一个包含指向 的字节,长度值为 。StringhelloString[0..5][starting_index..ending_index]starting_indexending_indexending_indexstarting_indexlet world = &s[6..11];worlds5

图 4-7 在图表中显示了这一点。

三表:一张表,表示 s 的堆栈数据,其中指向
到字符串 data “Hello world” 的表中索引 0 处的字节
堆。第三个表 rep-resent 了 slice world 的堆栈数据,其中
的长度值为 5,并指向堆数据表的字节 6。

图 4-7:引用 String 的一部分的 String 切片

使用 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

文件名: 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 中相同,通过 查找 Space 的第一个匹配项。当我们找到一个空格时,我们返回一个 string 切片,使用字符串的开头和空格的索引作为 起始索引和结束索引。

现在,当我们调用 时,我们会返回一个与 基础数据。该值由对 切片和切片中的元素数。first_word

返回 slice 也适用于函数:second_word

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

我们现在有一个简单的 API,它更难搞砸,因为 编译器将确保对 的引用保持有效。记得 示例 4-8 中程序中的 bug,当我们获取索引到 第一个单词,但随后清除了字符串,所以我们的索引无效?该代码是 逻辑上不正确,但没有立即显示任何错误。问题会 如果我们一直尝试使用第一个单词索引和 emptyTed,则稍后显示 字符串。切片使这个错误变得不可能,并让我们知道我们有一个问题 我们的代码要快得多。使用 slice 版本的 将抛出一个 编译时错误:Stringfirst_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

回想一下借用规则,如果我们有一个对 something,我们也不能采用可变引用。因为需要 truncate 的 ,它需要获取一个可变的引用。之后的 to 调用使用 中的 引用 ,因此不可变的 此时 reference 必须仍处于 active 状态。Rust 不允许 mutable reference in 和不可变的 reference in from 存在于 同时,编译失败。Rust 不仅使我们的 API 更易于使用, 但它也消除了编译时的一整类错误!clearStringprintln!clearwordclearword

字符串文本作为切片

回想一下,我们讨论了存储在二进制文件中的字符串 Literals。现在 了解了 slices,我们可以正确理解字符串字面量:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

这里的类型是 :它是一个指向 的特定点的切片 二进制。这也是字符串 Literals 不可变的原因; 是一个 immutable 引用。s&str&str

字符串切片作为参数

知道你可以获取 Literals 和 Value 的切片,我们就可以 对 的另一项改进,那就是它的签名:Stringfirst_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 作为参数类型来改进函数first_words

如果我们有一个字符串 slice,我们可以直接传递它。如果我们有一个 ,我们 可以将 的切片 或 引用传递给 。这 灵活性利用了 Deref 强制转换,我们将在“使用 Functions 和 方法“部分。StringStringString

定义一个函数来获取字符串 slice 而不是对 a 的引用,使我们的 API 更加通用和有用,而不会丢失任何功能:String

文件名: 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);
}

其他切片

正如您可能想象的那样,字符串切片是特定于字符串的。但是有一个 更通用的切片类型也是如此。考虑这个数组:

#![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/)为准