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(string 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 展示了一個圖例。
圖 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 簡單易用,也在編譯時就消除了一整類的錯誤!
還記得我們講到過字符串字面值被儲存在二進(jìn)制文件中嗎?現(xiàn)在知道 slice 了,我們就可以正確地理解字符串字面值了:
let s = "Hello, world!";
這里 s
的類型是 &str
:它是一個指向二進(jìn)制程序特定位置的 slice。這也就是為什么字符串字面值是不可變的;&str
是一個不可變引用。
在知道了能夠獲取字面值和 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 類型??紤]一下這個數(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ì)討論這些集合。
所有權(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
中。
更多建議: