所有權
所有權(系統)是 Rust 最為與眾不同的特性,它讓 Rust 無需垃圾回收器(garbage collector)即可保證內存安全。因此,理解 Rust 中所有權的運作方式非常重要。
這裏是非常重非常重的一個知識點,這裏一定要打好基礎.
因為所有權對很多開發者來説都是一個新概念,需要一些時間來適應。好消息是隨着你對 Rust 和所有權系統的規則越來越有經驗,你就越能自然地編寫出安全和高效的代碼。持之以恆!
堆(Heap)與棧(Stack)
在很多語言中,你並不需要經常考慮到棧與堆。不過在像 Rust 這樣的系統編程語言中,值是位於棧上還是堆上在很大程度上影響了語言的行為以及為何必須做出這樣的抉擇.
棧和堆都是代碼在運行時可供使用的內存,但是它們的結構不同。棧以放入值的順序存儲值並以相反順序取出值。這也被稱作 後進先出(last in, first out)。想象一下一疊盤子:當增加更多盤子時,把它們放在盤子堆的頂部,當需要盤子時,也從頂部拿走。不能從中間也不能從底部增加或拿走盤子!增加數據叫做 進棧(pushing onto the stack),而移出數據叫做 出棧(popping off the stack)
棧中的所有數據都必須佔用已知且固定的大小。在編譯時大小未知或大小可能變化的數據,要改為存儲在堆上。堆是缺乏組織的:當向堆放入數據時,你要請求一定大小的空間。內存分配器(memory allocator)在堆的某處找到一塊足夠大的空位,把它標記為已使用,並返回一個表示該位置地址的 指針(pointer)。這個過程稱作 在堆上分配內存(allocating on the heap),有時簡稱為 “分配”(allocating)。將數據推入棧中並不被認為是分配。因為指針的大小是已知並且固定的,你可以將指針存儲在棧上,不過當需要實際數據時,必須訪問指針。
入棧比在堆上分配內存要快,因為(入棧時)分配器無需為存儲新數據去搜索內存空間;其位置總是在棧頂。相比之下,在堆上分配內存則需要更多的工作,這是因為分配器必須首先找到一塊足夠存放數據的內存空間,並接着做一些記錄為下一次分配做準備。
當你的代碼調用一個函數時,傳遞給函數的值(包括可能指向堆上數據的指針)和函數的局部變量被壓入棧中。當函數結束時,這些值被移出棧。
跟蹤哪部分代碼正在使用堆上的哪些數據,最大限度地減少堆上的重複數據量,以及清理堆上不再使用的數據確保不會耗盡空間,這些問題正是所有權系統要處理的。一旦理解了所有權,你就不需要經常考慮棧和堆了,不過明白了所有權的存在就是為了管理堆數據,能夠幫助解釋為什麼所有權要以這種方式工作。
所有權規則
- Rust 中的每一個值都有一個被稱為其 所有者(owner)的變量。
- 值在任一時刻有且只有一個所有者。
- 當所有者(變量)離開作用域,這個值將被丟棄。
變量作用域
在所有權的第一個例子中,我們看看一些變量的 作用域(scope)。作用域是一個項(item)在程序中有效的範圍
fn main() {
{ // s 在這裏無效, 它尚未聲明
let s = "hello"; // 從此處起,s 開始有效
// 使用 s
} // 此作用域已結束,s 不再有效
}
換句話説,這裏有兩個重要的時間點,當 s 進入作用域 時,它就是有效的,這一直持續到它 離開作用域 為止。
String類型
前面介紹的類型都是已知大小的,可以存儲在棧中,並且當離開作用域時被移出棧,如果代碼的另一部分需要在不同的作用域中使用相同的值,可以快速簡單地複製它們來創建一個新的獨立實例。不過我們需要尋找一個存儲在堆上的數據來探索 Rust 是如何知道該在何時清理數據的。
我們已經見過字符串字面量,即被硬編碼進程序裏的字符串值。字符串字面量是很方便的,不過它們並不適合使用文本的每一種場景。原因之一就是它們是不可變的。另一個原因是並非所有字符串的值都能在編寫代碼時就知道:例如,要是想獲取用户輸入並存儲該怎麼辦呢?為此,Rust 有第二個字符串類型,String。這個類型管理被分配到堆上的數據,所以能夠存儲在編譯時未知大小的文本。可以使用 from 函數基於字符串字面量來創建 String,如下:
let s = String::from("hello");
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串後追加字面值
println!("{}", s); // 將打印 `hello, world!`
為什麼 String 可變而字面量卻不行呢?區別在於兩個類型對內存的處理上
內存與分配
就字符串字面量來説,我們在編譯時就知道其內容,所以文本被直接硬編碼進最終的可執行文件中。這使得字符串字面量快速且高效。不過這些特性都只得益於字符串字面量的不可變性。不幸的是,我們不能為了每一個在編譯時大小未知的文本而將一塊內存放入二進制文件中,並且它的大小還可能隨着程序運行而改變。
對於 String 類型,為了支持一個可變,可增長的文本片段,需要在堆上分配一塊在編譯時未知大小的內存來存放內容。這意味着:
- 必須在運行時向內存分配器請求內存。
- 需要一個當我們處理完 String 時將內存返回給分配器的方法。
第一部分由我們完成:當調用 String::from 時,它的實現(implementation)請求其所需的內存。這在編程語言中是非常通用的。
然而,第二部分實現起來就各有區別了。在有 垃圾回收(garbage collector,GC)的語言中, GC 記錄並清除不再使用的內存,而我們並不需要關心它。沒有 GC 的話,識別出不再使用的內存並調用代碼顯式釋放就是我們的責任了,跟請求內存的時候一樣。從歷史的角度上説正確處理內存回收曾經是一個困難的編程問題。如果忘記回收了會浪費內存。如果過早回收了,將會出現無效變量。如果重複回收,這也是個 bug。我們需要精確地為一個 allocate 配對一個 free。
Rust 採取了一個不同的策略:內存在擁有它的變量離開作用域後就被自動釋放。
{
let s = String::from("hello"); // 從此處起,s 開始有效
// 使用 s
} // 此作用域已結束,
// s 不再有效
這是一個將 String 需要的內存返回給分配器的很自然的位置:當 s 離開作用域的時候。當變量離開作用域,Rust 為我們調用一個特殊的函數。這個函數叫做 drop,在這裏 String 的作者可以放置釋放內存的代碼。Rust 在結尾的 } 處自動調用 drop。
變量與數據交互的方式一 移動
在 Rust 中,多個變量能夠以不同的方式與同一數據交互
let x = 5;
let y = x;
將 5 綁定到 x;接着生成一個值 x 的拷貝並綁定到 y”。現在有了兩個變量,x 和 y,都等於 5,因為整數是有已知固定大小的簡單值,所以這兩個 5 被放入了棧中。
let s1 = String::from("hello");
let s2 = s1;
String 由三部分組成,如圖所示:一個指向存放字符串內容內存的指針,一個長度,和一個容量。這一組數據存儲在棧上。右側則是堆上存放內容的內存部分。
長度表示 String 的內容當前使用了多少字節的內存。容量是 String 從分配器總共獲取了多少字節的內存。長度與容量的區別是很重要的,不過在當前上下文中並不重要,所以現在可以忽略容量。
我們將 s1 賦值給 s2,String 的數據被複制了,這意味着我們從棧上拷貝了它的指針、長度和容量。我們並沒有複製指針指向的堆上數據。如圖:
之前我們提到過當變量離開作用域後,Rust 自動調用 drop 函數並清理變量的堆內存。不過兩個數據指針指向了同一位置。這就有了一個問題:當 s2 和 s1 離開作用域,他們都會嘗試釋放相同的內存。這是一個叫做 二次釋放(double free)的錯誤,也是之前提到過的內存安全性 bug 之一。兩次釋放(相同)內存會導致內存污染,它可能會導致潛在的安全漏洞。
為了確保內存安全,這種場景下 Rust 的處理有另一個細節值得注意。在 let s2 = s1 之後,Rust 認為 s1 不再有效,因此 Rust 不需要在 s1 離開作用域後清理任何東西。
如果你在其他語言中聽説過術語 淺拷貝(shallow copy)和 深拷貝(deep copy),那麼拷貝指針、長度和容量而不拷貝數據可能聽起來像淺拷貝。不過因為 Rust 同時使第一個變量無效了,這個操作被稱為 移動(move),而不是淺拷貝。上面的例子可以解讀為 s1 被 移動 到了 s2 中。
這樣就解決了我們的問題!因為只有 s2 是有效的,當其離開作用域,它就釋放自己的內存,完畢。
Rust 不會自動進行深拷貝:這意味着當你在 Rust 中進行某些操作時(如賦值、函數參數傳遞等),默認情況下不會進行深拷貝。相反,它會進行淺拷貝或者直接移動數據的所有權。
由於 Rust 不會自動進行深拷貝,任何自動的複製操作通常只涉及簡單的內存複製(即淺拷貝或移動語義)。這種設計對運行時性能有以下好處:
- 低開銷:淺拷貝或移動語義的開銷非常小,因為它只需要複製少量的元數據(如指針),而不需要遞歸地複製整個數據結構。
- 避免不必要的複雜性:深拷貝可能會帶來額外的複雜性和潛在的性能問題,尤其是在處理大型數據結構時。通過避免自動深拷貝,Rust 確保了更高的性能和更清晰的所有權模型。
變量與數據的交互方式二 克隆
如果我們 確實 需要深度複製 String 中堆上的數據,而不僅僅是棧上的數據,可以使用一個叫做 clone 的通用函數。
let s1 = String::from("hello");
let s2 = s1.clone();
當出現 clone 調用時,你知道一些特定的代碼被執行而且這些代碼可能相當消耗資源。你很容易察覺到一些不尋常的事情正在發生。
只在棧上的數據 拷貝
let x = 5;
let y = x;
但這段代碼似乎與我們剛剛學到的內容相矛盾:沒有調用 clone,不過 x 依然有效且沒有被移動到 y 中。
原因是像整型這樣的在編譯時已知大小的類型被整個存儲在棧上,所以拷貝其實際的值是快速的。這意味着沒有理由在創建變量 y 後使 x 無效。換句話説,這裏沒有深淺拷貝的區別,所以這裏調用 clone 並不會與通常的淺拷貝有什麼不同,我們可以不用管它。
Rust 有一個叫做 Copy trait 的特殊標註,可以用在類似整型這樣的存儲在棧上的類型上。如果一個類型實現了 Copy trait,那麼一箇舊的變量在將其賦值給其他變量後仍然可用。Rust 不允許自身或其任何部分實現了 Drop trait 的類型使用 Copy trait。如果我們對其值離開作用域時需要特殊處理的類型使用 Copy 標註,將會出現一個編譯時錯誤。要學習如何為你的類型添加 Copy 標註以實現該 trait,請閲讀附錄 C 中的 “可派生的 trait”。
那麼哪些類型實現了 Copy trait 呢?你可以查看給定類型的文檔來確認,不過作為一個通用的規則,任何一組簡單標量值的組合都可以實現 Copy,任何不需要分配內存或某種形式資源的類型都可以實現 Copy 。如下是一些 Copy 的類型:
- 所有整數類型,比如 u32。
- 布爾類型,bool,它的值是 true 和 false。
- 所有浮點數類型,比如 f64。
- 字符類型,char。
- 元組,當且僅當其包含的類型也都實現 Copy 的時候。比如,(i32, i32) 實現了 Copy,但 (i32, String) 就沒有。
所有權與函數
將值傳遞給函數在語義上與給變量賦值相似。向函數傳遞值可能會移動或者複製,就像賦值語句一樣。
fn main() {
let s = String::from("hello"); // s 進入作用域
takes_ownership(s); // s 的值移動到函數裏 ...
// ... 所以到這裏不再有效
let x = 5; // x 進入作用域
makes_copy(x); // x 應該移動函數裏,
// 但 i32 是 Copy 的,所以在後面可繼續使用 x
} // 這裏, x 先移出了作用域,然後是 s。但因為 s 的值已被移走,
// 所以不會有特殊操作
fn takes_ownership(some_string: String) { // some_string 進入作用域
println!("{}", some_string);
} // 這裏,some_string 移出作用域並調用 `drop` 方法。佔用的內存被釋放
fn makes_copy(some_integer: i32) { // some_integer 進入作用域
println!("{}", some_integer);
} // 這裏,some_integer 移出作用域。不會有特殊操作
返回值與作用域
fn main() {
let s1 = gives_ownership(); // gives_ownership 將返回值
// 移給 s1
let s2 = String::from("hello"); // s2 進入作用域
let s3 = takes_and_gives_back(s2); // s2 被移動到
// takes_and_gives_back 中,
// 它也將返回值移給 s3
} // 這裏, s3 移出作用域並被丟棄。s2 也移出作用域,但已被移走,
// 所以什麼也不會發生。s1 移出作用域並被丟棄
fn gives_ownership() -> String { // gives_ownership 將返回值移動給
// 調用它的函數
let some_string = String::from("yours"); // some_string 進入作用域
some_string // 返回 some_string 並移出給調用的函數
}
// takes_and_gives_back 將傳入字符串並返回該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入作用域
a_string // 返回 a_string 並移出給調用的函數
}
轉移返回值的所有權
變量的所有權總是遵循相同的模式:將值賦給另一個變量時移動它。當持有堆中數據值的變量離開作用域時,其值將通過 drop 被清理掉,除非數據被移動為另一個變量所有。
在每一個函數中都獲取所有權並接着返回所有權有些囉嗦。如果我們想要函數使用一個值但不獲取所有權該怎麼辦呢?如果我們還要接着使用它的話,每次都傳進去再返回來就有點煩人了,除此之外,我們也可能想返回函數體中產生的一些數據。
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的長度
(s, length)
}
但是這未免有些形式主義,而且這種場景應該很常見。幸運的是,Rust 對此提供了一個功能,叫做 引用.
引用於借用
下面是如何定義並使用一個(新的)calculate_length 函數,它以一個對象的引用作為參數而不是獲取值的所有權:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
首先,注意變量聲明和函數返回值中的所有元組代碼都消失了。其次,注意我們傳遞 &s1 給 calculate_length,同時在函數定義中,我們獲取 &String 而不是 String。
這個 & 符號就是 引用,它們允許你使用值但不獲取其所有權。下圖展示了一張示意圖
與使用 & 引用相反的操作是 解引用(dereferencing),它使用解引用運算符,*
let s1 = String::from("hello");
let len = calculate_length(&s1);
&s1 語法讓我們創建一個 指向 值 s1 的引用,但是並不擁有它。因為並不擁有這個值,所以當引用停止使用時,它所指向的值也不會被丟棄。
同理,函數簽名使用 & 來表明參數 s 的類型是一個引用。讓我們增加一些解釋性的註釋:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
// s 是對 String 的引用
fn calculate_length(s: &String) -> usize {
s.len()
} // 這裏,s 離開了作用域。但因為它並不擁有引用值的所有權,
// 所以什麼也不會發生
變量 s 有效的作用域與函數參數的作用域一樣,不過當引用停止使用時並不丟棄它指向的數據,因為我們沒有所有權。當函數使用引用而不是實際值作為參數,無需返回值來交還所有權,因為就不曾擁有所有權。
我們將創建一個引用的行為稱為 借用(borrowing)。正如現實生活中,如果一個人擁有某樣東西,你可以從他那裏借來。當你使用完畢,必須還回去。
正如變量在默認情況下是不可變的一樣,引用也是不可變的。我們無法通過引用修改內容。
可變引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
首先,我們必須將 s 改為 mut。然後必須在調用 change 函數的地方創建一個可變引用 &mut s,並更新函數簽名以接受一個可變引用 some_string: &mut String。這就非常清楚地表明,change 函數將改變它所借用的值。
不過可變引用有一個很大的限制:在同一時間,只能有一個對某一特定數據的可變引用。嘗試創建兩個可變引用的代碼將會失敗:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
這個報錯説這段代碼是無效的,因為我們不能在同一時間多次將 s 作為可變變量借用。第一個可變的借入在 r1 中,並且必須持續到在 println! 中使用它,但是在那個可變引用的創建和它的使用之間,我們又嘗試在 r2 中創建另一個可變引用,它借用了與 r1 相同的數據。
止同一時間對同一數據進行多個可變引用的限制允許可變性,不過是以一種受限制的方式允許。新 Rustacean 們經常難以適應這一點,因為大部分語言中變量任何時候都是可變的。
這個限制的好處是 Rust 可以在編譯時就避免數據競爭。數據競爭(data race)類似於競態條件,它由這三個行為造成:
- 兩個或更多指針同時訪問同一數據。
- 至少有一個指針被用來寫入數據。
- 沒有同步數據訪問的機制
注意以上三個行為同時發生才會造成數據競爭.
數據競爭會導致未定義行為,難以在運行時追蹤,並且難以診斷和修復;Rust 避免了這種情況的發生,因為它甚至不會編譯存在數據競爭的代碼!
我們再來看兩個例子
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在這裏離開了作用域,所以我們完全可以創建一個新的引用
let r2 = &mut s;
}
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 沒問題
let r2 = &s; // 沒問題
let r3 = &mut s; // 大問題
println!("{}, {}, and {}", r1, r2, r3);
}
rust允許擁有多個可變引用,只是不能同時擁有,這裏是相對一個可變變量來説的如第一個例子;
第二個例子告訴我們:
同時使用可變與不可變引用,因為這樣會導致r3直接影響了r1和r2的值,因為你一個不小心操作了r3那就導致了麻煩;那麼要怎樣解決上面的問題呢,直接將let r3 = &mut s;移動到println!之後,並去掉println!上的打印輸出r3,這樣程序就沒有問題了,因為r1,r2都不操作實際的只,他只是相當與一個觀察者,當這兩個觀察者使用完畢換回去以後我們在使用可變引用就沒問題了,如下代碼.
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 沒問題
let r2 = &s; // 沒問題
println!("{}, {}", r1, r2);
let r3 = &mut s; // 沒問題
println!("{}", r3);
}
在這裏我們也可以看得出來多個不可變引用是沒問題的,當然了這個值你們誰願意看就看唄反正你也不能改變我的值.多個不可變引用是可以的,因為沒有哪個只能讀取數據的人有能力影響其他人讀取到的數據。
懸垂引用
在具有指針的語言中,很容易通過釋放內存時保留指向它的指針而錯誤地生成一個 懸垂指針(dangling pointer),所謂懸垂指針是其指向的內存可能已經被分配給其它持有者。相比之下,在 Rust 中編譯器確保引用永遠也不會變成懸垂狀態:當你擁有一些數據的引用,編譯器確保數據不會在其引用之前離開作用域。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
這裏是有錯誤的,因為 s 是在 dangle 函數內創建的,當 dangle 的代碼執行完畢後,s 將被釋放。不過我們嘗試返回它的引用。這意味着這個引用會指向一個無效的 String,這可不對!Rust 不會允許我們這麼做。
解決辦法是我們直接將String返回去,也就是直接將所有權轉移,如下:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
引用的規則
- 在任意時間,要麼擁有一個可變引用,要麼擁有多個不可變引用(針對單一個變量)
- 引用必須總是有效的
切片Slice類型
另一個沒有所有權的數據類型是 slice。slice 允許你引用集合中一段連續的元素序列,而不用引用整個集合。
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()
}
我們來説一下這個函數的功能,首先從函數簽名説起,函數名為first_word接受一個字符串的引用當做參數返回一個usize,然後我們在説一下函數具體的實現功能,功能方面首先使用as_bytes將字符串轉換成一個字節數組,並且給到bytes下面就開始進入到for循環,這個循環是用來便利這個字符數組的,首先使用iter()是一個迭代器返回字符數字中的每一個元素,enumerate()包裝迭代器返回的結果,將這個返回結果當做元組的一部分,然後返回結果第一個勻速是元組的索引,第二元素是集合中元素的引用.也就是對應到我們的i以及&item;當我們進入到循環體重,對所有元素的引用進行判斷,這個元素是否是空格,如果是空格則返回這個元素的索引.最後將字符串的長度返回即s.len()
現在有了一個找到字符串中第一個單詞結尾索引的方法,不過這有一個問題。我們返回了一個獨立的 usize,不過它只在 &String 的上下文中才是一個有意義的數字。換句話説,因為它是一個與 String 相分離的值,無法保證將來它仍然有效。
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word 的值為 5
s.clear(); // 這清空了字符串,使其等於 ""
// word 在此處的值仍然是 5,
// 但是沒有更多的字符串讓我們可以有效地應用數值 5。word 的值現在完全無效!
}
這個程序編譯時沒有任何錯誤,而且在調用 s.clear() 之後使用 word 也不會出錯。因為 word 與 s 狀態完全沒有聯繫,所以 word 仍然包含值 5。可以嘗試用值 5 來提取變量 s 的第一個單詞,不過這是有 bug 的,因為在我們將 5 保存到 word 之後 s 的內容已經改變。
幸運的是,Rust 為這個問題提供了一個解決方法:字符串 slice。
字符串 slice
字符串 slice(string slice)是 String 中一部分值的引用,
fn main() {
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] 指定的 range 創建一個 slice,其中 starting_index 是 slice 的第一個位置,ending_index 則是 slice 最後一個位置的後一個值。在其內部,slice 的數據結構存儲了 slice 的開始位置和長度,長度對應於 ending_index 減去 starting_index 的值。所以對於 let world = &s[6..11]; 的情況,world 將是一個包含指向 s 索引 6 的指針和長度值 5 的 slice
對於 Rust 的 .. range 語法,如果想要從索引 0 開始,可以不寫兩個點號之前的值。
依此類推,如果 slice 包含 String 的最後一個字節,也可以捨棄尾部的數字
也可以同時捨棄這兩個值來獲取整個字符串的 slice
在記住所有這些知識後,讓我們重寫 first_word 來返回一個 slice。“字符串 slice” 的類型聲明寫作 &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[..]
}
在來看一下下面的例子
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);
}
回憶一下借用規則,當擁有某值的不可變引用時,就不能再獲取一個可變引用。因為 clear 需要清空 String,它嘗試獲取一個可變引用。在調用 clear 之後的 println! 使用了 word 中的引用,所以這個不可變的引用在此時必須仍然有效。Rust 不允許 clear 中的可變引用和 word 中的不可變引用同時存在,因此編譯失敗。Rust 不僅使得我們的 API 簡單易用,也在編譯時就消除了一整類的錯誤!
字符串字面量就是slice
還記得我們講到過字符串字面量被儲存在二進制文件中嗎?現在知道 slice 了,我們就可以正確地理解字符串字面量了:
fn main() {
let s = "Hello, world!";
}
這裏 s 的類型是 &str:它是一個指向二進制程序特定位置的 slice。這也就是為什麼字符串字面量是不可變的;&str 是一個不可變引用
字符串slice作為參數
在知道了能夠獲取字面量和 String 的 slice 後,我們對 first_word 做了改進,這是它的簽名
fn first_word(s: &String) -> &str {
而更有經驗的 Rustacean 會編寫出下面的簽名,因為它使得可以對 String 值和 &str 值使用相同的函數:
fn first_word(s: &str) -> &str {
其他類型slice
字符串 slice,正如你想象的那樣,是針對字符串的。不過也有更通用的 slice 類型。考慮一下這個數組:
let a = [1, 2, 3, 4, 5];
就跟我們想要獲取字符串的一部分那樣,我們也會想要引用數組的一部分。我們可以這樣做:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
這個 slice 的類型是 &[i32]。它跟字符串 slice 的工作方式一樣,通過存儲第一個集合元素的引用和一個集合總長度。你可以對其他所有集合使用這類 slice。