深入理解Rust所有權(quán)
所有權(quán)是 Rust 最獨(dú)特的特性,對(duì)語言的其他部分有著深刻的影響。它使 Rust 能夠在不需要垃圾收集器的情況下保證內(nèi)存安全,因此理解所有權(quán)是如何工作的很重要。在本文中,我們將討論所有權(quán)以及幾個(gè)相關(guān)的特性:借用、切片,以及 Rust 如何在內(nèi)存中布局?jǐn)?shù)據(jù)。
什么是所有權(quán)?
所有權(quán)是一組控制 Rust 程序如何管理內(nèi)存的規(guī)則。
所有程序在運(yùn)行時(shí)都必須管理它們使用計(jì)算機(jī)內(nèi)存的方式。一些語言有垃圾收集,在程序運(yùn)行時(shí)定期查找不再使用的內(nèi)存;在其他語言中,程序員必須顯式地分配和釋放內(nèi)存。Rust 使用了第三種方法:內(nèi)存通過一個(gè)所有權(quán)系統(tǒng)進(jìn)行管理,該系統(tǒng)擁有一組編譯器檢查的規(guī)則。如果違反了任何規(guī)則,程序?qū)o法編譯。
所有權(quán)的主要目的是管理堆數(shù)據(jù)。
跟蹤代碼的哪些部分正在使用堆上的哪些數(shù)據(jù),最小化堆上的重復(fù)數(shù)據(jù)量,以及清理堆上未使用的數(shù)據(jù),這樣就不會(huì)耗盡空間,這些都是所有權(quán)可以解決的問題。
所有權(quán)規(guī)則
首先,讓我們看一下所有權(quán)規(guī)則:
- Rust 中的每個(gè)值都有一個(gè)所有者。
- 一次只能有一個(gè)所有者。
- 當(dāng)所有者超出范圍時(shí),該值將被刪除。
變量作用域
作用域是程序中某項(xiàng)有效的范圍。變量從聲明它的地方開始有效,直到當(dāng)前作用域結(jié)束。
示例:
{ // s is not valid here, it's not yet declared let s = "hello"; // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid
這里有兩個(gè)重要的時(shí)間點(diǎn):
- 當(dāng) s 進(jìn)入作用域時(shí),它是有效的。
- 它在超出作用域之前一直有效。
在這一點(diǎn)上,作用域和變量何時(shí)有效之間的關(guān)系與其他編程語言中的關(guān)系類似。
引子:字符串變量和字面量
我們已經(jīng)看到了字符串字面量,其中字符串值被硬編碼到程序中。字符串字面值很方便,但它們并不適合我們可能想要使用文本的所有情況。一個(gè)原因是它們是不可變的。另一個(gè)問題是,當(dāng)我們編寫代碼時(shí),不是每個(gè)字符串值都是已知的。
對(duì)于這些情況,可以創(chuàng)建字符串變量:
let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String println!("{s}"); // This will print `hello, world!`
這種類型的字符串可以被改變。
為什么字符串可以改變,而字面量不能?不同之處在于這兩種類型處理內(nèi)存的方式。
內(nèi)存和分配
對(duì)于字符串字面值,我們?cè)诰幾g時(shí)就知道其內(nèi)容,因此文本直接硬編碼到最終的可執(zhí)行文件中。這就是字符串字面值快速高效的原因。但這些屬性僅來自字符串文字的不變性。不幸的是,我們不能為每個(gè)在編譯時(shí)大小未知且在運(yùn)行程序時(shí)大小可能改變的文本塊放入二進(jìn)制文件中的內(nèi)存塊。
對(duì)于 String 類型,為了支持可變的、可增長的文本片段,我們需要在堆上分配一定數(shù)量的內(nèi)存(在編譯時(shí)未知)來保存內(nèi)容。這意味著:
- 內(nèi)存必須在運(yùn)行時(shí)從內(nèi)存分配器請(qǐng)求。
- 我們需要一種方法,在使用完 String 后將這些內(nèi)存返回給分配器。
第一部分由我們完成,當(dāng)調(diào)用String::from時(shí),它的實(shí)現(xiàn)請(qǐng)求它所需的內(nèi)存。這在編程語言中是非常普遍的。
然而,第二部分是不同的。在帶有垃圾收集器(GC)的語言中,GC 跟蹤并清理不再使用的內(nèi)存,我們不需要考慮它。
在大多數(shù)沒有 GC 的語言中,我們有責(zé)任識(shí)別內(nèi)存何時(shí)不再被使用,并調(diào)用代碼顯式釋放它,就像我們請(qǐng)求它一樣。正確地做到這一點(diǎn)歷來是一個(gè)困難的編程問題。如果我們忘記了,我們就會(huì)浪費(fèi)記憶。如果我們做得太早,就會(huì)得到一個(gè)無效的變量。如果我們做兩次,這也是一個(gè) bug。我們需要恰好配對(duì)一個(gè)已分配的和一個(gè)空閑的。
Rust 采用不同的路徑:一旦擁有內(nèi)存的變量超出作用域,內(nèi)存就會(huì)自動(dòng)返回。
{ let s = String::from("hello"); // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid
當(dāng)變量超出作用域時(shí),Rust 會(huì)為我們調(diào)用一個(gè)特殊的函數(shù)。這個(gè)函數(shù)被稱為 drop,該函數(shù)將內(nèi)存返回給分配器。
注意:在 C++ 中,這種在項(xiàng)目生命周期結(jié)束時(shí)釋放資源的模式有時(shí)被稱為資源獲取即初始化(RAII)。
與 Move 交互的變量和數(shù)據(jù)
在 Rust 中,多個(gè)變量可以以不同的方式與相同的數(shù)據(jù)交互。
示例 1:
let x = 5; let y = x;
我們現(xiàn)在有兩個(gè)變量,x 和 y,它們都等于 5。因?yàn)檎麛?shù)是具有已知的固定大小的簡單值,并且這兩個(gè) 5 值被壓入堆棧。
示例 2:
let s1 = String::from("hello"); let s2 = s1;
字符串由三部分組成:指向存儲(chǔ)字符串內(nèi)容的內(nèi)存的指針、長度和容量。長度是指字符串的內(nèi)容當(dāng)前使用了多少內(nèi)存(以字節(jié)為單位)。容量是字符串從分配器接收到的總內(nèi)存量(以字節(jié)為單位)。
這組數(shù)據(jù)存儲(chǔ)在堆棧上,右邊是堆中保存內(nèi)容的內(nèi)存。
當(dāng)將 s1 賦值給 s2 時(shí),復(fù)制了 String 數(shù)據(jù),這意味著復(fù)制了堆棧上的指針、長度和容量。但是,不復(fù)制指針?biāo)赶虻亩焉系臄?shù)據(jù)。
前面我們說過,當(dāng)變量超出作用域時(shí),Rust 會(huì)自動(dòng)調(diào)用 drop 函數(shù)并為該變量清理堆內(nèi)存。但是上圖顯示兩個(gè)數(shù)據(jù)指針都指向同一個(gè)位置。這是一個(gè)問題:當(dāng) s2 和 s1 超出作用域時(shí),它們都將嘗試釋放相同的內(nèi)存。這被稱為 double free error,是內(nèi)存安全錯(cuò)誤之一。釋放內(nèi)存兩次可能會(huì)導(dǎo)致內(nèi)存損壞,這可能會(huì)導(dǎo)致安全漏洞。
為了確保內(nèi)存安全,在 let s2 = s1;
行之后,Rust 認(rèn)為 s1 不再有效。因此,當(dāng) s1 超出作用域時(shí),Rust 不需要釋放任何東西。
在創(chuàng)建 s2 之后嘗試使用 s1 會(huì)報(bào)錯(cuò):使用無效的引用。
在 C++ 中,你可能聽說過淺拷貝和深拷貝這兩個(gè)術(shù)語,那么在不復(fù)制數(shù)據(jù)的情況下復(fù)制指針、長度和容量的概念,可以視為淺拷貝。但是因?yàn)?Rust 會(huì)使第一個(gè)變量無效,所以它被稱為移動(dòng)(Move),而不是淺拷貝。在這個(gè)例子中,我們會(huì)說 s1 被移動(dòng)到 s2。
這就解決了我們的問題!只有 s2 有效,當(dāng)它超出作用域時(shí),它會(huì)單獨(dú)釋放內(nèi)存,這樣就完成了。
此外,這還隱含了一個(gè)設(shè)計(jì)選擇:Rust 永遠(yuǎn)不會(huì)自動(dòng)創(chuàng)建數(shù)據(jù)的“深度”副本。因此,就運(yùn)行時(shí)性能而言,任何自動(dòng)復(fù)制都可以被認(rèn)為是廉價(jià)的。
范圍和分配
作用域、所有權(quán)和通過 drop 函數(shù)釋放的內(nèi)存之間的關(guān)系也是如此。
當(dāng)你給一個(gè)已經(jīng)存在的變量賦一個(gè)全新的值時(shí),Rust 會(huì)調(diào)用 drop 并立即釋放原始值的內(nèi)存。
示例:
let mut s = String::from("hello"); s = String::from("ahoy"); println!("{s}, world!");
我們首先聲明一個(gè)變量 s,并將其綁定到一個(gè)值為 “hello” 的字符串。然后,我們立即創(chuàng)建一個(gè)值為 “ahoy” 的新 String,并將其賦值給 s。此時(shí),原始字符串立即超出了作用域,Rust 運(yùn)行 drop函數(shù)立即釋放 “hello” 的內(nèi)存。
克?。–lone)
Rust 提供一個(gè)叫 clone 的方法進(jìn)行深拷貝。
示例:
let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}");
堆數(shù)據(jù)確實(shí)被復(fù)制了。
只在棧上的數(shù)據(jù):復(fù)制(Copy)
示例:
let x = 5; let y = x; println!("x = {x}, y = {y}");
這段代碼似乎與我們剛剛學(xué)到的內(nèi)容相矛盾:我們沒有調(diào)用 clone,但是 x 仍然有效,并且沒有移動(dòng)到 y 中。
原因是,在編譯時(shí)具有已知大小的整數(shù)等類型完全存儲(chǔ)在堆棧中,因此可以快速復(fù)制實(shí)際值。這意味著我們沒有理由在創(chuàng)建變量 y 后阻止 x 的有效性。換句話說,這里沒有深度復(fù)制和淺復(fù)制的區(qū)別,所以調(diào)用 clone 與通常的淺復(fù)制沒有任何不同,我們可以省略它。
Rust 有一個(gè)特殊的注釋,叫做 Copy trait,我們可以把它放在存儲(chǔ)在堆棧上的類型上,就像整數(shù)一樣。如果一個(gè)類型實(shí)現(xiàn)了 Copy 特性,那么使用它的變量不會(huì)移動(dòng),而是被簡單地復(fù)制,使它們?cè)谫x值給另一個(gè)變量后仍然有效。
如果類型或其任何部分實(shí)現(xiàn)了 Drop 特性,Rust 將不允許我們用 Copy 注釋類型。如果該類型需要在值超出作用域時(shí)發(fā)生一些特殊的事情,并且向該類型添加 Copy 注釋,則會(huì)得到編譯時(shí)錯(cuò)誤。
一般地,任何一組簡單標(biāo)量值都可以實(shí)現(xiàn) Copy,并且不需要分配或某種形式的資源來實(shí)現(xiàn) Copy。下面是一些實(shí)現(xiàn) Copy 的類型:
- 所有整數(shù)類型,如 u32。
- 所所有浮點(diǎn)類型,如 f64。
- 所布爾類型,bool。
- 所字符類型,char。
- 只包含實(shí)現(xiàn) Copy 的類型的元組。例如,(i32, i32)實(shí)現(xiàn)了Copy,但(i32, String)沒有。
所有權(quán)與函數(shù)
將值傳遞給函數(shù)的機(jī)制類似于將值賦給變量的機(jī)制。將變量傳遞給函數(shù)會(huì)移動(dòng)或復(fù)制,就像賦值一樣。
示例:
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // because i32 implements the Copy trait, // x does NOT move into the function, println!("{}", x); // so it's okay to use x afterward } // Here, x goes out of scope, then s. But because s's value was moved, nothing // special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
如果我們?cè)噲D在調(diào)用 takes_ownership 之后使用 s, Rust 會(huì)拋出一個(gè)編譯時(shí)錯(cuò)誤。
返回值和作用域
返回值也可以轉(zhuǎn)移所有權(quán)。
示例:
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns one fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
變量的所有權(quán)每次都遵循相同的模式:將值賦給另一個(gè)變量會(huì)移動(dòng)該變量。當(dāng)包含堆上數(shù)據(jù)的變量超出作用域時(shí),除非數(shù)據(jù)的所有權(quán)已移動(dòng)到另一個(gè)變量,否則該值將通過 drop 清除。
Rust 允許我們使用元組返回多個(gè)值,雖然這是可行的,但是獲取所有權(quán)并返回每個(gè)函數(shù)的所有權(quán)有點(diǎn)繁瑣。
示例:
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
如果我們想讓一個(gè)函數(shù)使用一個(gè)值,但不獲得所有權(quán),該怎么辦?
幸運(yùn)的是,Rust 有一個(gè)不用轉(zhuǎn)移所有權(quán)就能使用值的特性,叫做引用。
引用(References)和借用(Borrowing)
引用類似于指針,因?yàn)樗且粋€(gè)地址,我們可以按照它訪問存儲(chǔ)在該地址的數(shù)據(jù)。
引用用 & 符號(hào)表示。與使用 & 進(jìn)行引用相反的是解引用,它是通過解引用操作符 * 完成的。
與指針不同,引用保證在其生命周期內(nèi)指向特定類型的有效值。
示例:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { // s is a reference to a String s.len() } // Here, s goes out of scope. But because s does not have ownership of what // it refers to, the value is not dropped.
我們稱創(chuàng)建引用的操作為借用。&s1 語法允許我們創(chuàng)建一個(gè)引用 s,該引用引用 s1 的值,但不擁有該值。因?yàn)橐貌粨碛兴?,所以?dāng)引用停止使用時(shí),它所指向的值不會(huì)被刪除。同樣,函數(shù)的定義使用 & 表示形參 s 的類型是引用。
引用也是不可變的。我們不允許修改我們引用過的東西。
示例:
fn main() { let s = String::from("hello"); change(&s); } fn change(some_string: &String) { some_string.push_str(", world"); }
這段代碼會(huì)報(bào)錯(cuò):error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference。
Rust 通過“借用檢查器”確保引用的安全性。
變量對(duì)其數(shù)據(jù)有三種權(quán)限:
- 讀:數(shù)據(jù)可以被復(fù)制到另一個(gè)位置。
- 寫:數(shù)據(jù)可以被修改。
- 擁有:數(shù)據(jù)可以被移動(dòng)或釋放。
這些權(quán)限在運(yùn)行時(shí)并不存在,僅在編譯器內(nèi)部存在。
默認(rèn)情況下,變量對(duì)其數(shù)據(jù)具有讀/擁有的權(quán)限,如果變量是可變的,那么它還擁有寫權(quán)限。
重點(diǎn):引用可以臨時(shí)解除這些權(quán)限。
可變的引用
修改之前的代碼,使其允許我們通過使用可變引用的一些小調(diào)整來修改借用值。
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
用 &mut s 就可以創(chuàng)建一個(gè)可變引用。
可變引用有一個(gè)很大的限制:如果對(duì)一個(gè)值有一個(gè)可變引用,那么就不能有對(duì)該值的其他引用。下面的代碼試圖創(chuàng)建對(duì) s 的兩個(gè)可變引用將會(huì)失?。?/p>
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
規(guī)定不能同時(shí)對(duì)同一數(shù)據(jù)進(jìn)行多個(gè)可變引用的限制,這樣做的好處是 Rust 可以在編譯時(shí)防止數(shù)據(jù)競(jìng)爭。數(shù)據(jù)競(jìng)爭類似于競(jìng)爭條件,發(fā)生在以下三種行為時(shí):
- 兩個(gè)或多個(gè)指針同時(shí)訪問相同的數(shù)據(jù)。
- 至少有一個(gè)指針被用來寫數(shù)據(jù)。
- 沒有使用任何機(jī)制來同步對(duì)數(shù)據(jù)的訪問。
數(shù)據(jù)競(jìng)爭會(huì)導(dǎo)致未定義的行為,當(dāng)你試圖在運(yùn)行時(shí)追蹤它們時(shí),可能很難診斷和修復(fù)它們。Rust 通過拒絕編譯帶有數(shù)據(jù)競(jìng)爭的代碼來防止這個(gè)問題!
我們可以使用花括號(hào)來創(chuàng)建一個(gè)新的作用域,允許多個(gè)可變引用,只是不能同時(shí)使用:
let mut s = String::from("hello"); { let r1 = &mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = &mut s;
Rust 對(duì)可變引用和不可變引用的組合強(qiáng)制了類似的規(guī)則:
- 對(duì)同一值有不可變引用的同時(shí),也不能有可變引用。
- 允許使用多個(gè)不可變引用。
示例:
let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem let r3 = &mut s; // BIG PROBLEM println!("{}, {}, and {}", r1, r2, r3);
報(bào)錯(cuò):error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable。
請(qǐng)注意,引用的作用域從它被引入的地方開始,一直持續(xù)到最后一次使用該引用的時(shí)候。例如,下面這段代碼可以編譯,因?yàn)椴豢勺円玫淖詈笠淮问褂檬窃?println!,在引入可變引用之前:
let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{r1} and {r2}"); // variables r1 and r2 will not be used after this point let r3 = &mut s; // no problem println!("{r3}");
編譯器可以在作用域結(jié)束之前的某個(gè)點(diǎn)上判斷出引用不再被使用。
加深對(duì)可變引用的理解
可變引用提供對(duì)數(shù)據(jù)唯一的且非擁有的訪問。
不可變引用是只讀的??勺円迷诓灰苿?dòng)數(shù)據(jù)的情況下,臨時(shí)提供可變?cè)L問。
可變引用臨時(shí)降級(jí)為只讀引用
對(duì)一個(gè)可變引用再進(jìn)行引用,可以暫時(shí)剝奪可變引用的寫權(quán)限,新引用只具有讀權(quán)限。
權(quán)限在生命周期結(jié)束時(shí)被返回
在創(chuàng)建了 x 的引用 y 后,x 的寫和擁有權(quán)限暫時(shí)被剝奪,直到 y 的最后一次被使用之后,x 重新獲得寫和擁有權(quán)限。
懸空的引用
在使用指針的語言中,很容易通過釋放一些內(nèi)存而保留指向該內(nèi)存的指針來錯(cuò)誤地創(chuàng)建懸空指針。
相比之下,在 Rust 中,編譯器保證引用永遠(yuǎn)不會(huì)是懸空引用:如果你有對(duì)某些數(shù)據(jù)的引用,編譯器將確保數(shù)據(jù)不會(huì)在對(duì)數(shù)據(jù)的引用超出作用域之前超出作用域。
即,數(shù)據(jù)必須在其所有的引用存在的期間存活。
示例:創(chuàng)建了 s 的引用 s_ref 后, s 的擁有權(quán)限暫時(shí)被剝奪,于是不能移動(dòng)或刪除,程序在 drop(s) 出報(bào)錯(cuò)。
讓我們嘗試創(chuàng)建一個(gè)懸空引用,看看 Rust 是如何用編譯時(shí)錯(cuò)誤來防止它們的:
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
報(bào)錯(cuò):error[E0106]: missing lifetime specifier。
這個(gè)錯(cuò)誤消息涉及到一個(gè)我們尚未涉及的特性:生命周期。在這里我們先不討論這個(gè)特性,而是分析 dangle() 函數(shù)的錯(cuò)誤原因:因?yàn)?s 是在 dangle 內(nèi)部創(chuàng)建的,所以當(dāng) dangle 的代碼完成時(shí),s 將被釋放。但是我們?cè)囍祷貙?duì)它的引用。這意味著這個(gè)引用將指向一個(gè)無效的字符串,于是發(fā)生了錯(cuò)誤。
這里的解決方案是直接返回 String,而不是其引用:
fn no_dangle() -> String { let s = String::from("hello"); s }
總結(jié):
- 在任何給定的時(shí)間,可以有一個(gè)可變引用或任意數(shù)量的不可變引用。
- 引用必須總是有效的。
接下來,我們來看看另一種類型的引用:切片。
切片(Slice)
切片允許引用集合中連續(xù)的元素序列,而不是整個(gè)集合。
切片是一種引用,所以它沒有所有權(quán)。
這里有一個(gè)小編程問題:編寫一個(gè)函數(shù),它接受一個(gè)由空格分隔的單詞字符串,并返回它在該字符串中找到的第一個(gè)單詞的長度。如果函數(shù)在字符串中沒有找到空格,則整個(gè)字符串必須是一個(gè)單詞,因此應(yīng)該返回整個(gè)字符串的長度。
讓我們來看看如何在不使用切片的情況下編寫這個(gè)函數(shù),以理解切片將解決的問題:
fn first_word(s: &String) -> usize { // convert String to an array of bytes let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() }
我們現(xiàn)在有一種方法來找出字符串中第一個(gè)單詞末尾的索引,但是有一個(gè)問題。我們自己返回了一個(gè) usize,但它只是一個(gè)獨(dú)立于 String 的值,所以不能保證它在將來仍然有效。
示例:
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 `s` no longer has any content // that we could meaningfully use with the value `5`, so `word` is now // totally invalid! }
這個(gè)程序編譯時(shí)沒有任何錯(cuò)誤,如果在調(diào)用 s.clear() 之后使用 word,也不會(huì)出現(xiàn)任何錯(cuò)誤。因?yàn)?word 根本沒有連接到 s 的狀態(tài),所以 word 仍然包含值 5。我們可以使用這個(gè)值 5 和變量 s 來嘗試提取出第一個(gè)單詞,但這將是一個(gè) bug,因?yàn)樽詮奈覀儗?5 保存在 word 中以來,s 的內(nèi)容已經(jīng)發(fā)生了變化。
word 中的索引與 s 中的數(shù)據(jù)不同步是危險(xiǎn)的。幸運(yùn)的是,Rust 有一個(gè)解決方案:字符串切片。
字符串切片是對(duì)字符串的一部分的引用,它看起來像這樣:
let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11];
指定 [starting_index…ending_index],其中 starting_index 是片中的第一個(gè)位置,ending_index 比片中的最后一個(gè)位置大 1。
如果想從下標(biāo) 0 開始,starting_index 可以省略,下面兩種切片是一樣的:
let s = String::from("hello"); let slice1 = &s[0..2]; let slice2 = &s[..2];
同樣,如果切片包含 String 的最后一個(gè)字符,ending_index 也可以省略,下面兩種切片是一樣的:
let s = String::from("hello"); let len = s.len(); let slice1 = &s[3..len]; let slice2 = &s[3..];
掌握了切片后,重寫之前的 first_word 函數(shù),這次返回的是字符串而非下標(biāo):
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[..] }
還記得之前程序中的錯(cuò)誤嗎?當(dāng)我們獲得了第一個(gè)單詞末尾的索引,但隨后清除了字符串,因此索引無效。這段代碼在邏輯上是不正確的,但沒有立即顯示出任何錯(cuò)誤。如果我們一直嘗試將第一個(gè)單詞索引與空字符串一起使用,那么問題就會(huì)出現(xiàn)。切片使這個(gè) bug 不可能出現(xiàn),并讓我們更快地知道我們的代碼有問題。
示例:
fn main() { let mut s = String::from("hello world"); let word = first_word(&s); s.clear(); // error! println!("the first word is: {word}"); }
報(bào)錯(cuò):error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable。
回顧借用規(guī)則,如果有對(duì)某物的不可變引用,就不能同時(shí)使用可變引用。因?yàn)?clear 需要截?cái)?String,所以它需要獲得一個(gè)可變引用。println! 在調(diào)用 clear 之后使用 word 中的引用,因此不可變引用在那時(shí)必須仍然是活動(dòng)的。Rust 不允許 clear 中的可變引用和 word 中的不可變引用同時(shí)存在,編譯失敗。
作為切片的字符串字面量
回想一下,我們討論過將字符串字面值存儲(chǔ)在二進(jìn)制文件中?,F(xiàn)在我們知道了切片,我們可以正確地理解字符串字面值:
let s = "Hello, world!";
這里 s 的類型是 &str,一個(gè)指向二進(jìn)制數(shù)據(jù)中特定點(diǎn)的切片。這也是為什么字符串字面值是不可變的,&str 是一個(gè)不可變引用。
作為參數(shù)的字符串切片
知道可以取字面量和字符串值的切片后,我們對(duì) first_word 又做了一個(gè)改進(jìn),那就是它的聲明:
fn first_word(s: &str) -> &str {
定義一個(gè)函數(shù)來接受一個(gè)字符串切片而不是一個(gè)字符串的引用,將提高函數(shù)的靈活性。如果我們有一個(gè)字符串切片,我們可以直接傳遞。如果有一個(gè) String 對(duì)象,則可以傳遞 String 對(duì)象的切片或?qū)?String 對(duì)象的引用。這種靈活性利用了取消強(qiáng)制轉(zhuǎn)換。
其他切片
數(shù)組也可以有切片:
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]);
這個(gè)切片的類型是 &[i32]。
總結(jié)
所有權(quán)、借用和切片的概念確保了 Rust 程序在編譯時(shí)的內(nèi)存安全。Rust 語言提供了對(duì)內(nèi)存使用的控制,但是當(dāng)數(shù)據(jù)所有者超出范圍時(shí),數(shù)據(jù)所有者會(huì)自動(dòng)清理數(shù)據(jù),這意味著不必編寫和調(diào)試額外的代碼來獲得這種控制。
到此這篇關(guān)于深入理解Rust所有權(quán)的文章就介紹到這了,更多相關(guān)Rust所有權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何基于Rust實(shí)現(xiàn)文本搜索minigrep
這篇文章主要介紹了基于Rust實(shí)現(xiàn)的文本搜索minigrep,本次演示介紹針對(duì)原作者代碼程序的查詢邏輯做了一點(diǎn)點(diǎn)小的優(yōu)化,原程序邏輯的查詢是放在了程序運(yùn)行的時(shí)候,邏輯修改后啟動(dòng)的時(shí)候可以添加參數(shù),也可以啟動(dòng)后添加,需要的朋友可以參考下2024-08-08在win10上使用mingw64編譯器配置Rust開發(fā)環(huán)境和idea 配置Rust 插件
在win10上配置 Rust 開發(fā)環(huán)境(使用 mingw64編譯器)和 idea 配置 Rust 插件的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-03-03