鍍金池/ 教程/ Java/ 所有權(quán)(Ownership)
標(biāo)準(zhǔn)輸入與輸出
消息傳遞
循環(huán)
注釋
Rust for Mac OS
幾種智能指針
Cell, RefCell
trait對(duì)象 (trait object)
rust web 開發(fā)
Unsafe、原始指針
Macro
迭代器
函數(shù)
Borrow, BorrowMut, ToOwned
快速上手
二叉樹
編輯器
測(cè)試與評(píng)測(cè)
Deref
安裝Rust
哈希表 HashMap
原生類型
17.錯(cuò)誤處理
VS Code 安裝配置
動(dòng)態(tài)數(shù)組Vec
模式匹配
操作符和格式化字符串
Rust for Linux
函數(shù)參數(shù)
Visual Studio
vim/GVim安裝配置
閉包作為參數(shù)和返回值
安全(Safety)
Cow
生命周期( Lifetime )
閉包的實(shí)現(xiàn)
所有權(quán)(Ownership)
Atom
將Rust編譯成庫
類型、運(yùn)算符和字符串
類型系統(tǒng)中的幾個(gè)常見 trait
特性
屬性和編譯器參數(shù)
Spacemacs
集合類型
Rust json處理
Heap & Stack
并行
標(biāo)準(zhǔn)庫示例
基本程序結(jié)構(gòu)
鏈表
trait 和 trait對(duì)象
前期準(zhǔn)備
代碼風(fēng)格
編譯器參數(shù)
基于語義化版本的項(xiàng)目版本聲明與管理
Rust 版本管理工具: rustup
引用&借用(References&Borrowing)
注釋與文檔
10.1 trait關(guān)鍵字
模式
調(diào)用ffi函數(shù)
unsafe
并發(fā),并行,多線程編程
AsRef 和 AsMut
Rust旅程
Rust for Windows
結(jié)構(gòu)體與枚舉
條件分支
附錄I-術(shù)語表
變量綁定與原生類型
Mutex 與 RwLock
泛型
裸指針
常用數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)
系統(tǒng)命令:調(diào)用grep
Into/From 及其在 String 和 &str 互轉(zhuǎn)上的應(yīng)用
共享內(nèi)存
Sublime
網(wǎng)絡(luò)模塊:W貓的回音
函數(shù)返回值
包和模塊
高階函數(shù)
函數(shù)與方法
match關(guān)鍵字
隊(duì)列
目錄操作:簡(jiǎn)單grep
語句和表達(dá)式
并發(fā)編程
閉包
測(cè)試
閉包的語法
同步
迭代器
String
Send 和 Sync
Rc 和 Arc
屬性
Emacs
優(yōu)先隊(duì)列
Prelude
cargo簡(jiǎn)介
控制流(control flow)
數(shù)組、動(dòng)態(tài)數(shù)組和字符串
FFI
模塊和包系統(tǒng)、Prelude
實(shí)戰(zhàn)篇
Rust 是一門系統(tǒng)級(jí)編程語言,被設(shè)計(jì)為保證內(nèi)存和線程安全,并防止段錯(cuò)誤。作為系統(tǒng)級(jí)編程語言,它的基本理念是 “零開銷抽象”。理
運(yùn)算符重載
Any和反射
rust數(shù)據(jù)庫操作
輸入輸出流
復(fù)合類型
性能測(cè)試

所有權(quán)(Ownership)

所有權(quán)(Ownership)

在進(jìn)入正題之前,大家先回憶下一般的編程語言知識(shí)。 對(duì)于一般的編程語言,通常會(huì)先聲明一個(gè)變量,然后初始化它。 例如在C語言中:

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

盡管可以編譯通過,但這是一段非常糟糕的代碼,現(xiàn)實(shí)中我相信大家都不會(huì)這么去寫。變量a和c都是局部變量,函數(shù)結(jié)束后將局部變量a的地址返回,但局部變量a存在棧中,在離開作用域后,局部變量所申請(qǐng)的棧上內(nèi)存都會(huì)被系統(tǒng)回收,從而造成了Dangling Pointer的問題。這是一個(gè)非常典型的內(nèi)存安全問題。很多編程語言都存在類似這樣的內(nèi)存安全問題。再來看變量c,c的值是常量字符串,存儲(chǔ)于常量區(qū),可能這個(gè)函數(shù)我們只調(diào)用了一次,我們可能不再想使用這個(gè)字符串,但xyz只有當(dāng)整個(gè)程序結(jié)束后系統(tǒng)才能回收這片內(nèi)存,這點(diǎn)讓程序員是不是也很無奈?

備注:對(duì)于xyz,可根據(jù)實(shí)際情況,通過堆的方式,手動(dòng)管理(申請(qǐng)和釋放)內(nèi)存。

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

綁定(Binding)

重要:首先必須強(qiáng)調(diào)下,準(zhǔn)確地說Rust中并沒有變量這一概念,而應(yīng)該稱為標(biāo)識(shí)符,目標(biāo)資源(內(nèi)存,存放value)綁定到這個(gè)標(biāo)識(shí)符

{
    let x: i32;       // 標(biāo)識(shí)符x, 沒有綁定任何資源
    let y: i32 = 100; // 標(biāo)識(shí)符y,綁定資源100
}

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

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

上面定義了一個(gè)i32類型的標(biāo)識(shí)符a,如果你直接println!,你會(huì)收到一個(gè)error報(bào)錯(cuò):

error: use of possibly uninitialized variable: a

這是因?yàn)镽ust并不會(huì)像其他語言一樣可以為變量默認(rèn)初始化值,Rust明確規(guī)定變量的初始值必須由程序員自己決定。

正確的做法:

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

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

作用域

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

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

編譯后會(huì)得到如下error錯(cuò)誤:

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

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

移動(dòng)語義(move)

先看如下代碼:

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

編譯后會(huì)得到如下的報(bào)錯(cuò):

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

錯(cuò)誤的意思是在println中訪問了被moved的變量a。那為什么會(huì)有這種報(bào)錯(cuò)呢?具體含義是什么? 在Rust中,和“綁定”概念相輔相成的另一個(gè)機(jī)制就是“轉(zhuǎn)移move所有權(quán)”,意思是,可以把資源的所有權(quán)(ownership)從一個(gè)綁定轉(zhuǎn)移(move)成另一個(gè)綁定,這個(gè)操作同樣通過let關(guān)鍵字完成,和綁定不同的是,=兩邊的左值和右值均為兩個(gè)標(biāo)識(shí)符:

語法:
    let 標(biāo)識(shí)符A = 標(biāo)識(shí)符B;  // 把“B”綁定資源的所有權(quán)轉(zhuǎn)移給“A”

move前后的內(nèi)存示意如下:

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

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

這里有些人可能會(huì)疑問,move后,如果變量A和變量B離開作用域,所對(duì)應(yīng)的內(nèi)存會(huì)不會(huì)造成“Double Free”的問題?答案是否定的,Rust規(guī)定,只有資源的所有者銷毀后才釋放內(nèi)存,而無論這個(gè)資源是否被多次move,同一時(shí)刻只有一個(gè)owner,所以該資源的內(nèi)存也只會(huì)被free一次。 通過這個(gè)機(jī)制,就保證了內(nèi)存安全。是不是覺得很強(qiáng)大?

Copy特性

有讀者仿照“move”小節(jié)中的例子寫了下面一個(gè)例子,然后說“a被move后是可以訪問的”:

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

編譯確實(shí)可以通過,輸出為100。這是為什么呢,是不是跟move小節(jié)里的結(jié)論相悖了? 其實(shí)不然,這其實(shí)是根據(jù)變量類型是否實(shí)現(xiàn)Copy特性決定的。對(duì)于實(shí)現(xiàn)Copy特性的變量,在move時(shí)會(huì)拷貝資源到新內(nèi)存區(qū)域,并把新內(nèi)存區(qū)域的資源bindingb。

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

move前后的ab對(duì)應(yīng)資源內(nèi)存的地址不同。

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

淺拷貝與深拷貝

前面例子中move String和i32用法的差異,其實(shí)和很多面向?qū)ο缶幊陶Z言中“淺拷貝”和“深拷貝”的區(qū)別類似。對(duì)于基本數(shù)據(jù)類型來說,“深拷貝”和“淺拷貝“產(chǎn)生的效果相同。對(duì)于引用對(duì)象類型來說,”淺拷貝“更像僅僅拷貝了對(duì)象的內(nèi)存地址。 如果我們想實(shí)現(xiàn)對(duì)String的”深拷貝“怎么辦? 可以直接調(diào)用String的Clone特性實(shí)現(xiàn)對(duì)內(nèi)存的值拷貝而不是簡(jiǎn)單的地址拷貝。

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

這個(gè)時(shí)候可以編譯通過,并且成功打印"xyz"。

clone后的效果等同如下:

Before move:
a <=> 內(nèi)存(地址:A,內(nèi)容:"xyz")
After move:
a <=> 內(nèi)存(地址:A,內(nèi)容:"xyz")
b <=> 內(nèi)存(地址:B,內(nèi)容:"xyz")
注意,然后a和b對(duì)應(yīng)的資源值相同,但是內(nèi)存地址并不一樣。

可變性

通過上面,我們已經(jīng)已經(jīng)了解了變量聲明、值綁定、以及移動(dòng)move語義等等相關(guān)知識(shí),但是還沒有進(jìn)行過修改變量值這么簡(jiǎn)單的操作,在其他語言中看似簡(jiǎn)單到不值得一提的事卻在Rust中暗藏玄機(jī)。 按照其他編程語言思維,修改一個(gè)變量的值:

let a: i32 = 100;
a = 200;

很抱歉,這么簡(jiǎn)單的操作依然還會(huì)報(bào)錯(cuò):

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

:3 a = 200;

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

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

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

例如:

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

“可變綁定”后,目標(biāo)內(nèi)存還是同一塊,只不過,可以通過新綁定的a去修改這片內(nèi)存了。

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

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

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

const PI:f32 = 3.14;

可變性的目的就是嚴(yán)格區(qū)分綁定的可變性,以便編譯器可以更好的優(yōu)化,也提高了內(nèi)存安全性。

高級(jí)Copy特性

在前面的小節(jié)有簡(jiǎn)單了解Copy特性,接下來我們來深入了解下這個(gè)特性。 Copy特性定義在標(biāo)準(zhǔn)庫std::marker::Copy中:

pub trait Copy: Clone { }

一旦一種類型實(shí)現(xiàn)了Copy特性,這就意味著這種類型可以通過的簡(jiǎn)單的位(bits)拷貝實(shí)現(xiàn)拷貝。從前面知識(shí)我們知道“綁定”存在move語義(所有權(quán)轉(zhuǎn)移),但是,一旦這種類型實(shí)現(xiàn)了Copy特性,會(huì)先拷貝內(nèi)容到新內(nèi)存區(qū)域,然后把新內(nèi)存區(qū)域和這個(gè)標(biāo)識(shí)符做綁定。

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

struct Foo {  //可實(shí)現(xiàn)Copy特性
    a: i32,
    b: bool,
}

struct Bar {  //不可實(shí)現(xiàn)Copy特性
    l: Vec<i32>,
}

因?yàn)?code>Foo的屬性ab的類型i32bool均實(shí)現(xiàn)了Copy特性,所以Foo也是可以實(shí)現(xiàn)Copy特性的。但對(duì)于Bar來說,它的屬性lVec<T>類型,這種類型并沒有實(shí)現(xiàn)Copy特性,所以Bar也是無法實(shí)現(xiàn)Copy特性的。

那么我們?nèi)绾蝸韺?shí)現(xiàn)Copy特性呢? 有兩種方式可以實(shí)現(xiàn)。

  1. 通過derive讓Rust編譯器自動(dòng)實(shí)現(xiàn)

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

    編譯器會(huì)自動(dòng)檢查Foo的所有屬性是否實(shí)現(xiàn)了Copy特性,一旦檢查通過,便會(huì)為Foo自動(dòng)實(shí)現(xiàn)Copy特性。

  2. 手動(dòng)實(shí)現(xiàn)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);  //打?。篎oo { a: 100, b: true }
          println!("{:?}", y);  //打印:Foo { a: 100, b: false }
      }
    

    從結(jié)果我們發(fā)現(xiàn)let mut y = x后,x并沒有因?yàn)樗袡?quán)move而出現(xiàn)不可訪問錯(cuò)誤。 因?yàn)?code>Foo繼承了Copy特性和Clone特性,所以例子中我們實(shí)現(xiàn)了這兩個(gè)特性。

高級(jí)move

我們從前面的小節(jié)了解到,let綁定會(huì)發(fā)生所有權(quán)轉(zhuǎn)移的情況,但ownership轉(zhuǎn)移卻因?yàn)橘Y源類型是否實(shí)現(xiàn)Copy特性而行為不同:

let x: T = something;
let y = x;
  • 類型T沒有實(shí)現(xiàn)Copy特性:x所有權(quán)轉(zhuǎn)移到y。
  • 類型T實(shí)現(xiàn)了Copy特性:拷貝x所綁定的資源新資源,并把新資源的所有權(quán)綁定給y,x依然擁有原資源的所有權(quán)。
move關(guān)鍵字

move關(guān)鍵字常用在閉包中,強(qiáng)制閉包獲取所有權(quán)。

例子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);
}

結(jié)果: x=100, y=102

注意: 例子1是比較特別的,使不使用 move 對(duì)結(jié)果都沒什么影響,因?yàn)?code>x綁定的資源是i32類型,屬于 primitive type,實(shí)現(xiàn)了 Copy trait,所以在閉包使用 move 的時(shí)候,是先 copy 了x ,在 move 的時(shí)候是 move 了這份 clone 的 x,所以后面的 println!引用 x 的時(shí)候沒有報(bào)錯(cuò)。

例子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);
}

報(bào)錯(cuò): error: use of moved value: x [E0382]

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

這是因?yàn)閙ove關(guān)鍵字,會(huì)把閉包中的外部變量的所有權(quán)move到包體內(nèi),發(fā)生了所有權(quán)轉(zhuǎn)移的問題,所以println訪問x會(huì)如上錯(cuò)誤。如果我們?nèi)サ?code>println就可以編譯通過。

那么,如果我們想在包體外依然訪問x,即x不失去所有權(quán),怎么辦?

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

我們只是去掉了move,去掉move后,包體內(nèi)就會(huì)對(duì)x進(jìn)行了可變借用,而不是“剝奪”x的所有權(quán),細(xì)心的同學(xué)還注意到我們?cè)谇昂筮€加了{}大括號(hào)作用域,是為了作用域結(jié)束后讓可變借用失效,這樣println才可以成功訪問并打印我們期待的內(nèi)容。

關(guān)于“Borrowing借用”知識(shí)我們會(huì)在下一個(gè)大節(jié)中詳細(xì)講解。

上一篇:泛型下一篇:Heap &amp; Stack