Rust 所有權(quán)

計(jì)算機(jī)程序必須在運(yùn)行時(shí)管理它們所使用的內(nèi)存資源。

大多數(shù)的編程語言都有管理內(nèi)存的功能:

C/C++ 這樣的語言主要通過手動(dòng)方式管理內(nèi)存,開發(fā)者需要手動(dòng)的申請和釋放內(nèi)存資源。但為了提高開發(fā)效率,只要不影響程序功能的實(shí)現(xiàn),許多開發(fā)者沒有及時(shí)釋放內(nèi)存的習(xí)慣。所以手動(dòng)管理內(nèi)存的方式常常造成資源浪費(fèi)。

Java 語言編寫的程序在虛擬機(jī)(JVM)中運(yùn)行,JVM 具備自動(dòng)回收內(nèi)存資源的功能。但這種方式常常會(huì)降低運(yùn)行時(shí)效率,所以 JVM 會(huì)盡可能少的回收資源,這樣也會(huì)使程序占用較大的內(nèi)存資源。

所有權(quán)對大多數(shù)開發(fā)者而言是一個(gè)新穎的概念,它是 Rust 語言為高效使用內(nèi)存而設(shè)計(jì)的語法機(jī)制。所有權(quán)概念是為了讓 Rust 在編譯階段更有效地分析內(nèi)存資源的有用性以實(shí)現(xiàn)內(nèi)存管理而誕生的概念。

所有權(quán)規(guī)則

所有權(quán)有以下三條規(guī)則:

  • Rust 中的每個(gè)值都有一個(gè)變量,稱為其所有者。

  • 一次只能有一個(gè)所有者。

  • 當(dāng)所有者不在程序運(yùn)行范圍時(shí),該值將被刪除。

這三條規(guī)則是所有權(quán)概念的基礎(chǔ)。

接下來將介紹與所有權(quán)概念有關(guān)的概念。

變量范圍

我們用下面這段程序描述變量范圍的概念:

{
    // 在聲明以前,變量 s 無效
    let s = "nhooo";
    // 這里是變量 s 的可用范圍
}
// 變量范圍已經(jīng)結(jié)束,變量 s 無效

變量范圍是變量的一個(gè)屬性,其代表變量的可行域,默認(rèn)從聲明變量開始有效直到變量所在域結(jié)束。

內(nèi)存和分配

如果我們定義了一個(gè)變量并給它賦予一個(gè)值,這個(gè)變量的值存在于內(nèi)存中。這種情況很普遍。但如果我們需要儲(chǔ)存的數(shù)據(jù)長度不確定(比如用戶輸入的一串字符串),我們就無法在定義時(shí)明確數(shù)據(jù)長度,也就無法在編譯階段令程序分配固定長度的內(nèi)存空間供數(shù)據(jù)儲(chǔ)存使用。(有人說分配盡可能大的空間可以解決問題,但這個(gè)方法很不文明)。這就需要提供一種在程序運(yùn)行時(shí)程序自己申請使用內(nèi)存的機(jī)制——堆。本章所講的所有"內(nèi)存資源"都指的是堆所占用的內(nèi)存空間。

有分配就有釋放,程序不能一直占用某個(gè)內(nèi)存資源。因此決定資源是否浪費(fèi)的關(guān)鍵因素就是資源有沒有及時(shí)的釋放。

我們把字符串樣例程序用 C 語言等價(jià)編寫:

{
    char *s = "nhooo";
    free(s); // 釋放 s 資源
}

很顯然,Rust 中沒有調(diào)用 free 函數(shù)來釋放字符串 s 的資源(我知道這樣在 C 語言中是不正確的寫法,因?yàn)?"nhooo" 不在堆中,這里假設(shè)它在)。Rust 之所以沒有明示釋放的步驟是因?yàn)樵谧兞糠秶Y(jié)束的時(shí)候,Rust 編譯器自動(dòng)添加了調(diào)用釋放資源函數(shù)的步驟。

這種機(jī)制看似很簡單了:它不過是幫助程序員在適當(dāng)?shù)牡胤教砑恿艘粋€(gè)釋放資源的函數(shù)調(diào)用而已。但這種簡單的機(jī)制可以有效地解決一個(gè)史上最令程序員頭疼的編程問題。

變量與數(shù)據(jù)交互的方式

變量與數(shù)據(jù)交互方式主要有移動(dòng)(Move)和克隆(Clone)兩種:

移動(dòng)

多個(gè)變量可以在 Rust 中以不同的方式與相同的數(shù)據(jù)交互:

let x = 5;
let y = x;

這個(gè)程序?qū)⒅?5 綁定到變量 x,然后將 x 的值復(fù)制并賦值給變量 y?,F(xiàn)在棧中將有兩個(gè)值 5。此情況中的數(shù)據(jù)是"基本數(shù)據(jù)"類型的數(shù)據(jù),不需要存儲(chǔ)到堆中,僅在棧中的數(shù)據(jù)的"移動(dòng)"方式是直接復(fù)制,這不會(huì)花費(fèi)更長的時(shí)間或更多的存儲(chǔ)空間。"基本數(shù)據(jù)"類型有這些:

  • 所有整數(shù)類型,例如 i32 、 u32 、 i64 等。

  • 布爾類型 bool,值為 true 或 false 。

  • 所有浮點(diǎn)類型,f32 和 f64。

  • 字符類型 char。

  • 僅包含以上類型數(shù)據(jù)的元組(Tuples)。

但如果發(fā)生交互的數(shù)據(jù)在堆中就是另外一種情況:

let s1 = String::from("hello");
let s2 = s1;

第一步產(chǎn)生一個(gè) String 對象,值為 "hello"。其中  "hello" 可以認(rèn)為是類似于長度不確定的數(shù)據(jù),需要在堆中存儲(chǔ)。

第二步的情況略有不同(這不是完全真的,僅用來對比參考):

如圖所示:兩個(gè) String 對象在棧中,每個(gè) String 對象都有一個(gè)指針指向堆中的 "hello" 字符串。在給 s2 賦值時(shí),只有棧中的數(shù)據(jù)被復(fù)制了,堆中的字符串依然還是原來的字符串。

前面我們說過,當(dāng)變量超出范圍時(shí),Rust 自動(dòng)調(diào)用釋放資源函數(shù)并清理該變量的堆內(nèi)存。但是 s1 和 s2 都被釋放的話堆區(qū)中的 "hello" 被釋放兩次,這是不被系統(tǒng)允許的。為了確保安全,在給 s2 賦值時(shí) s1 已經(jīng)無效了。沒錯(cuò),在把 s1 的值賦給 s2 以后 s1 將不可以再被使用。下面這段程序是錯(cuò)的:

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // 錯(cuò)誤!s1 已經(jīng)失效

所以實(shí)際情況是:

s1 名存實(shí)亡。

克隆

Rust會(huì)盡可能地降低程序的運(yùn)行成本,所以默認(rèn)情況下,長度較大的數(shù)據(jù)存放在堆中,且采用移動(dòng)的方式進(jìn)行數(shù)據(jù)交互。但如果需要將數(shù)據(jù)單純的復(fù)制一份以供他用,可以使用數(shù)據(jù)的第二種交互方式——克隆。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}", s1, s2);
}

運(yùn)行結(jié)果:

s1 = hello, s2 = hello

這里是真的將堆中的 "hello" 復(fù)制了一份,所以 s1 和 s2 都分別綁定了一個(gè)值,釋放的時(shí)候也會(huì)被當(dāng)作兩個(gè)資源。

當(dāng)然,克隆僅在需要復(fù)制的情況下使用,畢竟復(fù)制數(shù)據(jù)會(huì)花費(fèi)更多的時(shí)間。

涉及函數(shù)的所有權(quán)機(jī)制

對于變量來說這是最復(fù)雜的情況了。

如果將一個(gè)變量當(dāng)作函數(shù)的參數(shù)傳給其他函數(shù),怎樣安全的處理所有權(quán)呢?

下面這段程序描述了這種情況下所有權(quán)機(jī)制的運(yùn)行原理:

fn main() {
    let s = String::from("hello");
    // s 被聲明有效

    takes_ownership(s);
    // s 的值被當(dāng)作參數(shù)傳入函數(shù)
    // 所以可以當(dāng)作 s 已經(jīng)被移動(dòng),從這里開始已經(jīng)無效

    let x = 5;
    // x 被聲明有效

    makes_copy(x);
    // x 的值被當(dāng)作參數(shù)傳入函數(shù)
    // 但 x 是基本類型,依然有效
    // 在這里依然可以使用 x 卻不能使用 s

} // 函數(shù)結(jié)束, x 無效, 然后是 s. 但 s 已被移動(dòng), 所以不用被釋放


fn takes_ownership(some_string: String) { 
    // 一個(gè) String 參數(shù) some_string 傳入,有效
    println!("{}", some_string);
} // 函數(shù)結(jié)束, 參數(shù) some_string 在這里釋放

fn makes_copy(some_integer: i32) { 
    // 一個(gè) i32 參數(shù) some_integer 傳入,有效
    println!("{}", some_integer);
} // 函數(shù)結(jié)束, 參數(shù) some_integer 是基本類型, 無需釋放

如果將變量當(dāng)作參數(shù)傳入函數(shù),那么它和移動(dòng)的效果是一樣的。

函數(shù)返回值的所有權(quán)機(jī)制

fn main() {
    let s1 = gives_ownership();
    // gives_ownership 移動(dòng)它的返回值到 s1

    let s2 = String::from("hello");
    // s2 被聲明有效

    let s3 = takes_and_gives_back(s2);
    // s2 被當(dāng)作參數(shù)移動(dòng), s3 獲得返回值所有權(quán)
} // s3 無效被釋放, s2 被移動(dòng), s1 無效被釋放.

fn gives_ownership() -> String {
    let some_string = String::from("hello");
    // some_string 被聲明有效

    return some_string;
    // some_string 被當(dāng)作返回值移動(dòng)出函數(shù)
}

fn takes_and_gives_back(a_string: String) -> String { 
    // a_string 被聲明有效

    a_string  // a_string 被當(dāng)作返回值移出函數(shù)
}

被當(dāng)作函數(shù)返回值的變量所有權(quán)將會(huì)被移動(dòng)出函數(shù)并返回到調(diào)用函數(shù)的地方,而不會(huì)直接被無效釋放。

引用與租借

引用(Reference)是 C++ 開發(fā)者較為熟悉的概念。

如果你熟悉指針的概念,你可以把它看作一種指針。

實(shí)質(zhì)上"引用"是變量的間接訪問方式。

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 is {}, s2 is {}", s1, s2);
}

運(yùn)行結(jié)果:

s1 is hello, s2 is hello

& 運(yùn)算符可以取變量的"引用"。

當(dāng)一個(gè)變量的值被引用時(shí),變量本身不會(huì)被認(rèn)定無效。因?yàn)?quot;引用"并沒有在棧中復(fù)制變量的值:

函數(shù)參數(shù)傳遞的道理一樣:

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()
}

運(yùn)行結(jié)果:

The length of 'hello' is 5.

引用不會(huì)獲得值的所有權(quán)。

引用只能租借(Borrow)值的所有權(quán)。

引用本身也是一個(gè)類型并具有一個(gè)值,這個(gè)值記錄的是別的值所在的位置,但引用不具有所指值的所有權(quán):

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s2);
}

這段程序不正確:因?yàn)?s2 租借的 s1 已經(jīng)將所有權(quán)移動(dòng)到 s3,所以 s2 將無法繼續(xù)租借使用 s1 的所有權(quán)。如果需要使用 s2 使用該值,必須重新租借:

fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s2;
    s2 = &s3; // 重新從 s3 租借所有權(quán)
    println!("{}", s2);
}

這段程序是正確的。

既然引用不具有所有權(quán),即使它租借了所有權(quán),它也只享有使用權(quán)(這跟租房子是一個(gè)道理)。

如果嘗試?yán)米饨鑱淼臋?quán)利來修改數(shù)據(jù)會(huì)被阻止:

fn main() {
    let s1 = String::from("run");
    let s2 = &s1; 
    println!("{}", s2);
    s2.push_str("oob"); // 錯(cuò)誤,禁止修改租借的值
    println!("{}", s2);
}

這段程序中 s2 嘗試修改 s1 的值被阻止,租借的所有權(quán)不能修改所有者的值。

當(dāng)然,也存在一種可變的租借方式,就像你租一個(gè)房子,如果物業(yè)指定房主可以修改房子結(jié)構(gòu),房主在租借時(shí)也在合同中聲明賦予你這種權(quán)利,你是可以重新裝修房子的:

fn main() {
    let mut s1 = String::from("run");
    // s1 是可變的

    let s2 = &mut s1;
    // s2 是可變的引用

    s2.push_str("oob");
    println!("{}", s2);
}

這段程序就沒有問題了。我們用 &mut  修飾可變的引用類型。

可變引用與不可變引用相比除了權(quán)限不同以外,可變引用不允許多重引用,但不可變引用可以:

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

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

這段程序不正確,因?yàn)槎嘀乜勺円昧?s。

Rust 對可變引用的這種設(shè)計(jì)主要出于對并發(fā)狀態(tài)下發(fā)生數(shù)據(jù)訪問碰撞的考慮,在編譯階段就避免了這種事情的發(fā)生。

由于發(fā)生數(shù)據(jù)訪問碰撞的必要條件之一是數(shù)據(jù)被至少一個(gè)使用者寫且同時(shí)被至少一個(gè)其他使用者讀或?qū)?,所以在一個(gè)值被可變引用時(shí)不允許再次被任何引用。

垂懸引用(Dangling References)

這是一個(gè)換了個(gè)名字的概念,如果放在有指針概念的編程語言里它就指的是那種沒有實(shí)際指向一個(gè)真正能訪問的數(shù)據(jù)的指針(注意,不一定是空指針,還有可能是已經(jīng)釋放的資源)。它們就像失去懸掛物體的繩子,所以叫"垂懸引用"。

"垂懸引用"在 Rust 語言里不允許出現(xiàn),如果有,編譯器會(huì)發(fā)現(xiàn)它。

下面是一個(gè)垂懸的典型案例:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

很顯然,伴隨著 dangle 函數(shù)的結(jié)束,其局部變量的值本身沒有被當(dāng)作返回值,被釋放了。但它的引用卻被返回,這個(gè)引用所指向的值已經(jīng)不能確定的存在,故不允許其出現(xiàn)。

丰满人妻一级特黄a大片,午夜无码免费福利一级,欧美亚洲精品在线,国产婷婷成人久久Av免费高清