Rust Slice 類型

2023-03-22 15:09 更新
ch04-03-slices.md
commit a5e0c5b2c5f9054be3b961aea2c7edfeea591de8

slice 允許你引用集合中一段連續(xù)的元素序列,而不用引用整個集合。slice 是一類引用,所以它沒有所有權(quán)。

這里有一個編程小習(xí)題:編寫一個函數(shù),該函數(shù)接收一個用空格分隔單詞的字符串,并返回在該字符串中找到的第一個單詞。如果函數(shù)在該字符串中并未找到空格,則整個字符串就是一個單詞,所以應(yīng)該返回整個字符串。

讓我們推敲下如何不用 slice 編寫這個函數(shù)的簽名,來理解 slice 能解決的問題:

fn first_word(s: &String) -> ?

first_word 函數(shù)有一個參數(shù) &String。因為我們不需要所有權(quán),所以這沒有問題。不過應(yīng)該返回什么呢?我們并沒有一個真正獲取 部分 字符串的辦法。不過,我們可以返回單詞結(jié)尾的索引,結(jié)尾由一個空格表示。試試如示例 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 函數(shù)返回 String 參數(shù)的一個字節(jié)索引值

因為需要逐個元素的檢查 String 中的值是否為空格,需要用 as_bytes 方法將 String 轉(zhuǎn)化為字節(jié)數(shù)組:

    let bytes = s.as_bytes();

接下來,使用 iter 方法在字節(jié)數(shù)組上創(chuàng)建一個迭代器:

    for (i, &item) in bytes.iter().enumerate() {

我們將在第十三章詳細(xì)討論迭代器?,F(xiàn)在,只需知道 iter 方法返回集合中的每一個元素,而 enumerate 包裝了 iter 的結(jié)果,將這些元素作為元組的一部分來返回。enumerate 返回的元組中,第一個元素是索引,第二個元素是集合中元素的引用。這比我們自己計算索引要方便一些。

因為 enumerate 方法返回一個元組,我們可以使用模式來解構(gòu),我們將在第六章中進(jìn)一步討論有關(guān)模式的問題。所以在 for 循環(huán)中,我們指定了一個模式,其中元組中的 i 是索引而元組中的 &item 是單個字節(jié)。因為我們從 .iter().enumerate() 中獲取了集合元素的引用,所以模式中使用了 &。

在 for 循環(huán)中,我們通過字節(jié)的字面值語法來尋找代表空格的字節(jié)。如果找到了一個空格,返回它的位置。否則,使用 s.len() 返回字符串的長度:

        if item == b' ' {
            return i;
        }
    }

    s.len()

現(xiàn)在有了一個找到字符串中第一個單詞結(jié)尾索引的方法,不過這有一個問題。我們返回了一個獨立的 usize,不過它只在 &String 的上下文中才是一個有意義的數(shù)字。換句話說,因為它是一個與 String 相分離的值,無法保證將來它仍然有效??紤]一下示例 4-8 中使用了示例 4-7 中 first_word 函數(shù)的程序。

文件名: src/main.rs

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

    let word = first_word(&s); // word 的值為 5

    s.clear(); // 這清空了字符串,使其等于 ""

    // word 在此處的值仍然是 5,
    // 但是沒有更多的字符串讓我們可以有效地應(yīng)用數(shù)值 5。word 的值現(xiàn)在完全無效!
}

示例 4-8:存儲 first_word 函數(shù)調(diào)用的返回值并接著改變 String 的內(nèi)容

這個程序編譯時沒有任何錯誤,而且在調(diào)用 s.clear() 之后使用 word 也不會出錯。因為 word 與 s 狀態(tài)完全沒有聯(lián)系,所以 word 仍然包含值 5。可以嘗試用值 5 來提取變量 s 的第一個單詞,不過這是有 bug 的,因為在我們將 5 保存到 word 之后 s 的內(nèi)容已經(jīng)改變。

我們不得不時刻擔(dān)心 word 的索引與 s 中的數(shù)據(jù)不再同步,這很啰嗦且易出錯!如果編寫這么一個 second_word 函數(shù)的話,管理索引這件事將更加容易出問題。它的簽名看起來像這樣:

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

現(xiàn)在我們要跟蹤一個開始索引  一個結(jié)尾索引,同時有了更多從數(shù)據(jù)的某個特定狀態(tài)計算而來的值,但都完全沒有與這個狀態(tài)相關(guān)聯(lián)。現(xiàn)在有三個飄忽不定的不相關(guān)變量需要保持同步。

幸運的是,Rust 為這個問題提供了一個解決方法:字符串 slice。

字符串 slice

字符串 slicestring slice)是 String 中一部分值的引用,它看起來像這樣:

    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] 指定的 range 創(chuàng)建一個 slice,其中 starting_index 是 slice 的第一個位置,ending_index 則是 slice 最后一個位置的后一個值。在其內(nèi)部,slice 的數(shù)據(jù)結(jié)構(gòu)存儲了 slice 的開始位置和長度,長度對應(yīng)于 ending_index 減去 starting_index 的值。所以對于 let world = &s[6..11]; 的情況,world 將是一個包含指向 s 索引 6 的指針和長度值 5 的 slice。

圖 4-6 展示了一個圖例。

trpl04-06

圖 4-6:引用了部分 String 的字符串 slice

對于 Rust 的 .. range 語法,如果想要從索引 0 開始,可以不寫兩個點號之前的值。換句話說,如下兩個語句是相同的:

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

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

依此類推,如果 slice 包含 String 的最后一個字節(jié),也可以舍棄尾部的數(shù)字。這意味著如下也是相同的:

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

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

也可以同時舍棄這兩個值來獲取整個字符串的 slice。所以如下亦是相同的:

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

let len = s.len();

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

注意:字符串 slice range 的索引必須位于有效的 UTF-8 字符邊界內(nèi),如果嘗試從一個多字節(jié)字符的中間位置創(chuàng)建字符串 slice,則程序?qū)蝈e誤而退出。出于介紹字符串 slice 的目的,本部分假設(shè)只使用 ASCII 字符集;第八章的 “使用字符串存儲 UTF-8 編碼的文本” 部分會更加全面的討論 UTF-8 處理問題。

在記住所有這些知識后,讓我們重寫 first_word 來返回一個 slice?!白址?slice” 的類型聲明寫作 &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 相同的方式獲取單詞結(jié)尾的索引,通過尋找第一個出現(xiàn)的空格。當(dāng)找到一個空格,我們返回一個字符串 slice,它使用字符串的開始和空格的索引作為開始和結(jié)束的索引。

現(xiàn)在當(dāng)調(diào)用 first_word 時,會返回與底層數(shù)據(jù)關(guān)聯(lián)的單個值。這個值由一個 slice 開始位置的引用和 slice 中元素的數(shù)量組成。

second_word 函數(shù)也可以改為返回一個 slice:

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

現(xiàn)在我們有了一個不易混淆且直觀的 API 了,因為編譯器會確保指向 String 的引用持續(xù)有效。還記得示例 4-8 程序中,那個當(dāng)我們獲取第一個單詞結(jié)尾的索引后,接著就清除了字符串導(dǎo)致索引就無效的 bug 嗎?那些代碼在邏輯上是不正確的,但卻沒有顯示任何直接的錯誤。問題會在之后嘗試對空字符串使用第一個單詞的索引時出現(xiàn)。slice 就不可能出現(xiàn)這種 bug 并讓我們更早的知道出問題了。使用 slice 版本的 first_word 會拋出一個編譯時錯誤:

文件名: src/main.rs

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

    let word = first_word(&s);

    s.clear(); // 錯誤!

    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` due to previous error

回憶一下借用規(guī)則,當(dāng)擁有某值的不可變引用時,就不能再獲取一個可變引用。因為 clear 需要清空 String,它嘗試獲取一個可變引用。在調(diào)用 clear 之后的 println! 使用了 word 中的引用,所以這個不可變的引用在此時必須仍然有效。Rust 不允許 clear 中的可變引用和 word 中的不可變引用同時存在,因此編譯失敗。Rust 不僅使得我們的 API 簡單易用,也在編譯時就消除了一整類的錯誤!

字符串字面值就是 slice

還記得我們講到過字符串字面值被儲存在二進(jìn)制文件中嗎?現(xiàn)在知道 slice 了,我們就可以正確地理解字符串字面值了:

let s = "Hello, world!";

這里 s 的類型是 &str:它是一個指向二進(jìn)制程序特定位置的 slice。這也就是為什么字符串字面值是不可變的;&str 是一個不可變引用。

字符串 slice 作為參數(shù)

在知道了能夠獲取字面值和 String 的 slice 后,我們對 first_word 做了改進(jìn),這是它的簽名:

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

而更有經(jīng)驗的 Rustacean 會編寫出示例 4-9 中的簽名,因為它使得可以對 &String 值和 &str 值使用相同的函數(shù):

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

示例 4-9: 通過將 s 參數(shù)的類型改為字符串 slice 來改進(jìn) first_word 函數(shù)

如果有一個字符串 slice,可以直接傳遞它。如果有一個 String,則可以傳遞整個 String 的 slice 或?qū)?nbsp;String 的引用。這種靈活性利用了 deref coercions 的優(yōu)勢,這個特性我們將在“函數(shù)和方法的隱式 Deref 強(qiáng)制轉(zhuǎn)換”章節(jié)中介紹。定義一個獲取字符串 slice 而不是 String 引用的函數(shù)使得我們的 API 更加通用并且不會丟失任何功能:

文件名: src/main.rs

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

    // `first_word` 適用于 `String`(的 slice),整體或全部
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` 也適用于 `String` 的引用,
    // 這等價于整個 `String` 的 slice
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` 適用于字符串字面值,整體或全部
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // 因為字符串字面值已經(jīng) **是** 字符串 slice 了,
    // 這也是適用的,無需 slice 語法!
    let word = first_word(my_string_literal);
}

其他類型的 slice

字符串 slice,正如你想象的那樣,是針對字符串的。不過也有更通用的 slice 類型??紤]一下這個數(shù)組:

let a = [1, 2, 3, 4, 5];

就跟我們想要獲取字符串的一部分那樣,我們也會想要引用數(shù)組的一部分。我們可以這樣做:

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);

這個 slice 的類型是 &[i32]。它跟字符串 slice 的工作方式一樣,通過存儲第一個集合元素的引用和一個集合總長度。你可以對其他所有集合使用這類 slice。第八章講到 vector 時會詳細(xì)討論這些集合。

總結(jié)

所有權(quán)、借用和 slice 這些概念讓 Rust 程序在編譯時確保內(nèi)存安全。Rust 語言提供了跟其他系統(tǒng)編程語言相同的方式來控制你使用的內(nèi)存,但擁有數(shù)據(jù)所有者在離開作用域后自動清除其數(shù)據(jù)的功能意味著你無須額外編寫和調(diào)試相關(guān)的控制代碼。

所有權(quán)系統(tǒng)影響了 Rust 中很多其他部分的工作方式,所以我們還會繼續(xù)講到這些概念,這將貫穿本書的余下內(nèi)容。讓我們開始第五章,來看看如何將多份數(shù)據(jù)組合進(jìn)一個 struct 中。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號