原文鏈接:https://kaisery.github.io/trpl-zh-cn/ch04-01-what-is-ownership.html
- 作用域
- ** 變量與數(shù)據(jù)交互的方式(一):移動
- ** 變量與數(shù)據(jù)交互的方式(二):克隆
Rust 的核心功能(之一)是 所有權(quán)(ownership)。
所有運(yùn)行的程序都必須管理其使用計(jì)算機(jī)內(nèi)存的方式。
Rust 則選擇了第三種方式:通過所有權(quán)系統(tǒng)管理內(nèi)存,編譯器在編譯時會根據(jù)一系列的規(guī)則進(jìn)行檢查。在運(yùn)行時,所有權(quán)系統(tǒng)的任何功能都不會減慢程序。
因?yàn)樗袡?quán)對很多程序員來說都是一個新概念,需要一些時間來適應(yīng)。
棧與堆
棧以放入值的順序存儲值并以相反順序取出值。這也被稱作 后進(jìn)先出(last in, first out)。想象一下一疊盤子:當(dāng)增加更多盤子時,把它們放在盤子堆的頂部,當(dāng)需要盤子時,也從頂部拿走。不能從中間也不能從底部增加或拿走盤子!增加數(shù)據(jù)叫做 進(jìn)棧(pushing onto the stack),而移出數(shù)據(jù)叫做 出棧(popping off the stack)
棧的操作是十分快速的,這主要是得益于它存取數(shù)據(jù)的方式:因?yàn)閿?shù)據(jù)存取的位置總是在棧頂而不需要尋找一個位置存放或讀取數(shù)據(jù)。另一個讓操作??焖俚膶傩允?,棧中的所有數(shù)據(jù)都必須占用已知且固定的大小。
在編譯時大小未知或大小可能變化的數(shù)據(jù),要改為存儲在堆上。
堆是缺乏組織的:當(dāng)向堆放入數(shù)據(jù)時,你要請求一定大小的空間。操作系統(tǒng)在堆的某處找到一塊足夠大的空位,把它標(biāo)記為已使用,并返回一個表示該位置地址的 指針(pointer)。這個過程稱作 在堆上分配內(nèi)存(allocating on the heap),有時簡稱為 “分配”(allocating)。
將數(shù)據(jù)推入棧中并不被認(rèn)為是分配。因?yàn)橹羔樀拇笮∈且阎⑶夜潭ǖ模憧梢詫⒅羔槾鎯υ跅I?,不過當(dāng)需要實(shí)際數(shù)據(jù)時,必須訪問指針。
想象一下去餐館就座吃飯。當(dāng)進(jìn)入時,你說明有幾個人,餐館員工會找到一個夠大的空桌子并領(lǐng)你們過去。如果有人來遲了,他們也可以通過詢問來找到你們坐在哪。
訪問堆上的數(shù)據(jù)比訪問棧上的數(shù)據(jù)慢,
因?yàn)楸仨毻ㄟ^指針來訪問?,F(xiàn)代處理器在內(nèi)存中跳轉(zhuǎn)越少就越快(緩存)。
假設(shè)有一個服務(wù)員在餐廳里處理多個桌子的點(diǎn)菜。在一個桌子報完所有菜后再移動到下一個桌子是最有效率的。從桌子 A 聽一個菜,接著桌子 B 聽一個菜,然后再桌子 A,然后再桌子 B 這樣的流程會更加緩慢。
跟蹤哪部分代碼正在使用堆上的哪些數(shù)據(jù),最大限度的減少堆上的重復(fù)數(shù)據(jù)的數(shù)量,以及清理堆上不再使用的數(shù)據(jù)確保不會耗盡空間,這些問題正是所有權(quán)系統(tǒng)要處理的。一旦理解了所有權(quán),你就不需要經(jīng)??紤]棧和堆了,不過明白了所有權(quán)的存在就是為了管理堆數(shù)據(jù),能夠幫助解釋為什么所有權(quán)要以這種方式工作。
所有權(quán)規(guī)則
1.Rust 中的每一個值都有一個被稱為其 所有者(owner)的變量。
2.值有且只有一個所有者。
3.當(dāng)所有者(變量)離開作用域,這個值將被丟棄。變量作用域(scope)
fn main() {
{ // s 在這里無效, 它尚未聲明
let s = "hello"; // 從此處起,s 是有效的
// 使用 s
} // 此作用域已結(jié)束,s 不再有效
}
示例 4-1:一個變量和其有效的作用域
換句話說,這里有兩個重要的時間點(diǎn):
當(dāng) s 進(jìn)入作用域 時,它就是有效的。
這一直持續(xù)到它 離開作用域 為止。
- String 類型
String。這個類型被分配到堆上,所以能夠存儲在編譯時未知大小的文本??梢允褂?from 函數(shù)基于字符串字面值來創(chuàng)建 String
fn main() {
let mut s = String::from("hello”);
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 將打印 `hello, world!`
}
- 內(nèi)存和分配
對于 String 類型,為了支持一個可變,可增長的文本片段,需要在堆上分配一塊在編譯時未知大小的內(nèi)存來存放內(nèi)容。這意味著:
1.必須在運(yùn)行時向操作系統(tǒng)請求內(nèi)存。
2.需要一個當(dāng)我們處理完 String 時將內(nèi)存返回給操作系統(tǒng)的方法。
第一部分由我們完成:當(dāng)調(diào)用 String::from 時,它的實(shí)現(xiàn) (implementation) 請求其所需的內(nèi)存。
第二部分實(shí)現(xiàn):內(nèi)存在擁有它的變量離開作用域后就被自動釋放。
#![allow(unused_variables)]
fn main() {
{
let s = String::from("hello"); // 從此處起,s 是有效的
// 使用 s
} // 此作用域已結(jié)束,
// s 不再有效
}
當(dāng) s 離開作用域的時候。當(dāng)變量離開作用域,Rust 為我們調(diào)用一個特殊的函數(shù)。這個函數(shù)叫做 drop,在這里 String 的作者可以放置釋放內(nèi)存的代碼。Rust 在結(jié)尾的 } 處自動調(diào)用 drop。
變量與數(shù)據(jù)交互的方式(一):移動
fn main() {
let s1 = String::from("hello”);
let s2 = s1;
}
看看圖 4-1 以了解 String 的底層會發(fā)生什么。String 由三部分組成,如圖左側(cè)所示:
一個指向存放字符串內(nèi)容內(nèi)存的指針,一個長度,和一個容量。
這一組數(shù)據(jù)存儲在棧上。右側(cè)則是堆上存放內(nèi)容的內(nèi)存部分。

圖 4-1:將值 "hello" 綁定給 s1 的 String 在內(nèi)存中的表現(xiàn)形式
長度表示 String 的內(nèi)容當(dāng)前使用了多少字節(jié)的內(nèi)存。容量是 String 從操作系統(tǒng)總共獲取了多少字節(jié)的內(nèi)存。
當(dāng)我們將 s1 賦值給 s2,String 的數(shù)據(jù)被復(fù)制了,這意味著我們從棧上拷貝了它的指針、長度和容量。我們并沒有復(fù)制指針指向的堆上數(shù)據(jù)。換句話說,內(nèi)存中數(shù)據(jù)的表現(xiàn)如圖 4-2 所示。

圖 4-2:變量 s2 的內(nèi)存表現(xiàn),它有一份 s1 指針、長度和容量的拷貝
之前我們提到過當(dāng)變量離開作用域后,Rust 自動調(diào)用 drop 函數(shù)并清理變量的堆內(nèi)存。不過圖 4-2 展示了兩個數(shù)據(jù)指針指向了同一位置。這就有了一個問題:當(dāng) s2 和 s1 離開作用域,他們都會嘗試釋放相同的內(nèi)存。這是一個叫做 二次釋放(double free)的錯誤,也是之前提到過的內(nèi)存安全性 bug 之一。兩次釋放(相同)內(nèi)存會導(dǎo)致內(nèi)存污染,它可能會導(dǎo)致潛在的安全漏洞。
為了確保內(nèi)存安全,這種場景下 Rust 的處理有另一個細(xì)節(jié)值得注意。與其嘗試拷貝被分配的內(nèi)存,Rust 則認(rèn)為 s1 不再有效,因此 Rust 不需要在 s1 離開作用域后清理任何東西。
如果你在其他語言中聽說過術(shù)語 淺拷貝(shallow copy)和 深拷貝(deep copy),那么拷貝指針、長度和容量而不拷貝數(shù)據(jù)可能聽起來像淺拷貝。不過因?yàn)?Rust 同時使第一個變量無效了,這個操作被稱為 移動(move),而不是淺拷貝。上面的例子可以解讀為 s1 被 移動 到了 s2 中。那么具體發(fā)生了什么,如圖 4-4 所示。

這樣就解決了我們的問題!因?yàn)橹挥?s2 是有效的,當(dāng)其離開作用域,它就釋放自己的內(nèi)存,完畢。
變量與數(shù)據(jù)交互的方式(二):克隆
如果我們 確實(shí) 需要深度復(fù)制 String 中堆上的數(shù)據(jù),而不僅僅是棧上的數(shù)據(jù),可以使用一個叫做 clone 的通用函數(shù)。
fn main() {
let s1 = String::from("hello”);
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
產(chǎn)生的結(jié)果如下:

當(dāng)出現(xiàn) clone 調(diào)用時,你知道一些特定的代碼被執(zhí)行而且這些代碼可能相當(dāng)消耗資源。你很容易察覺到一些不尋常的事情正在發(fā)生。
所有權(quán)與函數(shù)
向函數(shù)傳遞值可能會移動或者復(fù)制,就像賦值語句一樣。
fn main() {
let s = String::from("hello"); // s 進(jìn)入作用域
takes_ownership(s); // s 的值移動到函數(shù)里 …
// ... 所以到這里不再有效
let x = 5; // x 進(jìn)入作用域
makes_copy(x); // x 應(yīng)該移動函數(shù)里,
// 但 i32 是 Copy 的,所以在后面可繼續(xù)使用 x
} // 這里, x 先移出了作用域,然后是 s。但因?yàn)?s 的值已被移走,
// 所以不會有特殊操作
fn takes_ownership(some_string: String) { // some_string 進(jìn)入作用域
println!("{}", some_string);
} // 這里,some_string 移出作用域并調(diào)用 `drop` 方法。占用的內(nèi)存被釋放
fn makes_copy(some_integer: i32) { // some_integer 進(jìn)入作用域
println!("{}", some_integer);
} // 這里,some_integer 移出作用域。不會有特殊操作
返回值與作用域
示例 4-4: 轉(zhuǎn)移返回值的所有權(quán)
fn main() {
let s1 = gives_ownership(); // gives_ownership 將返回值
// 移給 s1
let s2 = String::from("hello"); // s2 進(jìn)入作用域
let s3 = takes_and_gives_back(s2); // s2 被移動到
// takes_and_gives_back 中,
// 它也將返回值移給 s3
} // 這里, s3 移出作用域并被丟棄。s2 也移出作用域,但已被移走,
// 所以什么也不會發(fā)生。s1 移出作用域并被丟棄
fn gives_ownership() -> String { // gives_ownership 將返回值移動給
// 調(diào)用它的函數(shù)
let some_string = String::from("hello"); // some_string 進(jìn)入作用域.
some_string // 返回 some_string 并移出給調(diào)用的函數(shù)
}
// takes_and_gives_back 將傳入字符串并返回該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進(jìn)入作用域
a_string // 返回 a_string 并移出給調(diào)用的函數(shù)
}
變量的所有權(quán)總是遵循相同的模式:將值賦給另一個變量時移動它。當(dāng)持有堆中數(shù)據(jù)值的變量離開作用域時,其值將通過 drop 被清理掉,除非數(shù)據(jù)被移動為另一個變量所有。
在每一個函數(shù)中都獲取所有權(quán)并接著返回所有權(quán)有些啰嗦。如果我們想要函數(shù)使用一個值但不獲取所有權(quán)該怎么辦呢?如果我們還要接著使用它的話,每次都傳進(jìn)去再返回來就有點(diǎn)煩人了.
引用(references)。