所有權(Ownership)

在進入正題之前,大家先回憶下一般的編程語言知識。 對於一般的編程語言,通常會先聲明一個變量,然後初始化它。 例如在C語言中:

int* foo() {
    int a;          // 變量a的作用域開始
    a = 100;
    char *c = "xyz";   // 變量c的作用域開始
    return &a;
}                   // 變量a和c的作用域結束

儘管可以編譯通過,但這是一段非常糟糕的代碼,現實中我相信大家都不會這麼去寫。變量a和c都是局部變量,函數結束後將局部變量a的地址返回,但局部變量a存在棧中,在離開作用域後,局部變量所申請的棧上內存都會被系統回收,從而造成了Dangling Pointer的問題。這是一個非常典型的內存安全問題。很多編程語言都存在類似這樣的內存安全問題。再來看變量cc的值是常量字符串,存儲於常量區,可能這個函數我們只調用了一次,我們可能不再想使用這個字符串,但xyz只有當整個程序結束後系統才能回收這片內存,這點讓程序員是不是也很無奈?

備註:對於xyz,可根據實際情況,通過堆的方式,手動管理(申請和釋放)內存。

所以,內存安全和內存管理通常是程序員眼中的兩大頭疼問題。令人興奮的是,Rust卻不再讓你擔心內存安全問題,也不用再操心內存管理的麻煩,那Rust是如何做到這一點的?請往下看。

綁定(Binding)

重要:首先必須強調下,準確地說Rust中並沒有變量這一概念,而應該稱為標識符,目標資源(內存,存放value)綁定到這個標識符

{
    let x: i32;       // 標識符x, 沒有綁定任何資源
    let y: i32 = 100; // 標識符y,綁定資源100
}

好了,我們繼續看下以下一段Rust代碼:

{
    let a: i32;
    println!("{}", a);
}

上面定義了一個i32類型的標識符a,如果你直接println!,你會收到一個error報錯:

error: use of possibly uninitialized variable: a

這是因為Rust並不會像其他語言一樣可以為變量默認初始化值,Rust明確規定變量的初始值必須由程序員自己決定

正確的做法:

{
    let a: i32;
    a = 100; //必須初始化a
    println!("{}", a);
}

其實,let關鍵字並不只是聲明變量的意思,它還有一層特殊且重要的概念-綁定。通俗的講,let關鍵字可以把一個標識符和一段內存區域做“綁定”,綁定後,這段內存就被這個標識符所擁有,這個標識符也成為這段內存的唯一所有者。 所以,a = 100發生了這麼幾個動作,首先在棧內存上分配一個i32的資源,並填充值100,隨後,把這個資源與a做綁定,讓a成為資源的所有者(Owner)。

作用域

像C語言一樣,Rust通過{}大括號定義作用域:

{
    {
        let a: i32 = 100;
    }
    println!("{}", a);
}

編譯後會得到如下error錯誤:

b.rs:3:20: 3:21 error: unresolved name a [E0425] b.rs:3 println!("{}", a);

像C語言一樣,在局部變量離開作用域後,變量隨即會被銷燬;但不同是,Rust會連同變量綁定的內存,不管是否為常量字符串,連同所有者變量一起被銷燬釋放。所以上面的例子,a銷燬後再次訪問a就會提示無法找到變量a的錯誤。這些所有的一切都是在編譯過程中完成的。

移動語義(move)

先看如下代碼:

{
    let a: String = String::from("xyz");
    let b = a;
    println!("{}", a);
}

編譯後會得到如下的報錯:

c.rs:4:20: 4:21 error: use of moved value: a [E0382] c.rs:4 println!("{}", a);

錯誤的意思是在println中訪問了被moved的變量a。那為什麼會有這種報錯呢?具體含義是什麼? 在Rust中,和“綁定”概念相輔相成的另一個機制就是“轉移move所有權”,意思是,可以把資源的所有權(ownership)從一個綁定轉移(move)成另一個綁定,這個操作同樣通過let關鍵字完成,和綁定不同的是,=兩邊的左值和右值均為兩個標識符:

語法:
    let 標識符A = 標識符B;  // 把“B”綁定資源的所有權轉移給“A”

move前後的內存示意如下:

Before move:
a <=> 內存(地址:A,內容:"xyz")
After move:
a
b <=> 內存(地址:A,內容:"xyz")

被move的變量不可以繼續被使用。否則提示錯誤error: use of moved value

這裡有些人可能會疑問,move後,如果變量A和變量B離開作用域,所對應的內存會不會造成“Double Free”的問題?答案是否定的,Rust規定,只有資源的所有者銷燬後才釋放內存,而無論這個資源是否被多次move,同一時刻只有一個owner,所以該資源的內存也只會被free一次。 通過這個機制,就保證了內存安全。是不是覺得很強大?

Copy特性

有讀者仿照“move”小節中的例子寫了下面一個例子,然後說“a被move後是可以訪問的”:

    let a: i32 = 100;
    let b = a;
    println!("{}", a);

編譯確實可以通過,輸出為100。這是為什麼呢,是不是跟move小節裡的結論相悖了? 其實不然,這其實是根據變量類型是否實現Copy特性決定的。對於實現Copy特性的變量,在move時會拷貝資源到新內存區域,並把新內存區域的資源bindingb

Before move:
a <=> 內存(地址:A,內容:100)
After move:
a <=> 內存(地址:A,內容:100)
b <=> 內存(地址:B,內容:100)

move前後的ab對應資源內存的地址不同。

在Rust中,基本數據類型(Primitive Types)均實現了Copy特性,包括i8, i16, i32, i64, usize, u8, u16, u32, u64, f32, f64, (), bool, char等等。其他支持Copy的數據類型可以參考官方文檔的Copy章節

淺拷貝與深拷貝

前面例子中move String和i32用法的差異,其實和很多面向對象編程語言中“淺拷貝”和“深拷貝”的區別類似。對於基本數據類型來說,“深拷貝”和“淺拷貝“產生的效果相同。對於引用對象類型來說,”淺拷貝“更像僅僅拷貝了對象的內存地址。 如果我們想實現對String的”深拷貝“怎麼辦? 可以直接調用String的Clone特性實現對內存的值拷貝而不是簡單的地址拷貝。

{
    let a: String = String::from("xyz");
    let b = a.clone();  // <-注意此處的clone
    println!("{}", a);
}

這個時候可以編譯通過,並且成功打印"xyz"。

clone後的效果等同如下:

Before move:
a <=> 內存(地址:A,內容:"xyz")
After move:
a <=> 內存(地址:A,內容:"xyz")
b <=> 內存(地址:B,內容:"xyz")
注意,然後a和b對應的資源值相同,但是內存地址並不一樣。

可變性

通過上面,我們已經已經瞭解了變量聲明、值綁定、以及移動move語義等等相關知識,但是還沒有進行過修改變量值這麼簡單的操作,在其他語言中看似簡單到不值得一提的事卻在Rust中暗藏玄機。 按照其他編程語言思維,修改一個變量的值:

let a: i32 = 100;
a = 200;

很抱歉,這麼簡單的操作依然還會報錯:

error: re-assignment of immutable variable a [E0384]

:3 a = 200;

不能對不可變綁定賦值。如果要修改值,必須用關鍵字mut聲明綁定為可變的:

let mut a: i32 = 100;  // 通過關鍵字mut聲明a是可變的
a = 200;

想到“不可變”我們第一時間想到了const常量,但不可變綁定與const常量是完全不同的兩種概念;首先,“不可變”準確地應該稱為“不可變綁定”,是用來約束綁定行為的,“不可變綁定”後不能通過原“所有者”更改資源內容。

例如:

let a = vec![1, 2, 3];  //不可變綁定, a <=> 內存區域A(1,2,3)
let mut a = a;  //可變綁定, a <=> 內存區域A(1,2,3), 注意此a已非上句a,只是名字一樣而已
a.push(4);
println!("{:?}", a);  //打印:[1, 2, 3, 4]

“可變綁定”後,目標內存還是同一塊,只不過,可以通過新綁定的a去修改這片內存了。

let mut a: &str = "abc";  //可變綁定, a <=> 內存區域A("abc")
a = "xyz";    //綁定到另一內存區域, a <=> 內存區域B("xyz")
println!("{:?}", a);  //打印:"xyz"

上面這種情況不要混淆了,a = "xyz"表示a綁定目標資源發生了變化。

其實,Rust中也有const常量,常量不存在“綁定”之說,和其他語言的常量含義相同:

const PI:f32 = 3.14;

可變性的目的就是嚴格區分綁定的可變性,以便編譯器可以更好的優化,也提高了內存安全性。

高級Copy特性

在前面的小節有簡單瞭解Copy特性,接下來我們來深入瞭解下這個特性。 Copy特性定義在標準庫std::marker::Copy中:

pub trait Copy: Clone { }

一旦一種類型實現了Copy特性,這就意味著這種類型可以通過的簡單的位(bits)拷貝實現拷貝。從前面知識我們知道“綁定”存在move語義(所有權轉移),但是,一旦這種類型實現了Copy特性,會先拷貝內容到新內存區域,然後把新內存區域和這個標識符做綁定。

哪些情況下我們自定義的類型(如某個Struct等)可以實現Copy特性? 只要這種類型的屬性類型都實現了Copy特性,那麼這個類型就可以實現Copy特性。 例如:

struct Foo {  //可實現Copy特性
    a: i32,
    b: bool,
}

struct Bar {  //不可實現Copy特性
    l: Vec<i32>,
}

因為Foo的屬性ab的類型i32bool均實現了Copy特性,所以Foo也是可以實現Copy特性的。但對於Bar來說,它的屬性lVec<T>類型,這種類型並沒有實現Copy特性,所以Bar也是無法實現Copy特性的。

那麼我們如何來實現Copy特性呢? 有兩種方式可以實現。

  1. 通過derive讓Rust編譯器自動實現

     #[derive(Copy, Clone)]
     struct Foo {
         a: i32,
         b: bool,
     }
    

    編譯器會自動檢查Foo的所有屬性是否實現了Copy特性,一旦檢查通過,便會為Foo自動實現Copy特性。

  2. 手動實現CloneCopy trait

     #[derive(Debug)]
     struct Foo {
         a: i32,
         b: bool,
     }
     impl Copy for Foo {}
     impl Clone for Foo {
         fn clone(&self) -> Foo {
             Foo{a: self.a, b: self.b}
         }
     }
     fn main() {
         let x = Foo{ a: 100, b: true};
         let mut y = x;
         y.b = false;
    
         println!("{:?}", x);  //打印:Foo { a: 100, b: true }
         println!("{:?}", y);  //打印:Foo { a: 100, b: false }
     }
    

    從結果我們發現let mut y = x後,x並沒有因為所有權move而出現不可訪問錯誤。 因為Foo繼承了Copy特性和Clone特性,所以例子中我們實現了這兩個特性。

高級move

我們從前面的小節瞭解到,let綁定會發生所有權轉移的情況,但ownership轉移卻因為資源類型是否實現Copy特性而行為不同:

let x: T = something;
let y = x;
  • 類型T沒有實現Copy特性:x所有權轉移到y
  • 類型T實現了Copy特性:拷貝x所綁定的資源新資源,並把新資源的所有權綁定給yx依然擁有原資源的所有權。
move關鍵字

move關鍵字常用在閉包中,強制閉包獲取所有權。

例子1:

fn main() {
    let x: i32 = 100;
    let some_closure = move |i: i32| i + x;
    let y = some_closure(2);
    println!("x={}, y={}", x, y);
}

結果: x=100, y=102

注意: 例子1是比較特別的,使不使用 move 對結果都沒什麼影響,因為x綁定的資源是i32類型,屬於 primitive type,實現了 Copy trait,所以在閉包使用 move 的時候,是先 copy 了x ,在 move 的時候是 move 了這份 clone 的 x,所以後面的 println!引用 x 的時候沒有報錯。

例子2:

fn main() {
    let mut x: String = String::from("abc");
    let mut some_closure = move |c: char| x.push(c);
    let y = some_closure('d');
    println!("x={:?}", x);
}

報錯: error: use of moved value: x [E0382]

:5 println!("x={:?}", x);

這是因為move關鍵字,會把閉包中的外部變量的所有權move到包體內,發生了所有權轉移的問題,所以println訪問x會如上錯誤。如果我們去掉println就可以編譯通過。

那麼,如果我們想在包體外依然訪問x,即x不失去所有權,怎麼辦?

fn main() {
    let mut x: String = String::from("abc");
    {
        let mut some_closure = |c: char| x.push(c);
        some_closure('d');
    }
    println!("x={:?}", x);  //成功打印:x="abcd"
}

我們只是去掉了move,去掉move後,包體內就會對x進行了可變借用,而不是“剝奪”x的所有權,細心的同學還注意到我們在前後還加了{}大括號作用域,是為了作用域結束後讓可變借用失效,這樣println才可以成功訪問並打印我們期待的內容。

關於“Borrowing借用”知識我們會在下一個大節中詳細講解。

results matching ""

    No results matching ""