一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

Rust 所有權(quán):值的生與死,由誰來掌控?

 古明地覺O_o 2022-12-08 發(fā)布于北京

楔子

所有權(quán)可以說是 Rust 里面非常獨特的一個功能了,正是所有權(quán)概念和相關(guān)工具的引入,Rust 才能在沒有垃圾回收機(jī)制的前提下保障內(nèi)存安全。因此正確地了解所有權(quán)概念、以及它在 Rust 中的實現(xiàn)方式,對于所有 Rust 開發(fā)者來講都是十分重要的。

所有權(quán)概念本身的含義并不復(fù)雜,但作為 Rust 語言的核心功能,它對語言的其他部分產(chǎn)生了十分深遠(yuǎn)的影響。

一般來講,所有程序都需要管理自己在運行時使用的內(nèi)存空間,某些帶有垃圾回收機(jī)制的語言會在運行時定期檢查并回收那些不再使用的內(nèi)存;而在另外一些語言中,程序員需要手動地分配和釋放內(nèi)存。Rust 采用了與眾不同的第三種方式:它使用包含特定規(guī)則的所有權(quán)系統(tǒng)來管理內(nèi)存,這套規(guī)則允許編譯器在編譯過程中執(zhí)行檢查工作,而不會產(chǎn)生任何的運行時開銷。

因此所有權(quán)是 Rust 能將高效和安全兩方面同時兼顧的原因,而本次我們會通過一些示例來學(xué)習(xí)所有權(quán),這些示例將聚焦于一個十分常用的數(shù)據(jù)結(jié)構(gòu):字符串。


棧與堆

在許多編程語言中,程序員不需要頻繁地考慮??臻g和堆空間的區(qū)別。但對于 Rust 這樣的系統(tǒng)級編程語言來說,一個值被存儲在棧上還是被存儲在堆上,會極大地影響到語言的行為,進(jìn)而影響到我們編寫代碼時的設(shè)計抉擇。由于所有權(quán)的某些內(nèi)容會涉及棧與堆,所以讓我們再來復(fù)習(xí)一下它們。

棧和堆都是代碼在運行時可以使用的內(nèi)存空間,不過它們通常以不同的結(jié)構(gòu)組織而成。棧會以我們放入值時的順序來存儲它們,并以相反的順序?qū)⒅等〕觯@也就是所謂的后進(jìn)先出(Last In First Out,LIFO)策略。

你可以把棧上的操作想象成堆放盤子:當(dāng)你需要放置盤子時,你只能將它們放置在最上面,而當(dāng)你需要取出盤子時,你也只能從最上面取出。換句話說,你沒有辦法從中間或底部插入、移除盤子。用術(shù)語來講,添加數(shù)據(jù)這一操作被稱作入棧,移除數(shù)據(jù)則被稱作出棧。

所有存儲在棧中的數(shù)據(jù)都必須擁有一個已知且固定的大小,對于那些在編譯期無法確定大小的數(shù)據(jù),只能將它們存儲在堆中(在棧上是不安全的)。

而堆空間的管理較為松散:當(dāng)你希望將數(shù)據(jù)放入堆中時,你就可以請求特定大小的空間。操作系統(tǒng)會根據(jù)你的請求在堆中找到一塊足夠大的可用空間,將它標(biāo)記為已使用,并把指向這片空間的指針返回。這一過程就是所謂的堆分配,它也常常被簡稱為分配,至于將值壓入棧中則不叫分配。

由于指針的大小是固定的,且可以在編譯期確定(64位系統(tǒng)固定 8 字節(jié)),所以會將指針存儲在棧中,也就是棧區(qū)的指針指向堆區(qū)的數(shù)據(jù)。

可以把堆上的操作想象成到餐廳聚餐,當(dāng)你到達(dá)餐廳表明自己需要的座位數(shù)后,服務(wù)員會找到一張足夠大的空桌子,并將你們領(lǐng)過去入座。即便這時有小伙伴來遲了,他們也可以通過詢問你們就座的位置來找到你們。

向棧上壓入數(shù)據(jù)要遠(yuǎn)比在堆上進(jìn)行分配更有效率,因為如果是堆的話,操作系統(tǒng)還要搜索新數(shù)據(jù)的存儲位置,需要額外開銷;但棧不用,對于棧而言這個位置永遠(yuǎn)處于棧的頂端。除此之外,操作系統(tǒng)在堆上分配空間時還必須首先找到足夠放下對應(yīng)數(shù)據(jù)的空間,并進(jìn)行某些記錄,來協(xié)調(diào)隨后的其余分配操作。

訪問數(shù)據(jù)也是同理,由于指針存在棧上,數(shù)據(jù)存在堆上,所以要通過指針存儲的地址來訪問數(shù)據(jù)。而這會多一步指針跳轉(zhuǎn)的環(huán)節(jié),因此訪問堆上的數(shù)據(jù)要慢于訪問棧上的數(shù)據(jù)。一般來說,現(xiàn)代處理器在進(jìn)行計算的過程中,由于緩存的緣故,指令在內(nèi)存中跳轉(zhuǎn)的次數(shù)越多,性能就越差。

繼續(xù)使用上面的餐廳來作類比,假設(shè)現(xiàn)在同時有許多桌的顧客正在等待服務(wù)員的處理。那么最高效的處理方式自然是報完一張桌子所有的訂單之后再接著服務(wù)下一張桌子的顧客。而一旦服務(wù)員每次在單個桌子前只處理單個訂單,那么他就不得不浪費較多的時間往返于不同的桌子之間。

出于同樣的原因,處理器操作排布緊密的數(shù)據(jù)(在棧上)要比操作排布稀疏的數(shù)據(jù)(在堆上)有效率得多。另外,分配命令本身也可能消耗不少時鐘周期。


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

現(xiàn)在讓我們來具體看一看所有權(quán)規(guī)則,先將這些規(guī)則記下來,我們隨后會通過示例來解釋它們:

  • Rust 中的每一個值都有一個對應(yīng)的變量作為它的所有者;

  • 在同一時間內(nèi),值有且僅有一個所有者;

  • 當(dāng)所有者離開自己的作用域時,它持有的值就會被釋放掉;

作為所有權(quán)的第一個示例,我們先來了解一下變量的作用域。簡單來講,作用域是一個對象在程序中有效的范圍,假設(shè)有這樣一個變量:

fn f() {
    {
        println!("你好 世界");
        // 從這里開始,變量 s 被聲明
        // 我們可以使用這個變量了,之前都是不可用的
        let s = "hello world";  
        println!("{}", s);
    }  // 出了這個大括號,就不在變量 s 的作用域內(nèi)了
    // 此時再使用 s 這個變量就會報錯,s 的值也會被釋放掉
    // println!("{}", s);
}

因此要注意兩個關(guān)鍵點:

  • 1)變量在進(jìn)入作用域后變得有效;

  • 2)它會保持自己的有效性直到自己離開作用域為止;

所以 Rust 語言中變量的有效性與作用域之間的關(guān)系跟其它編程語言非常類似,現(xiàn)在讓我們繼續(xù)在作用域的基礎(chǔ)上學(xué)習(xí) String 類型。


String 類型

為了演示所有權(quán)的相關(guān)規(guī)則,我們需要一個特別的數(shù)據(jù)類型,它比之前介紹的數(shù)據(jù)類型(比如整型、浮點型等等)都要復(fù)雜。因為之前接觸的數(shù)據(jù)都存儲在棧上,并在變量離開作用域時自動從??臻g

但 String 則不同,它的數(shù)據(jù)存在堆上。而之所以選擇 String,是因為我們現(xiàn)在需要通過一個數(shù)據(jù)存儲在堆上的類型,來研究 Rust 如何自動回收這些數(shù)據(jù)。

我們將以 String 類型為例,并將注意力集中到 String 類型與所有權(quán)概念相關(guān)的部分,這些部分同樣適用于標(biāo)準(zhǔn)庫中提供的或者我們自己創(chuàng)建的其它復(fù)雜數(shù)據(jù)類型,另外后續(xù)還會更加深入地講解 String 類型。

首先在最開始的時候我們就接觸過字符串,比如 println!("hello world"),這個宏里面就是一個字符串。只不過這種字符串也稱作字符串字面量,也就是被硬編碼進(jìn)程序的字符串值。

字符串字面量的確是很方便,但它并不能滿足所有需要使用文本的場景。原因之一在于字符串字面量是不可變的,而另一個原因則在于并不是所有字符串的值都能夠在編寫代碼時確定。假如我們想要獲取用戶的輸入并保存,但是我們不知道用戶事先會輸入多少個字符,這時應(yīng)該怎么辦呢?

為了應(yīng)對這種情況,Rust 提供了第二種字符串類型 String,來彌補字符串字面量的不足。而 String 類型的字符串會在堆上分配到自己需要的存儲空間,所以它能夠處理編譯時大小未知的文本,我們可以調(diào)用 from 函數(shù)根據(jù)字符串字面量來創(chuàng)建一個 String 實例:

fn main() {
    let mut s = String::from("hello");
}

這里的雙冒號運算符允許我們調(diào)用置于 String 命名空間下面的特定函數(shù) from,而不需要使用類似于 string_from 這樣的名字。我們會在后續(xù)著重講解這個語法,并討論基于模塊的命名空間。

上面定義的字符串對象是可以動態(tài)變化的:

fn main() {
    // 我們前面介紹過元組,無論是賦值一個新的元組
    // 還是修改元組內(nèi)部的某個值,都涉及到值的改變
    // 因此變量都要聲明為可變,而字符串也是同理
    let mut s = String::from("hello");
    // 在尾部增加字符串字面量,因為 String 類型的值是可變的
    // 所以變量 s 也要聲明為可變,否則調(diào)用 push_str 方法報錯
    // 因為我們是調(diào)用變量 s 來修改 String 類型的值
    s.push_str(" world");
    println!("{}", s);  // hello world
}

所以 String 是可變的,但是字符串字面量不允許改變:

fn main() {
    // s 的值是字符串字面量,它的值無法改變
    let mut s = "hello";
    // 如果想打印 hello world
    // 我們只能給 s 賦值為一個新的字符串字面量
    // 但是我們無法修改原來的字符串字面量
    // 同理整數(shù)、浮點數(shù)也是如此,它們都無法改變
    // 如果想改只能給變量賦一個新的值
    s = "hello world";
    println!("{}", s);  // hello world
    
    // 而 String 則不同,String 類型的值是可以進(jìn)行修改的
    // 因為它申請在堆區(qū),大小可變,我們無需重新賦值
    // 而是可以通過 s.push_str 在原本的值上進(jìn)行修改
}

但是問題來了,為什么 String 是可變的,而字符串字面量不是?這是因為它們采用了不同的內(nèi)存處理方式。


內(nèi)存與分配

對于字符串字面量而言,由于我們在編譯時就知道內(nèi)容,所以這部分硬編碼的文本被直接嵌入到了最終的可執(zhí)行文件中。這就是訪問字符串字面量異常高效的原因,而這些性質(zhì)完全得益于字符串字面量的不可變性。

但不幸的是,我們沒有辦法將那些未知大小的文本在編譯期統(tǒng)統(tǒng)放入二進(jìn)制文件中,更何況這些文本的大小還可能隨著程序的運行而發(fā)生改變。

對于 String 類型而言,為了支持一個可變的、可增長的文本類型,我們需要在堆上分配一塊在編譯時未知大小的內(nèi)存來存放數(shù)據(jù)。這同時也意味著:

  • 我們使用的內(nèi)存是由操作系統(tǒng)在運行時動態(tài)分配出來的,并且內(nèi)存是堆上的內(nèi)存;

  • 當(dāng)使用完 String 時,我們需要通過某種方式來將這些堆內(nèi)存歸還給操作系統(tǒng);

這里的第一步由我們,也就是程序的編寫者,在調(diào)用 String::from 時完成,這個函數(shù)會請求自己需要的內(nèi)存空間。在大部分編程語言中都有類似的設(shè)計,即:由程序員來發(fā)起堆內(nèi)存的分配請求。

然而對于不同的編程語言來說,第二步實現(xiàn)起來就各有區(qū)別了。在某些擁有垃圾回收機(jī)制的語言中,GC 會代替程序員來負(fù)責(zé)記錄并清除那些不再使用的內(nèi)存。而對于那些沒有 GC 的語言來說,識別不再使用的內(nèi)存并調(diào)用代碼顯式釋放的工作就依然需要由程序員去完成,和請求分配的時候一樣。

按照以往的經(jīng)驗來看,正確地完成這些任務(wù)往往是十分困難的,假如我們忘記釋放內(nèi)存,那么就會造成內(nèi)存泄漏;假如我們過早地釋放內(nèi)存,那么就會產(chǎn)生一個非法變量;假如我們重復(fù)釋放同一塊內(nèi)存,那么就會產(chǎn)生無法預(yù)知的后果。因此為了程序的穩(wěn)定運行,我們必須嚴(yán)格地將分配和釋放操作一一對應(yīng)起來。

但 Rust 與這些語言都不同,Rust 提供了另一套解決方案:內(nèi)存會自動地在擁有它的變量離開作用域后進(jìn)行釋放。

fn main() {
    {
        let mut s = String::from("hello");
    }  // 在此處 s 就失效了,因為離開了作用域
       // 并且它的值也會被回收
}

審視上面的代碼,會發(fā)現(xiàn)有一個很適合用來回收內(nèi)存的地方:也就是變量 s 離開作用域的地方。Rust 在變量離開作用域時,會調(diào)用一個叫作 drop 的特殊函數(shù),來對堆內(nèi)存進(jìn)行釋放。

這種模式極大地影響了 Rust 的許多設(shè)計抉擇,并最終決定了我們現(xiàn)在編寫 Rust 代碼的方式。在上面的例子中,這套釋放機(jī)制看起來也許還算簡單,然而一旦把它放置在某些更加復(fù)雜的環(huán)境中,代碼呈現(xiàn)出來的行為往往會出乎你的意料,特別是當(dāng)我們擁有多個指向同一處堆內(nèi)存的變量時。下面就來看一看。

變量和數(shù)據(jù)交互的方式:移動

Rust 中的多個變量可以采用一種獨特的方式與同一數(shù)據(jù)進(jìn)行交互。

fn main() {
    let x = 5;
    let y = x;
    println!("x = {}, y = {}", x, y);  
    // x = 5, y = 5
}

這段代碼的執(zhí)行效果很好理解:將整數(shù)值 5 綁定到變量 x 上;然后創(chuàng)建一個 x 值的拷貝,并將它綁定到 y 上。結(jié)果我們有了兩個變量 x 和 y,它們的值都是 5。

這正是實際發(fā)生的情形,因為整數(shù)是已知固定大小的簡單值,所以兩個值 5 會同時被推入當(dāng)前的棧中。但是這針對的也只是存放在棧上的數(shù)據(jù),因為棧上的數(shù)據(jù)由操作系統(tǒng)來維護(hù),函數(shù)結(jié)束時數(shù)據(jù)自動回收,不需要我們關(guān)心,非常方便,并且數(shù)據(jù)在棧上復(fù)制的效率也是非常高的。

棧上的數(shù)據(jù)在傳遞時永遠(yuǎn)都是拷貝一份,但棧上的內(nèi)存分配是非常高效的,和堆是天壤之別。只需要改動棧指針(stack pointer),就可以預(yù)留相應(yīng)的空間;把棧指針改動回來,預(yù)留的空間又會被釋放掉??臻g的申請和釋放只是動動寄存器,不涉及額外計算、不涉及系統(tǒng)調(diào)用,因而效率很高。并且這個過程還是由操作系統(tǒng)自動維護(hù),不需要我們關(guān)心。

可如果是堆區(qū)的數(shù)據(jù)就不一定了,比如 String,我們將上面的代碼改一下。

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

以上兩段代碼非常相似,你也許會假設(shè)它們的運行方式也是一致的。也就是說,第二行代碼可能會生成一個 s1 值的拷貝,并將它綁定到 s2 上。不過,事實并非如此,如果執(zhí)行上面這段代碼,會報出如下錯誤:value borrowed here after move,至于這個錯誤是什么意思,我們一會說。

我們先來看一下 String 的內(nèi)存布局:

String 實際上由 3 部分組成:指向存放具體字符串的指針(ptr)、長度(len)以及容量(capacity),這部分的數(shù)據(jù)存儲在了棧中,圖中的左半部分。然后 ptr 指向了字符串存儲在堆上的文本內(nèi)容,圖中的右半部分。

長度字段用來記錄當(dāng)前 String 中的文本使用了多少字節(jié)的內(nèi)存,而容量字段則用來記錄 String 向操作系統(tǒng)總共申請到的內(nèi)存字節(jié)數(shù)量。如果你用過 Go 的話,那么會發(fā)現(xiàn)這和 Go 里面切片的結(jié)構(gòu)是一樣的。

當(dāng)把 s1 賦值給 s2 的時候,會把 s1 拷貝一份給 s2,因為 s1 和 s2 都是棧上的數(shù)據(jù),所以會直接拷貝一份。我們說過棧上的數(shù)據(jù)拷貝的效率非常高,和堆根本不在一個層次,并且也不需要我們來維護(hù),只不過大小固定,不能動態(tài)變化,畢竟速度擺在那里。

但需要注意的是,這里的拷貝僅僅是針對棧上的數(shù)據(jù),字符串里面的 ptr 指向的存儲在堆區(qū)的文本并沒有拷貝。

這么做完全可以理解,因為在堆上拷貝數(shù)據(jù)的效率遠(yuǎn)不如棧,所以不能像棧那樣直接將數(shù)據(jù)拷貝一份。而且存在堆上的數(shù)據(jù)也可能會比較大,這樣的話拷貝就更加消耗資源了。

然后當(dāng)一個變量離開它所在的作用域時,它的值就會被釋放,這里釋放的不僅僅是棧上的值。比如這里的 s1,當(dāng)離開了作用域之后,釋放的不僅僅是棧上的字符串本身,字符串里面的 ptr 指向的堆區(qū)的內(nèi)存同樣會被釋放,這是顯然的。

但是問題來了,s1 和 s2 里面的 ptr 指向的是同一份堆內(nèi)存,因為將 s1 拷貝給 s2 的時候只拷貝了字符串(結(jié)構(gòu)體),堆內(nèi)存并沒有拷貝,所以如果 s1 和 s2 都離開作用域的時候,那么同一份堆內(nèi)存不就被釋放兩次了嗎?這就是臭名昭著的二次釋放,而重復(fù)釋放內(nèi)存可能會導(dǎo)致某些正在使用的數(shù)據(jù)發(fā)生損壞,進(jìn)而產(chǎn)生潛在的安全隱患。

fn main() {
    {
        let s1 = String::from("hello world");
        let s2 = s1;
    }  // 在此處會調(diào)用 drop 函數(shù)清理棧上的 s1 和 s2
       // 以及 ptr 指向的堆上的內(nèi)存
}

但問題是堆上的內(nèi)存真的會被釋放兩次嗎?很明顯不會的,而 Rust 的做法也很簡單,為了確保內(nèi)存安全,同時也避免復(fù)制分配堆內(nèi)存,Rust 會直接將 s1 廢棄,不再將其視為一個有效的變量,因此 Rust 也不需要在 s1 離開作用域后清理任何東西。

而以上便發(fā)生了所有權(quán)的轉(zhuǎn)移,一開始 s1 對堆內(nèi)存是持有所有權(quán)的,但當(dāng)把 s1 賦值給 s2 的時候就會發(fā)生所有權(quán)的轉(zhuǎn)移。也就是說 let s2 = s1 之后,s1 將不再具有操控這份堆內(nèi)存的權(quán)利,這個權(quán)利被交給了 s2。而所有權(quán)一旦轉(zhuǎn)移,那么之前的變量就不能再用了,我們舉個例子:

fn main() {
    // s1 此時持有堆內(nèi)存的所有權(quán)
    let s1 = String::from("hello world");
    {
        // 所有權(quán)發(fā)生轉(zhuǎn)移,s1 不再具有操控堆內(nèi)存的權(quán)利
        // 該權(quán)利交給了 s2,之后 s1 就不能再用了
        // 至于堆內(nèi)存是否被釋放則只取決于 s2,和 s1 無關(guān)
        // 因為當(dāng) let s2 = s1 的那一刻,s1 就已經(jīng)失去生命了
        let s2 = s1;
    }  // 離開了作用域,在此處會調(diào)用 drop 函數(shù)清理棧上的 s2
       // 以及 ptr 指向的堆上的內(nèi)存
       
   // 因此看似 s1 還沒有離開自己所在作用域
   // 但實際上它早在 let s2 = s1 的時候就因為所有權(quán)的轉(zhuǎn)移而不能再使用了
   // 所以接下來再打印 s1 是會報錯的
}

因此一定要理解所有權(quán)這個概念。

  • Rust 中的每個值都有一個變量,被稱為所有者;

  • 一個值同時只能有一個所有者,如果這個值里面維護(hù)了一個指針,該指針指向了一片堆內(nèi)存,那么這片堆內(nèi)存同時也只能有一個所有者;

  • 當(dāng)所有者離開作用域時,該值(連同指向的堆內(nèi)存)會被刪除;

  • 既然變量是值的所有者,那么所有權(quán)顯然就是變量操作值的權(quán)利;

對于 String 類型的變量來說,是值里面有一個指針,這個指針指向了一份堆內(nèi)存。而一旦發(fā)生所有權(quán)的轉(zhuǎn)移,那么該變量就沒有權(quán)利再操作這片堆內(nèi)存了,因為一片堆內(nèi)存同時只能有一個所有者。至于后續(xù)這片堆內(nèi)存是否釋放、何時釋放都和該變量無關(guān)。并且發(fā)生所有權(quán)轉(zhuǎn)移之后,該變量也不能再使用了,當(dāng)該變量離開自己的作用域時,也不會二次釋放堆內(nèi)存,因為它已經(jīng)失去對之前這片堆內(nèi)存的所有權(quán)。

所以一旦涉及到變量的所有權(quán)時,這些變量的值基本上都是內(nèi)部會有一個指向堆內(nèi)存的指針。因為像整數(shù)、浮點數(shù)、字符串字面量等等這些只會分配在棧上的值,變量傳遞的時候都是直接把值拷貝一份,既然是拷貝,那么每個變量都擁有不同的值,此時也就不涉及所有權(quán)轉(zhuǎn)移啥的。

而 String 類型的變量則不同,因為它們的值雖然在棧上,但是值里面的指針指向的內(nèi)存在堆上,而該類型的變量在傳遞的時候不會拷貝堆內(nèi)存,所以為避免二次釋放,此時才會有所有權(quán)的轉(zhuǎn)移,因為值和值里面的指針指向的堆內(nèi)存同時都只能有一個所有者。

因此這一語義完美地解決了我們的問題,既然只有 s2 有效,那么也就只有它會在離開自己的作用域時釋放空間,所以再也沒有二次釋放的可能性了。此外這里還隱含了另一個設(shè)計原則:Rust 永遠(yuǎn)不會自動地創(chuàng)建數(shù)據(jù)的深度拷貝(堆上數(shù)據(jù)),那么在 Rust 中,任何自動的賦值操作都可以被視為高效的。

變量和數(shù)據(jù)交互的方式:克隆

只拷貝棧上數(shù)據(jù)、不拷貝堆上數(shù)據(jù),我們稱之為淺拷貝(shallow copy);棧上數(shù)據(jù)和堆上數(shù)據(jù)都拷貝,我們稱之為深拷貝(shallow copy)。但有時我們確實需要去深度拷貝 String 在堆上的數(shù)據(jù),而不僅僅是棧數(shù)據(jù)時,可以使用一個名為 clone 的方法。我們將在后續(xù)討論該內(nèi)容,但很明顯我們已經(jīng)在其它語言中見過類似的東西。

fn main() {
    let mut s1 = String::from("hello world");
    // 調(diào)用 s1.clone(),那么不僅拷貝棧上的字符串
    // 字符串內(nèi)的指針指向的真正用來存儲文本的堆內(nèi)存也會拷貝
    let mut s2 = s1.clone();
    // 如果是 let s2 = s1 的話,那么這里打印 s1 就會出錯
    // value borrowed here after move
    // 提示的錯誤信息涉及到了引用和借用,這兩個概念后續(xù)再聊
    // 暫時可以理解為 Rust 編譯器告訴我們:所有權(quán)轉(zhuǎn)移之后就不能再使用了
    // 但我們這里是把堆內(nèi)存也拷貝了一份,所以此時使用 s1 沒有問題
    println!("s1 = {}", s1);  // s1 = hello world
    println!("s2 = {}", s2);  // s2 = hello world

    // 修改 s1,不會影響 s2
    s1.push_str("......");
    println!("s1 = {}", s1);  // s1 = hello world......
    println!("s2 = {}", s2);  // s2 = hello world
}

當(dāng)你看到某處調(diào)用了 clone 時,你就應(yīng)該知道某些特定的代碼將會被執(zhí)行,而且這些代碼可能會相當(dāng)消耗資源。

棧上數(shù)據(jù)的復(fù)制

這些概念上面已經(jīng)說過了,但是提到了深淺拷貝,所以我們將兩者結(jié)合起來再說一下。

fn main() {
    let x = 5;
    let y = x;
    println!("x = {}, y = {}", x, y);  
    // x = 5, y = 5
}

這與我們剛剛學(xué)到的內(nèi)容似乎有些矛盾:即便代碼沒有調(diào)用 clone,x 在被賦值給 y 后也依然有效,且沒有發(fā)生移動現(xiàn)象。這是因為整數(shù)可以在編譯時確定自己的大小,并且能夠?qū)⒆约旱臄?shù)據(jù)完整地存儲在棧中(不涉及到堆),而棧上的數(shù)據(jù)在傳遞的時候會直接拷貝一份。

這也同樣意味著,在創(chuàng)建變量 y 后,我們沒有任何理由去阻止變量 x 繼續(xù)保持有效,因為 x 和 y 擁有的是不同的數(shù)據(jù)。換言之,如果不涉及到堆,只是棧上的數(shù)據(jù),那么深拷貝和淺拷貝沒有任何區(qū)別。

因此還是之前說的,對于整數(shù)、浮點數(shù)、字符串字面量這種,它們的數(shù)據(jù)只會存在棧上,而棧上的數(shù)據(jù)在傳遞的時候會直接拷貝一份,所以你的是你的,我的是我的,根本不需要擔(dān)心所有權(quán)的問題。而當(dāng)涉及到所有權(quán)轉(zhuǎn)移時,一定也會涉及到堆,因為堆內(nèi)存默認(rèn)不會拷貝,那么為了防止變量失效,需要調(diào)用 clone 方法進(jìn)行深度拷貝(除了拷貝棧上的數(shù)據(jù)、還拷貝堆上的數(shù)據(jù))。

而 Rust 提供了一個名為 copy 的 trait(什么是 trait 后續(xù)會詳細(xì)說),一旦某個類型擁有了 copy 這種 trait,那么它的變量就可以在賦值給其它變量之后仍然保持可用性,顯然完全存儲在棧上的數(shù)據(jù)類型都實現(xiàn)了 copy。

而除了 copy,還有一種 trait 叫 drop,我們之前說過,當(dāng)值涉及到堆的變量在離開作用域的時候會調(diào)用 drop 釋放堆內(nèi)存。而一旦某種類型實現(xiàn)了 drop,那么 Rust 就不允許其再實現(xiàn) copy,因為實現(xiàn)了 copy 就表示變量傳遞之后仍然可用(數(shù)據(jù)完全在棧上,不能涉及到堆),實現(xiàn)了 drop 就表示變量離開作用域之后釋放堆內(nèi)存(數(shù)據(jù)涉及到堆),所以兩者是矛盾的。

那么究竟哪些類型是 copy 的呢?我們可以查看特定類型的文檔來確定,不過一般來說,任何簡單標(biāo)量類型都是 copy 的,任何需要運行時動態(tài)分配內(nèi)存的類型都不是 copy 的。下面是一些擁有 copy 這種 trait 的類型:

  • 所有的整數(shù)類型,諸如 u32;

  • 僅擁有兩種值(true 和 false)的布爾類型:bool;

  • 字符類型:char;

  • 所有的浮點類型,諸如 f64;

  • 如果元組包含的所有字段的類型都是 copy 的,那么這個元組也是 copy 的。例如 (i32, i32) 是 copy 的,但 (i32, String) 則不是;


所有權(quán)與函數(shù)

將值傳遞給函數(shù)在語義上類似于對變量進(jìn)行賦值,而將變量傳遞給函數(shù)等價于變量的傳遞,因此同樣會觸發(fā)移動或復(fù)制。

fn main() {
    // 變量 s 進(jìn)入作用域
    let s = String::from("hello");  
    // s 的值作為實參被傳遞給了函數(shù)
    // 等價于變量傳遞,此時會發(fā)生所有權(quán)的轉(zhuǎn)移
    takes_ownership(s);  
                        // 所以變量 s 從這里開始將不再有效
                        // 后續(xù)不可以再使用 s 這個變量

    let x = 5;  // 變量 x 進(jìn)入作用域

    // x 的值作為實參被傳遞給了函數(shù),但 i32 是可 copy 的
    // 或者說它完全是棧上的數(shù)據(jù),因此 x 在拷貝之后不受影響
    makes_copy(x);  
                     // 所以接下來我們?nèi)匀豢梢允褂眠@個 x

}  // x 離開作用域,但 x 是棧上的數(shù)據(jù),操作系統(tǒng)負(fù)責(zé),無需我們關(guān)心
   // s 離開作用域,但由于 s 的所有權(quán)發(fā)生了轉(zhuǎn)移
   // 它不再具有操作堆內(nèi)存的權(quán)利,所以它離開作用域時不會有任何事情發(fā)生


                   // some_string 進(jìn)入作用域     
fn takes_ownership(some_string: String) {  
    println!("{}", some_string);
}  // some_string 離開作用域,drop 函數(shù)自動調(diào)用
   // some_string 內(nèi)部的指針指向的堆內(nèi)存也就被釋放掉了
   // 至于 some_string 的值本身(一個結(jié)構(gòu)體),它是位于棧上的
   // 而棧上的數(shù)據(jù)在函數(shù)結(jié)束后操作系統(tǒng)會處理它,我們只需要關(guān)注堆內(nèi)存即可


              // some_integer 進(jìn)入作用域
fn makes_copy(some_integer: i32) {  
    println!("{}", some_integer);
}  // some_integer 離開作用域,但 i32 是棧上數(shù)據(jù)
   // 操作系統(tǒng)會處理,所以此時不會有任何事情發(fā)生
   // 當(dāng)然這些棧上數(shù)據(jù)(可 copy)也沒有實現(xiàn) drop
   // 因為不會涉及到堆,而 drop 釋放的內(nèi)存指的是堆內(nèi)存

總而言之,函數(shù)里面的參數(shù)也是一個變量,所以把變量傳到函數(shù)里面,和把變量賦值給另一個變量是等價的。既然等價,那么表現(xiàn)出的行為也是一致的。


返回值與作用域

函數(shù)在返回的過程中也會發(fā)生所有權(quán)的轉(zhuǎn)移,我們舉個栗子:

// 該函數(shù)會將它的返回值的所有權(quán)轉(zhuǎn)移給調(diào)用方
fn gives_ownership() -> String {
    // some_string 進(jìn)入作用域
    let some_string = String::from("hello");  
    // some_string 作為返回值,會將所有權(quán)轉(zhuǎn)移給調(diào)用方
    some_string   
}

// 該函數(shù)會取得一個 String 的所有權(quán)并將它作為結(jié)果返回
                        // some_string 進(jìn)入作用域
fn takes_and_gives_back(some_string: String) -> String {  
    // some_string 作為返回值,會將所有權(quán)轉(zhuǎn)移給調(diào)用方
    // 等于說是先剝奪了所有權(quán),然后又還回去了
    some_string  

}


fn main() {
    // gives_ownership 將它返回值的所有權(quán)轉(zhuǎn)移給 s1
    let s1 = gives_ownership();  
    
    // s2 進(jìn)入作用域
    let s2 = String::from("hello");  

    // s2 進(jìn)入函數(shù),它的所有權(quán)被交給了 takes_and_gives_back 中的 some_string 參數(shù)
    // 所以 s2 之后無法再使用,然后該函數(shù)將值返回,所有權(quán)又交給了 s3
    let s3 = takes_and_gives_back(s2);
                               
}  // s3 離開作用域時會銷毀堆內(nèi)存,但 s2 的所有權(quán)已經(jīng)移動了 
   // 所以它離開作用域時不會發(fā)生任何事情
   // s1 最后離開作用域時也會釋放堆內(nèi)存,并且 s1 的所有權(quán)從始至終都沒有發(fā)生轉(zhuǎn)移

變量所有權(quán)的轉(zhuǎn)移總是遵循相同的模式:將一個變量賦值給另一個變量時就會轉(zhuǎn)移所有權(quán);當(dāng)一個持有堆數(shù)據(jù)的變量離開作用域時,它的數(shù)據(jù)就會被 drop 清理回收,除非這些數(shù)據(jù)的所有權(quán)移動到了另一個變量上面。

但是在所有的函數(shù)中都要先獲取所有權(quán)、再返回所有權(quán)似乎顯得有些煩瑣,假如你希望在調(diào)用函數(shù)時保留參數(shù)的所有權(quán),那么就不得不將傳入的值作為結(jié)果返回。除了這些需要保留所有權(quán)的值,函數(shù)還可能會返回它們本身的結(jié)果。我們舉個栗子:

// 該函數(shù)計算一個字符串的長度
fn get_length(s: String) -> (Stringusize) {
    // 因為這里的 s 會獲取變量的所有權(quán)
    // 而一旦獲取,那么調(diào)用方就不能再使用了
    // 所以我們除了要返回計算的長度之外
    // 還要返回這個字符串本身,也就是將所有權(quán)再交回去
    let length = s.len();
    (s, length)
    // Rust 對類型的要求很嚴(yán)格,計算的長度(以及索引)是一個 usize
    // 所以函數(shù)返回值簽名里面也要是 usize,不能是 int32
}


fn main() {
    let s = String::from("古明地覺");

    // 接收長度的同時,還要接收字符串本身,將所有權(quán)重新 "奪" 回來
    // 當(dāng)然,如果后續(xù)不再使用這個 s,那么也可以放棄所有權(quán)
    let (s, length) = get_length(s);
    println!("s = {}, length = {}", s, length); 
    /*
    s = 古明地覺, length = 12
    */

    // 從返回的結(jié)果也可以看出,Rust 采用了 utf-8 編碼,一個漢字 3 個字節(jié)
}

但這種寫法未免太過笨拙了,因為類似的概念在編程工作中相當(dāng)常見,所以 Rust 針對這類場景提供了一個名為引用的功能。

關(guān)于引用我們下一篇文章介紹。


小結(jié)

所有權(quán)這個概念本身不難理解,就把它當(dāng)成是操作堆內(nèi)存的權(quán)利。在 Python 里面,堆內(nèi)存可以有很多個所有者,并通過引用計數(shù)維護(hù)所有者的數(shù)量,當(dāng)沒有所有者的時候(也就是沒有變量引用的時候),那么釋放堆內(nèi)存。但維護(hù)引用計數(shù)需要額外的開銷,并且由于引用計數(shù)機(jī)制無法解決循環(huán)引用,還要有垃圾回收來負(fù)責(zé)兜底。

而 Rust 就簡單了,它讓每個堆內(nèi)存只能有一個所有者,換言之就是只能有一個變量持有對堆內(nèi)存的所有權(quán)。而一旦賦值給新的變量,Rust 就會讓所有權(quán)發(fā)生轉(zhuǎn)移,而不是像 Python 那樣讓多個變量都持有所有權(quán)。這樣 Rust 只需要關(guān)注持有所有權(quán)的那個變量即可,堆內(nèi)存是否釋放,就看持有所有權(quán)的變量所在的作用域是否已經(jīng)結(jié)束。

fn main() {
    let s1;
    {   
        // s2 持有對堆內(nèi)存的所有權(quán)
        let s2 =  String::from("古明地覺");
        // 所有權(quán)交給 s1,s2 不再具備操作堆內(nèi)存的權(quán)利
        s1 = s2;
    } // 到此 s2 所在的作用域已經(jīng)結(jié)束,s2 會被銷毀
      // 但銷毀的只是 s2 本身,它內(nèi)部指針指向的堆內(nèi)存則不會銷毀
      // 因為 s2 不是這片堆內(nèi)存的所有者,s1 才是
      // 所以堆內(nèi)存是否銷毀只和 s1 有關(guān)
    
    // 此處打印 s1,沒有問題
    print!("{}", s1);  // 古明地覺
}

所以對于那些已經(jīng)將所有權(quán)交出去的變量,等到所在的作用域結(jié)束后,它們在棧上的數(shù)據(jù)會被自動清理掉,至于內(nèi)部指針指向的堆區(qū)數(shù)據(jù)則與之無關(guān)。并且變量的所有權(quán)一旦轉(zhuǎn)移,我們就不能再使用了,當(dāng)然也不需要再關(guān)注了,等到作用域一結(jié)束,由操作系統(tǒng)自動將棧上數(shù)據(jù)清理掉即可。

因此 Rust 保證一份堆內(nèi)存只能有一個所有者,便可以在不使用垃圾回收的情況下保證內(nèi)存安全,這是一個非常有意思的設(shè)計。而如果確實需要同時存在兩個所有者,那么就通過 s.clone() 將堆上數(shù)據(jù)也拷貝一份,讓每個變量持有不同的堆區(qū)數(shù)據(jù)。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    亚洲日本韩国一区二区三区| 国内精品一区二区欧美| 天海翼高清二区三区在线| 91亚洲国产成人久久| 亚洲欧美日产综合在线网| 激情图日韩精品中文字幕| 欧美日韩国产综合在线| 国产精品免费精品一区二区| 九九热九九热九九热九九热| 夫妻性生活真人动作视频| 日韩精品免费一区二区三区| 久久精品亚洲欧美日韩| 尹人大香蕉中文在线播放| 亚洲中文在线观看小视频| 色婷婷在线精品国自产拍| 中文日韩精品视频在线| 国产伦精品一区二区三区高清版| 男女午夜视频在线观看免费| 久久精品国产亚洲熟女| 九九热这里只有免费精品| 高清一区二区三区大伊香蕉| 国产亚洲精品俞拍视频福利区| 亚洲国产精品肉丝袜久久| 日本成人中文字幕一区| 黄色片国产一区二区三区| 中文字幕高清不卡一区| 亚洲午夜福利不卡片在线| 99久久国产精品亚洲| 五月天丁香婷婷狠狠爱| 亚洲欧美日韩国产自拍| 婷婷色香五月综合激激情| 国产精品视频第一第二区| 老司机这里只有精品视频| 伊人国产精选免费观看在线视频| 亚洲中文字幕人妻系列| 懂色一区二区三区四区| 日韩专区欧美中文字幕| 99国产一区在线播放| 欧美在线视频一区观看| 好吊视频有精品永久免费| 亚洲成人久久精品国产|