自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Rust難懂?一文解讀其“所有權(quán)”和“借用”概念

譯文 精選
開發(fā)
Rust內(nèi)存安全秘籍

作者丨Senthil Nayagan

譯者 | 仇凱

審校丨Noe

如果不理解背后的設(shè)計(jì)理念和工作原理,那么就會對Rust的所有權(quán)和借用特性產(chǎn)生困惑。這種困惑尤其出現(xiàn)在將以前學(xué)習(xí)的編程風(fēng)格應(yīng)用于新范式時(shí),我們稱之為范式轉(zhuǎn)移。所有權(quán)是一個(gè)新穎的想法,初次接觸時(shí)非常難以理解。但是,隨著使用經(jīng)驗(yàn)的逐漸積累,開發(fā)人員會越來越深刻的體會到其設(shè)計(jì)理念的精妙。在進(jìn)一步學(xué)習(xí)Rust的所有權(quán)和借用特性之前。首先,需要了解下“內(nèi)存安全”和“內(nèi)存泄漏”的原理以及編程語言處理這兩種問題的方式。

 內(nèi)存安全 

內(nèi)存安全是指軟件程序的一種狀態(tài),在這種狀態(tài)下內(nèi)存指針或引用始終指向有效內(nèi)存。因?yàn)閮?nèi)存存在損壞的可能性,因此如果無法做到內(nèi)存安全,那么就幾乎無法保證程序的運(yùn)行效果。簡而言之,如果程序無法做到真正的內(nèi)存安全,那么就無法保證其功能可以正常實(shí)現(xiàn)和執(zhí)行。在運(yùn)行內(nèi)存不安全的程序時(shí),攻擊方可以利用漏洞在他人設(shè)備上竊取機(jī)密信息或任意執(zhí)行惡意代碼。下面我們將使用偽代碼來解釋有效內(nèi)存。

// pseudocode  #1 - shows valid reference
{ // scope starts here
int x = 5
int y = &x
} // scope ends here

在上面的偽代碼中,我們創(chuàng)建了一個(gè)值為10的變量x。我們使用&作為運(yùn)算符或關(guān)鍵字來創(chuàng)建引用。因此,語法&x允許我們創(chuàng)建對變量x的值的引用。簡單地說,我們創(chuàng)建了一個(gè)值為5的變量x和一個(gè)引用x的變量
y。

由于變量x和y在同一個(gè)代碼塊或作用域中,因此變量y對x的值的引用是有效引用。因此,變量y的值是5。

下面的偽代碼示例中,正如我們所見,x的作用域僅限于創(chuàng)建它的代碼塊。當(dāng)我們嘗試在其作用域之外訪問x時(shí),就會遇到懸空引用問題。懸空引用……?它到底是什么?

// pseudocode  #2 - shows invalid reference aka dangling reference
{ // scope starts here
int x = 5
} // scope ends here
int y = &x // can't access x from here; creates dangling reference

懸空引用

懸空引用的意思是指向已分配或已釋放內(nèi)存位置的指針。如果一個(gè)程序(也稱為進(jìn)程)引用了已釋放或已清除數(shù)據(jù)的內(nèi)存,就可能會崩潰或產(chǎn)生無法預(yù)知的結(jié)果。話雖如此,內(nèi)存不安全也是一些編程語言的特性,程序員使用這些特性處理無效數(shù)據(jù)。因此,內(nèi)存不安全引入了很多問題,這些問題可能會導(dǎo)致以下安全漏洞:

  • 越界讀取
  • 越界寫入
  • UAF漏洞(Use-After-Free)

內(nèi)存不安全導(dǎo)致的漏洞是許多高風(fēng)險(xiǎn)安全威脅的根源。更棘手的是,發(fā)現(xiàn)并處理此類漏洞對于開發(fā)人員來說是非常困難的。

內(nèi)存泄漏 

我們需要了解內(nèi)存泄漏的原理及其引發(fā)的嚴(yán)重后果。內(nèi)存泄漏是非正常的內(nèi)存使用形式,在這種情況下,會導(dǎo)致開發(fā)人員無法釋放已分配的堆內(nèi)存塊,即使該內(nèi)存塊已不再需要存放數(shù)據(jù)。這與內(nèi)存安全的概念是相反的。稍后我們將詳細(xì)探討不同的內(nèi)存類型,但現(xiàn)在,我們只需要知道棧會存儲在編譯時(shí)就長度固定的變量,而在運(yùn)行時(shí)會改變長度的變量則必須存放在堆上。與堆內(nèi)存分配相比,棧內(nèi)存分配被認(rèn)為是更安全的,因?yàn)閮?nèi)存會在與程序無關(guān)或不再需要時(shí)被自動釋放,釋放過程可以由程序員控制,也可以由運(yùn)行時(shí)系統(tǒng)自動判斷。

但是,當(dāng)程序員在堆上占用內(nèi)存并且未能將其釋放,且沒有垃圾回收器(例如C或C++)的情況下,就會發(fā)生內(nèi)存泄漏。此外,如果我們丟失了一塊內(nèi)存的所有引用且沒有釋放該內(nèi)存,也會發(fā)生內(nèi)存泄漏。我們的程序?qū)⒗^續(xù)擁有該內(nèi)存,卻無法再次使用它。

輕微的內(nèi)存泄漏并不是問題,但是如果一個(gè)程序占用了大量的內(nèi)存卻從不釋放,那么程序的內(nèi)存占用將持續(xù)上升,最終導(dǎo)致拒絕服務(wù)。

當(dāng)程序退出時(shí),操作系統(tǒng)會立即接管并釋放其占用的所有內(nèi)存。因此,只有當(dāng)程序運(yùn)行時(shí)內(nèi)存泄露問題才會存在。一旦程序終止,內(nèi)存泄露問題就會消失。讓我們回顧一下內(nèi)存泄漏的主要影響。

內(nèi)存泄漏能夠通過減少可用內(nèi)存(堆內(nèi)存)的數(shù)量來降低計(jì)算機(jī)的性能。它最終會導(dǎo)致系統(tǒng)整體或部分的工作異?;蛟斐蓢?yán)重的性能損耗。崩潰通常與內(nèi)存泄漏有關(guān)。不同編程語言應(yīng)對內(nèi)存泄漏的方法是不同的。內(nèi)存泄漏可能會從一個(gè)微小的幾乎“不明顯的問題”開始,但這些問題會迅速擴(kuò)散并對操作系統(tǒng)造成難以承受的影響。我們應(yīng)該盡最大努力密切關(guān)注內(nèi)存泄露問題,盡可能避免并修正相關(guān)錯(cuò)誤,而非任其發(fā)展。

內(nèi)存不安全和內(nèi)存泄漏 

內(nèi)存泄漏和內(nèi)存不安全是預(yù)防和補(bǔ)救方面最受關(guān)注的兩類問題。而且兩者相對獨(dú)立,并不會因?yàn)槠渲幸环N問題被修復(fù)而使另一種問題被自動修復(fù)。

圖片

圖1:內(nèi)存不安全與內(nèi)存泄漏

內(nèi)存的類型及其工作原理 

在我們繼續(xù)學(xué)習(xí)之前,我們需要了解不同類型的內(nèi)存在代碼中是如何被使用的。如下所示,這些內(nèi)存的結(jié)構(gòu)是不同的。

  • 寄存器
  • 靜態(tài)內(nèi)存
  • 棧內(nèi)存
  • 堆內(nèi)存

寄存器和靜態(tài)內(nèi)存類型不在本文的討論范圍。

1.棧內(nèi)存及其工作原理

棧按照接收順序存儲數(shù)據(jù),并以相反的順序?qū)⑵鋭h除。棧中的元素是按照后進(jìn)先出 (LIFO) 的順序進(jìn)行訪問的。將數(shù)據(jù)添加到棧中稱為“pushing”,將數(shù)據(jù)從棧中移除稱為“popping”。

所有存儲在棧上的數(shù)據(jù)都必須是已知且長度固定的。在編譯時(shí)長度未知或長度會發(fā)生變化數(shù)據(jù)必須存儲在堆上。

作為開發(fā)人員,我們不必為棧內(nèi)存的分配和釋放操心;棧內(nèi)存的分配和釋放是由編譯器“自動完成”。這意味著當(dāng)棧上的數(shù)據(jù)與程序無關(guān)(即超出范圍)時(shí),它會被自動刪除而無需人工的干預(yù)。

這種內(nèi)存分配方式也被稱為臨時(shí)內(nèi)存分配,因?yàn)楫?dāng)函數(shù)執(zhí)行結(jié)束時(shí),屬于該函數(shù)的所有數(shù)據(jù)都會“自動”從棧中被清除。Rust中的所有原始類型都存在棧中。數(shù)字、字符、切片、布爾值、固定大小的數(shù)組、包含原始元素的元組和函數(shù)指針等類型都可以存放在棧上。

2.堆內(nèi)存及其工作原理

與棧不同,當(dāng)我們將數(shù)據(jù)存放到堆上時(shí),需要請求一定的內(nèi)存空間。內(nèi)存分配器在堆中定位一個(gè)足夠大的未占用空間,并將其標(biāo)記為正在使用,同時(shí)返回該位置地址的引用。這就是內(nèi)存分配。

在堆上存放數(shù)據(jù)比在棧上存放數(shù)據(jù)要慢,因?yàn)闂S肋h(yuǎn)不需要內(nèi)存分配器尋找空位置來放置新數(shù)據(jù)。此外,由于我們必須通過指針來獲取堆上的數(shù)據(jù),所以堆的數(shù)據(jù)訪問速度要比棧慢。棧內(nèi)存是在編譯時(shí)就被分配和釋放的,與之不同的是,堆內(nèi)存是在程序指令執(zhí)行期間被分配和釋放的。在某些編程語言中,使用關(guān)鍵字new來分配堆內(nèi)存。關(guān)鍵字new(又名運(yùn)算符)表示在堆上請求分配內(nèi)存。如果堆上有充足的可用內(nèi)存,則運(yùn)算符new會將內(nèi)存初始化并返回分配內(nèi)存的唯一地址。值得一提的是,堆內(nèi)存是由程序員或運(yùn)行時(shí)系統(tǒng)“顯式”釋放的。

編程語言如何實(shí)現(xiàn)內(nèi)存安全? 

談到內(nèi)存管理,尤其是堆內(nèi)存,我們希望編程語言具有以下特征:

  • 不需要內(nèi)存時(shí)就盡快釋放,且不增加資源消耗。
  • 對已釋放數(shù)據(jù)的引用(也就是懸空引用)進(jìn)行自動維護(hù)。否則,極易發(fā)生程序崩潰和安全問題。

編程語言通過以下方式確保內(nèi)存安全:

  • 顯式內(nèi)存釋放(例如C和C++)
  • 自動或隱式內(nèi)存釋放(例如Java、Python和C#)
  • 基于區(qū)域的內(nèi)存管理
  • 線性或特殊類型系統(tǒng)

基于區(qū)域的內(nèi)存管理和線性系統(tǒng)都不在本文的討論范圍。

1.手動或顯式內(nèi)存釋放

在使用顯式內(nèi)存管理時(shí),程序員必須“手動”釋放或擦除已分配的內(nèi)存。運(yùn)算符“釋放”(例如,C中的delete)存在于需要顯式內(nèi)存釋放的語言中。

在C和C++等系統(tǒng)語言中,垃圾回收的成本很高,因此顯式內(nèi)存分配一定會長久存在的。將釋放內(nèi)存的職責(zé)留給程序員,能夠讓程序員在變量的生命周期內(nèi)擁有其完整的控制權(quán)。然而,如果釋放運(yùn)算符使用不當(dāng),軟件在執(zhí)行過程中就極易出現(xiàn)故障。事實(shí)上,這種手動分配和釋放內(nèi)存的過程很容易出錯(cuò)。常見的編碼錯(cuò)誤包括:

  • 懸空引用
  • 內(nèi)存泄漏

盡管如此,我們更傾向于手動內(nèi)存管理而非依賴?yán)厥諜C(jī)制,因?yàn)檫@賦予我們更多的控制權(quán)并能夠有更出色的性能表現(xiàn)。請注意,任何系統(tǒng)編程語言的設(shè)計(jì)目標(biāo)都是擁有更好的魯棒性。換句話說,編程語言的設(shè)計(jì)者在性能和便利性之間選擇了性能。

確保不使用任何指向已釋放內(nèi)存的指針,是開發(fā)人員的責(zé)任。

不久之前,有一些方法已經(jīng)被驗(yàn)證可以避免這些錯(cuò)誤,但這一切最終都?xì)w結(jié)為嚴(yán)格遵循代碼規(guī)范,這需要嚴(yán)格使用正確的內(nèi)存管理方法。

關(guān)鍵要點(diǎn)是:

  • 嚴(yán)格的內(nèi)存管理。
  • 懸空引用和內(nèi)存泄漏的安全性比較低。
  • 較長的開發(fā)周期。

2.自動或隱式內(nèi)存釋放

自動內(nèi)存管理已成為包括Java在內(nèi)的現(xiàn)代編程語言的基本特征。

在內(nèi)存自動釋放的場景中,垃圾回收器就是自動化的內(nèi)存管理器。垃圾回收器會周期性的遍歷堆內(nèi)存并回收未使用的內(nèi)存塊。它代替我們管理內(nèi)存的分配和釋放。因此,我們不必編寫代碼來執(zhí)行內(nèi)存管理任務(wù)。這很好,因?yàn)槔厥掌鲗⑽覀儚姆爆嵉膬?nèi)存管理職責(zé)中解放出來。同時(shí)也大大減少了開發(fā)時(shí)間。

但是,垃圾回收機(jī)制并非完美無缺的,它同樣有許多的缺點(diǎn)。在垃圾回收期間,程序需要暫停其他任務(wù),并消耗時(shí)間搜索需要清理和回收的內(nèi)存。

此外,自動內(nèi)存管理機(jī)制對內(nèi)存有更高優(yōu)先級的需求。垃圾回收器為我們執(zhí)行內(nèi)存釋放的操作,而這些操作會消耗內(nèi)存和CPU時(shí)鐘周期。因此,自動內(nèi)存管理會降低應(yīng)用程序的性能,尤其是在資源有限的大型應(yīng)用程序中。

關(guān)鍵要點(diǎn)是:

  • 自動內(nèi)存管理。
  • 解決懸空引用或內(nèi)存泄露問題,并高效提升內(nèi)存安全性。
  • 更簡潔的代碼。
  • 更快的開發(fā)周期。
  • 輕量化的內(nèi)存管理。
  • 由于它消耗內(nèi)存和CPU時(shí)鐘周期,因此延遲較高。

 Rust中的內(nèi)存安全 

一些語言提供垃圾回收機(jī)制,它在程序運(yùn)行時(shí)尋找不再使用的內(nèi)存;而其他語言則要求程序員顯式分配和釋放內(nèi)存。這兩種模型各有優(yōu)劣。垃圾回收機(jī)制雖然已經(jīng)廣泛應(yīng)用,但也有一些缺點(diǎn);它是以犧牲資源和性能為代價(jià),來提升開發(fā)人員的工作效率。

話雖如此,一邊是提供高效的內(nèi)存管理機(jī)制,而另一邊通過消除懸空引用和內(nèi)存泄漏提供更好的安全性。Rust將這兩種優(yōu)點(diǎn)集于一身。

圖片

圖2:Rust有優(yōu)秀的內(nèi)存管理機(jī)制,并提供很好的安全性

Rust采用了與上述兩種方案不同的方法,即基于規(guī)則組的所有權(quán)模型,編譯器驗(yàn)證這些規(guī)則組來確保內(nèi)存安全。只有在完全滿足這些規(guī)則的情況下,程序才會被編譯成功。事實(shí)上,所有權(quán)用編譯時(shí)的內(nèi)存安全檢查替代運(yùn)行時(shí)的垃圾回收機(jī)制。

圖片

顯式內(nèi)存管理、隱式內(nèi)存管理和Rust的所有權(quán)模型由于所有權(quán)是一個(gè)新的概念,因此,即使是與我一樣的程序員,學(xué)習(xí)和適應(yīng)它也需要一定時(shí)間。

所有權(quán) 

至此,我們對數(shù)據(jù)在內(nèi)存中的存儲方式有了基本的了解?,F(xiàn)在我們來認(rèn)真研究下Rust中的所有權(quán)模型。Rust最大的特點(diǎn)就是所有權(quán)模型,它在編譯時(shí)就保障了內(nèi)存的安全性。

首先,讓我們從字面意義來理解“所有權(quán)”。所有權(quán)是合法“擁有”和“控制”“某物”的一種狀態(tài)。話雖如此,我們必須明確誰是所有者以及所有者能夠擁有和控制什么。在Rust中,每個(gè)值都有對應(yīng)的變量,變量就是值的所有者。簡而言之,變量就是所有者,變量的值就是所有者能夠擁有和控制的東西。

圖片

圖3:變量綁定展示所有者及其值/資源

在所有權(quán)模型中,一旦變量超出作用域,內(nèi)存就會被自動釋放。當(dāng)值超出作用域或結(jié)束其生命周期時(shí),其析構(gòu)函數(shù)就會被調(diào)用。析構(gòu)函數(shù)(尤其是自動析構(gòu)函數(shù))是一個(gè)函數(shù),它通過從程序中刪除值的引用痕跡來釋放內(nèi)存。

1.借用檢查器

Rust通過借用檢查器(一種靜態(tài)分析器)實(shí)現(xiàn)所有權(quán)。借用檢查器是Rust編譯器中的一個(gè)組件,它跟蹤整個(gè)程序中數(shù)據(jù)的使用位置,并且根據(jù)所有權(quán)規(guī)則,它能夠識別需要執(zhí)行釋放的數(shù)據(jù)的位置。此外,借用檢查器可以確保在運(yùn)行時(shí),已釋放的內(nèi)存永遠(yuǎn)不會被訪問。它甚至消除了因?yàn)閿?shù)據(jù)出現(xiàn)同時(shí)突變(或修改)而引起的競爭問題。

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

如前所述,所有權(quán)模型建立在一組所有權(quán)規(guī)則之上,這些規(guī)則相對簡單。Rust編譯器
(rustc) 強(qiáng)制執(zhí)行以下規(guī)則:

  • 在Rust中,每個(gè)值都有對應(yīng)的變量,變量就是值的所有者。
  • 同時(shí)只能存在一個(gè)所有者。
  • 當(dāng)所有者超出作用域時(shí),該值將被刪除。

編譯時(shí)檢查的所有權(quán)規(guī)則會對以下內(nèi)存錯(cuò)誤進(jìn)行保護(hù):

  • 懸空引用:這是一個(gè)指向已經(jīng)不再包含數(shù)據(jù)的內(nèi)存地址的指針;此指針指向空數(shù)據(jù)或隨機(jī)數(shù)據(jù)。
  • UAF漏洞:當(dāng)內(nèi)存被釋放后,仍試圖訪問該內(nèi)存,就可能會導(dǎo)致程序崩潰。這個(gè)內(nèi)存位置經(jīng)常被黑客用來執(zhí)行惡意代碼。
  • 雙重釋放:當(dāng)已分配的內(nèi)存被釋放后,再次執(zhí)行釋放操作。這會導(dǎo)致程序崩潰,進(jìn)而暴露敏感信息。這種情況同樣允許黑客執(zhí)行惡意代碼。
  • 分段錯(cuò)誤:程序嘗試訪問被禁止訪問的內(nèi)存區(qū)域時(shí),就會出現(xiàn)分段錯(cuò)誤。
  • 緩沖區(qū)溢出:數(shù)據(jù)量超過內(nèi)存緩沖區(qū)的存儲容量時(shí),就會出現(xiàn)緩沖區(qū)溢出,這通常會導(dǎo)致程序崩潰。

在深入了解所有權(quán)規(guī)則之前,我們需要先了解copy、move和clone之間的區(qū)別。

3.copy

長度固定的數(shù)據(jù)類型(尤其是原始類型)可以存儲在棧中,并在其作用范圍結(jié)束時(shí)清除數(shù)據(jù)釋放內(nèi)存。如果其他代碼在其作用范圍內(nèi)需要相同數(shù)據(jù)的時(shí)候,還可以從棧中便捷的將該數(shù)據(jù)復(fù)制為一個(gè)新的獨(dú)立變量。因?yàn)闂?nèi)存的復(fù)制非常高效便捷,因此具有固定長度的原始類型被稱為擁有復(fù)制語義特性。它高效的創(chuàng)建了一個(gè)完美的復(fù)制品。

值得注意的是,具有固定長度的原始類型實(shí)現(xiàn)了通過復(fù)制特征來進(jìn)行復(fù)制。

let x =  "hello";
let y = x;
println!("{}", x) // hello
println!("{}", y) // hello

在Rust中,有兩種字符串:String(堆分配的,可增長的)和&str(固定大小,不能改變)。

因?yàn)閤存儲在棧中,所以復(fù)制它的值來為y生成一個(gè)副本非常容易。這種用法并不適用于存儲在堆上的數(shù)據(jù)。下面是棧的示意圖:

圖片

圖4:x和y都有自己的數(shù)據(jù)

復(fù)制數(shù)據(jù)會增加程序運(yùn)行時(shí)間和內(nèi)存消耗。因此,大塊數(shù)據(jù)不適合使用復(fù)制。

4.move

在Rust術(shù)語中,“move”意味著將轉(zhuǎn)移內(nèi)存的所有權(quán)。想象一下堆上存儲的數(shù)據(jù)類型有多復(fù)雜。

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

我們可以假設(shè)第二行(即
let s2 = s1;)將復(fù)制s1中的值并綁定到s2。然而所見并非所得。

下面我們將研究下String究竟在后臺執(zhí)行了什么樣的操作。String由存儲在棧中的三部分組成。實(shí)際的內(nèi)容(在本例中是hello)存儲在堆上。

  • 指針-指向保存字符串內(nèi)容的內(nèi)存。
  • 長度-字符串當(dāng)前使用的內(nèi)存大小(以字節(jié)為單位)。
  • 容量-字符串從分配器獲得的內(nèi)存總量(以字節(jié)為單位)。

換句話說,元數(shù)據(jù)存儲在棧上,而實(shí)際數(shù)據(jù)則存儲在堆上。

圖片

圖5:棧存儲元數(shù)據(jù),堆存儲實(shí)際內(nèi)容

當(dāng)我們將s1分配給s2時(shí),會復(fù)制字符串的元數(shù)據(jù),這意味著我們會復(fù)制棧上的指針、長度和容量。不會復(fù)制堆上指針?biāo)赶虻臄?shù)據(jù)。內(nèi)存中的數(shù)據(jù)如下所示:

圖片

圖6:變量s2獲取s1的指針、長度和容量的副本

值得注意的是,下圖表示的是Rust復(fù)制了堆數(shù)據(jù)后內(nèi)存的狀態(tài),真實(shí)情況并不是這樣的。如果Rust執(zhí)行此操作,當(dāng)堆數(shù)據(jù)很大時(shí),則s2 = s1操作在運(yùn)行時(shí)性能表現(xiàn)會非常差。

圖片

圖7:如果Rust復(fù)制堆數(shù)據(jù),s2 = s1操作的結(jié)果就是數(shù)據(jù)復(fù)制。但是,Rust默認(rèn)不執(zhí)行數(shù)據(jù)復(fù)制

請注意,當(dāng)復(fù)雜類型不在作用域內(nèi)時(shí),Rust將調(diào)用drop函數(shù)對堆內(nèi)存執(zhí)行顯式釋放。但是,圖6中的兩個(gè)數(shù)據(jù)指針指向同一個(gè)位置,Rust并不會這樣做。我們將很快進(jìn)入技術(shù)實(shí)現(xiàn)細(xì)節(jié)。

如前所述,當(dāng)我們將s1分配給s2時(shí),變量s2會復(fù)制s1的元數(shù)據(jù)(指針、長度和容量)。但是,將s1分配給s2之后,s1會發(fā)生什么呢?Rust認(rèn)為此時(shí)s1是無效的。是的,你沒看錯(cuò)。

讓我們重新思考下let s2 = s1的賦值操作。假設(shè)Rust在此操作之后仍然認(rèn)為s1是有效的,那么會發(fā)生什么。當(dāng)s2和s1超出范圍時(shí),它們都會嘗試釋放相同的內(nèi)存。這是個(gè)糟糕的情況。這被稱為雙重釋放錯(cuò)誤,它屬于內(nèi)存安全錯(cuò)誤的一種情況。雙重釋放內(nèi)存可能會導(dǎo)致內(nèi)存損壞,進(jìn)而帶來安全風(fēng)險(xiǎn)。

為了確保內(nèi)存安全,Rust在let s2 = s1操作執(zhí)行之后會認(rèn)為s1無效。因此,當(dāng)s1不在作用域內(nèi)時(shí),Rust不需要執(zhí)行內(nèi)存釋放。假如創(chuàng)建s2后仍然嘗試使用s1會發(fā)生什么,下面我們做個(gè)試驗(yàn)。

let s1 =  String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // Won't compile. 我們得到一個(gè)錯(cuò)誤。

我們會得到一個(gè)類似下面的錯(cuò)誤,因?yàn)镽ust不允許使用無效的引用:

$ cargo run
Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:6:28
|
3 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 | let s2 = s1;
| -- value moved here
5 |
6 | println!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.

左右滑動查看完整代碼

由于Rust在let s2 = s1操作執(zhí)行之后將s1的內(nèi)存所有權(quán)轉(zhuǎn)移給了s2,因此它認(rèn)為s1是無效的。這是s1失效后的內(nèi)存表示:

圖片

圖8:s1無效后的內(nèi)存表示

當(dāng)只有s2保持有效時(shí),就會在其超出范圍時(shí)執(zhí)行內(nèi)存釋放操作。因此,Rust消除了雙重釋放錯(cuò)誤出現(xiàn)的可能性。這是非常優(yōu)秀的解決方案!

5.clone

如果我們需要深度復(fù)制字符串的堆數(shù)據(jù),而不僅僅是棧數(shù)據(jù),可以使用一種叫做clone的方法。以下是克隆方法的使用示例:

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

clone方法確實(shí)將堆數(shù)據(jù)復(fù)制到s2中了。操作非常完美,下圖是示例:

圖片

圖9:clone方法確實(shí)將堆數(shù)據(jù)復(fù)制給s2

然而clone方法有一個(gè)嚴(yán)重的后果;它只復(fù)制數(shù)據(jù),卻不會同步兩者之間的任何更改。通常情況下,進(jìn)行clone應(yīng)當(dāng)審慎評估其效果和影響。

至此,我們詳細(xì)了解了copy、move和clone的技術(shù)原理。下面我們將詳細(xì)地了解所有權(quán)規(guī)則。

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

每個(gè)值都有對應(yīng)的變量,變量就是值的所有者。這意味著值都?xì)w變量所有。在下面的示例中,變量s擁有指向字符串的指針,而在第二行中,變量x擁有值1。

let s =  String::from("Rule 1");
let n = 1;

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

在給定時(shí)間,一個(gè)值只有一個(gè)所有者。一個(gè)人可以擁有許多寵物,但在所有權(quán)模型中,任何時(shí)候一個(gè)值都只有一個(gè)所有者:-)

原始類型是在編譯時(shí)就已知其固定長度的數(shù)據(jù)類型,下面是原始類型的使用示例。

let x = 10;
let y = x;
let z = x;

我們將10分配給變量x;換句話說,變量x擁有10。然后我們將x分配給y,同時(shí)將其分配給z。我們知道同一時(shí)間只能存在一個(gè)所有者,但沒有引發(fā)任何錯(cuò)誤。所以在此示例中,每次當(dāng)我們將x分配給一個(gè)新變量時(shí),編譯器都會對其進(jìn)行復(fù)制。

棧幀如下:x = 10,y = 10 和 z = 10。然而,真實(shí)情況似乎與我們的設(shè)想(x = 10、y = x 和 z = x)不一樣。我們知道,x是10的唯一所有者,y和z都不能擁有這個(gè)值。

圖片

圖10:編譯器將x復(fù)制到y(tǒng)和z

就像前文所述,由于復(fù)制棧內(nèi)存高效便捷,因此具有固定長度的原始類型被稱為擁有復(fù)制語義特性,而復(fù)雜類型就只是移動所有權(quán)。因此,在這種情況下,編譯器執(zhí)行了復(fù)制操作。

這種情況下,變量綁定的行為與其他的編程語言類似。為了闡釋所有權(quán)規(guī)則,我們需要復(fù)雜的數(shù)據(jù)類型。

我們通過觀察數(shù)據(jù)在堆上的存儲方式,來學(xué)習(xí)Rust如何判斷需要清理哪些數(shù)據(jù);字符串類型是就是一個(gè)很好的例子。我們將聚焦于字符串類型所有權(quán)相關(guān)的行為;這些原則也適用于其他復(fù)雜的數(shù)據(jù)類型。

眾所周知,存儲在堆上的復(fù)雜數(shù)據(jù)類型,其內(nèi)容在編譯時(shí)是不可預(yù)知的。我們來觀察這個(gè)示例:

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

println!("{}, world!", s1); // Won't compile. 我們得到一個(gè)錯(cuò)誤。

儲字符串類型的場景下,其存儲在堆上的數(shù)據(jù)長度可能會發(fā)生變化。這就表示:

  • 程序在運(yùn)行的時(shí)候就必須從內(nèi)存分配器請求內(nèi)存(我們稱之為第一部分)。
  • 當(dāng)不再需要該字符串后,需要將此內(nèi)存釋放回內(nèi)存分配器(我們稱之為第二部分)。

開發(fā)人員需要重點(diǎn)關(guān)注第一部分:當(dāng)我們從String::開始執(zhí)行調(diào)用時(shí),它會向內(nèi)存分配器請求內(nèi)存。這部分的技術(shù)實(shí)現(xiàn)在編程語言中很常見。

但是,第二部分是不一樣的。在擁有垃圾回收機(jī)制的編程語言中,垃圾回收器會跟蹤并清理不再使用的內(nèi)存,開發(fā)人員并不需要在此方面投入精力。在沒有垃圾回收機(jī)制的語言中,識別不再需要的內(nèi)存并顯式釋放內(nèi)存就是開發(fā)人員的職責(zé)。確保正確釋放內(nèi)存是一項(xiàng)很有挑戰(zhàn)性的編程任務(wù):

  • 如果我們忘記釋放內(nèi)存,就會造成內(nèi)存空間浪費(fèi)。
  • 如果我們過早的釋放內(nèi)存,那么就會產(chǎn)生一個(gè)無效變量。
  • 如果我們重復(fù)釋放內(nèi)存,程序就會出現(xiàn)BUG。

Rust以一種新穎的方式處理內(nèi)存釋放,有效減輕了開發(fā)人員的工作壓力:當(dāng)變量超出其作用域時(shí),內(nèi)存就會被自動釋放。

讓我們回到正題。在Rust中,對于復(fù)雜類型,諸如為變量賦值、參數(shù)傳遞或函數(shù)返回值這類的操作是不會執(zhí)行copy操作,而是會執(zhí)行move操作。簡而言之,對于復(fù)雜類型來說,通過轉(zhuǎn)移其所有權(quán)來完成上述任務(wù)。

當(dāng)復(fù)雜類型超出其作用域時(shí),Rust將調(diào)用drop函數(shù)顯式地執(zhí)行內(nèi)存釋放。

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

當(dāng)所有者超出作用域時(shí),該值將被刪除。再次考慮之前的示例:

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

println!("{}, world!", s1); // Won't compile. The value of s1 has already been dropped.

在將s1分配給s2之后(在let s2 = s1賦值語句中),s1的值就被釋放了。因此,賦值語句執(zhí)行后,s1就失效了。s1被釋放后的內(nèi)存狀態(tài):

圖片

圖11:s1被釋放后的內(nèi)存狀態(tài)

9.所有權(quán)如何變動

在Rust程序中,有三種方式可以在變量之間轉(zhuǎn)移所有權(quán):

(1)將一個(gè)變量的值分配給另一個(gè)變量(前文已經(jīng)討論過)。

(2)將值傳遞給函數(shù)。

(3)從函數(shù)返回值。

將值傳遞給函數(shù)

將值傳遞給函數(shù)與為變量賦值有相似的語義。就像賦值一樣,將變量傳遞給函數(shù)會導(dǎo)致變量被移動或復(fù)制。下面的例子向我們展示了復(fù)制和移動:

fn main() {
let s = String::from("hello"); // s comes into scope

move_ownership(s); // s's value moves into the function...
// so it's no longer valid from this
// point forward
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function
// It follows copy semantics since it's
// primitive, so we use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.

fn move_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called.
// The occupied memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

如果我們在調(diào)用move_ownership之后嘗試使用s,Rust會拋出編譯錯(cuò)誤。

從函數(shù)返回值

從函數(shù)返回值同樣可以轉(zhuǎn)移所有權(quán)。下面是一個(gè)包含返回值的函數(shù)示例,其注釋與上一個(gè)示例中的注釋相同。

fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}

// This function takes a String and returns it
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope

a_string // a_string is returned and moves out to the calling function
}

變量的所有權(quán)改變始終遵循相同的模式:當(dāng)值被分配給另一個(gè)變量時(shí),就會觸發(fā)move操作。除非數(shù)據(jù)的所有權(quán)被轉(zhuǎn)移至另一個(gè)變量,否則當(dāng)包含堆上數(shù)據(jù)的變量超出作用域時(shí),該值將被清除。

希望這能讓大家對所有權(quán)模型及其對Rust處理數(shù)據(jù)的方式(例如賦值引用和參數(shù)傳遞)有一個(gè)基本的了解。

等等。還有一件事…

沒有什么是完美無缺的,Rust所有權(quán)模型同樣有其局限性。當(dāng)我們開始使用Rust后,很快就會發(fā)現(xiàn)一些使用不順手的地方。我們已經(jīng)觀察到,獲取所有權(quán)然后函數(shù)傳遞所有權(quán)就有一些不順手。

令人討厭的是,假設(shè)我們想再次使用數(shù)據(jù),那么傳遞給函數(shù)的所有內(nèi)容都必須執(zhí)行返回操作,即使其他函數(shù)已經(jīng)返回了部分相關(guān)數(shù)據(jù)。如果我們需要一個(gè)函數(shù)只使用數(shù)據(jù)但是不獲取數(shù)據(jù)所有權(quán),又該如何操作呢?

參考以下示例。下面的代碼示例將報(bào)錯(cuò),因?yàn)橐坏┧袡?quán)轉(zhuǎn)移到print_vector函數(shù),變量v就不能再被最初擁有它的main函數(shù)(在 println 中!)使用。

fn main() {
let v = vec![10,20,30];
print_vector(v);
println!("{}", v[0]); // this line gives us an error
}

fn print_vector(x: Vec<i32>) {
println!("Inside print_vector function {:?}",x);
}

跟蹤所有權(quán)看似很容易,但是當(dāng)我們面對復(fù)雜的大型程序時(shí),它就會變得非常復(fù)雜。所以我們需要一種在不轉(zhuǎn)移“所有權(quán)”的情況下傳遞數(shù)據(jù)的方法,這就是“借用”概念發(fā)揮作用的地方。

借用 

借用,從字面意義上來說,就是收到一個(gè)物品并承諾會歸還。在Rust的上下文中,借用是一種在不獲取所有權(quán)的情況下訪問數(shù)據(jù)的方式,因?yàn)樗仨氃谇‘?dāng)?shù)臅r(shí)機(jī)返還給所有者。

當(dāng)我們借用一個(gè)數(shù)據(jù)時(shí),我們用運(yùn)算符&引用它的內(nèi)存地址。&被稱為引用。引用本身并沒有什么特別之處——它只是對內(nèi)存地址的指向。對于熟悉C語言指針的人來說,引用是指向內(nèi)存的指針,其中包含屬于另一個(gè)變量的數(shù)據(jù)。值得注意的是,Rust中的引用不能為空。實(shí)際上,引用就是一個(gè)指針;它是最基本的指針類型。大多數(shù)編程語言中只有一種指針類型,但Rust有多種指針類型。指針及其類型是另外一個(gè)話題,以后有機(jī)會將會單獨(dú)討論。

簡而言之,Rust將對某個(gè)數(shù)據(jù)的引用稱為借用,該數(shù)據(jù)最終必須返回給所有者。示例如下:

let x = 5;
let y = &x;

println!("Value y={}", y);
println!("Address of y={:p}", y);
println!("Deref of y={}", *y);

上述代碼生成以下輸出:

Value y=5
Address of y=0x7fff6c0f131c
Deref of y=5

示例中,變量y借用了變量x擁有的數(shù)據(jù),而x仍然擁有該數(shù)據(jù)的所有權(quán)。我們稱之為y對x的引用。當(dāng)y超出范圍時(shí),借用關(guān)系結(jié)束,由于y沒有該數(shù)據(jù)的所有權(quán),因此該數(shù)據(jù)不會被銷毀。要借用數(shù)據(jù),請使用運(yùn)算符&進(jìn)行引用。{:p}代表輸出以十六進(jìn)制表示的內(nèi)存位置。

在上面的代碼示例中,“*”(即星號)是對引用變量進(jìn)行操作的解引用運(yùn)算符。這個(gè)解引用運(yùn)算符允許我們獲取指針指向的內(nèi)存地址中的數(shù)據(jù)。

下面是函數(shù)通過借用,在不獲取所有權(quán)的情況下使用數(shù)據(jù)的示例:

fn main() {
let v = vec![10,20,30];
print_vector(&v);
println!("{}", v[0]); // can access v here as references can't move the value
}

fn print_vector(x: &Vec<i32>) {
println!("Inside print_vector function {:?}", x);
}

我們將引用 (&v)(又名pass-by-reference)而非所有權(quán)(即pass-by-value)傳遞給print_vector函數(shù)。因此在main函數(shù)中調(diào)用print_vector函數(shù)后,我們就可以訪問v了。

1.通過解引用運(yùn)算符跟蹤指針的指向數(shù)據(jù)

如前所述,引用是指針的一種類型,可以將指針視為指向存儲在其他位置的數(shù)據(jù)的箭頭。下面是一個(gè)示例:

let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);

在上面的代碼中,我們創(chuàng)建了一個(gè)對i32類型數(shù)據(jù)的引用,然后使用解引用運(yùn)算符跟蹤被引用的數(shù)據(jù)。變量x存儲一個(gè)i32類型的值5。我們將y設(shè)置為對x的引用。

下面是棧內(nèi)存的狀態(tài):

圖片

棧內(nèi)存狀態(tài)

我們可以斷言x等于5。然而,假如我們需要對y中的數(shù)據(jù)進(jìn)行斷言,就必須使用*y來跟蹤它所引用的數(shù)據(jù)(因此在這里解引用)。一旦我們解開了y的引用,就可以訪問y指向的整型數(shù)據(jù),然后將其與5進(jìn)行比較。

如果我們嘗試寫assert_eq!(5, y);就會得到以下編譯錯(cuò)誤提示:

error[E0277]:  can't compare `{integer}` with `&{integer}`
--> src/main.rs:11:5
|
11 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`

左右滑動查看完整代碼由于它們是不同的數(shù)據(jù)類型,因此無法對數(shù)字和數(shù)字的引用進(jìn)行比較。所以,我們必須通過解引用運(yùn)算符來跟蹤其指向的真實(shí)數(shù)據(jù)。

2.默認(rèn)情況下,引用是不可變的

默認(rèn)情況下,引用和變量一樣是不可變的——可以通過mut使其可變,但前提是它的所有者也是可變的:

let mut x = 5;
let y = &mut x;

不可變引用也被稱為共享引用,而可變引用也被稱為獨(dú)占引用。

我們觀察下面的案例。我們賦予對引用的只讀權(quán)限,因?yàn)槲覀兪褂玫氖沁\(yùn)算符&而非&mut。即使源變量n是可變的,ref_to_n和another_ref_to_n也不是可變的,因?yàn)樗鼈冎皇莕借用,并沒有n的所有權(quán)。

let mut n = 10;
let ref_to_n = &n;
let another_ref_to_n = &n;

借用檢查器將拋出以下錯(cuò)誤:

error[E0596]:  cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:4:9
|
3 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
4 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable

3.借用規(guī)則

有人可能會有疑問,為什么有些情況下開發(fā)人員更傾向于使用move而非借用。如果這是關(guān)鍵點(diǎn),那么為什么Rust還會有move語義,以及為什么不將默認(rèn)機(jī)制設(shè)置為借用?根本原因是在Rust中借用的使用是受到限制的。只有在特定情況下才允許借用。

借用有自己的一套規(guī)則,借用檢查器在編譯期間會嚴(yán)格執(zhí)行這些規(guī)則。制定這些規(guī)則是為了防止數(shù)據(jù)競爭。規(guī)則如下:

(1)借用者的作用域不能超過所有者的作用域。

(2)不可變引用的數(shù)量不受限制,但可變引用只能存在一個(gè)。

(3)所有者可以擁有可變引用或不可變引用,但不能同時(shí)擁有兩者。

(4)所有引用必須有效(不能為空)。

4.引用的作用域不能超過所有者的作用域

引用的作用域必須包含在所有者的作用域內(nèi)。否則,可能會引用一個(gè)已釋放的數(shù)據(jù),從而導(dǎo)致UAF錯(cuò)誤。

let x;
{
let y = 0;
x = &y;
}
println!("{}", x);

上面的示例程序嘗試在所有者y超出作用域后取消x對y的引用。Rust阻止了這種UAF錯(cuò)誤。

5.可以有很多不可變引用,但只允許有一個(gè)可變引用

特定數(shù)據(jù)可以擁有數(shù)量不受限制的不可變引用(又名共享引用),但只允許有一個(gè)可變引用(又名獨(dú)占引用)。這條規(guī)則的存在是為了消除數(shù)據(jù)競爭。當(dāng)兩個(gè)引用同時(shí)指向一個(gè)內(nèi)存位置,至少有一個(gè)在執(zhí)行寫操作,且它們的動作沒有進(jìn)行同步時(shí),這就被稱為數(shù)據(jù)競爭。

我們可以有數(shù)量不受限制的不可變引用,因?yàn)樗鼈儾粫臄?shù)據(jù)。另一方面,借用機(jī)制限制我們同一時(shí)刻只能擁有一個(gè)可變引用(&mut),目的是降低在編譯時(shí)出現(xiàn)數(shù)據(jù)競爭的可能性。

我們看看如下示例:

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

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

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

上面的代碼嘗試為s創(chuàng)建兩個(gè)可變引用(r1 和 r2),這最終會執(zhí)行失?。?/span>

error[E0499]:  cannot borrow `s` as mutable more than once at a time
--> src/main.rs:6:14
|
5 | let r1 = &mut s;
| ------ first mutable borrow occurs here
6 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
7 |
8 | println!("{}, {}", r1, r2);
| -- first borrow later used here

總結(jié) 

希望本文能夠澄清所有權(quán)和借用的概念。我還簡單介紹了借用檢查器,它是實(shí)現(xiàn)所有權(quán)和借用的基礎(chǔ)。正如我在開頭提到的,所有權(quán)是一個(gè)很有創(chuàng)意的想法,即使對于經(jīng)驗(yàn)豐富的開發(fā)人員來說,初次接觸也會很難理解其設(shè)計(jì)理念和使用方法,但是隨著使用經(jīng)驗(yàn)的增加,它就會成為開發(fā)人員得心應(yīng)手的工具。這只是Rust如何增強(qiáng)內(nèi)存安全的簡要說明。我試圖使這篇文章盡可能的易于理解,同時(shí)提供足夠的信息來掌握這些概念。有關(guān)Rust所有權(quán)特性的更多詳細(xì)信息,請查看他們的在線文檔。

當(dāng)開發(fā)項(xiàng)目對性能要求高時(shí),Rust是一個(gè)不錯(cuò)的選擇,它解決了困擾其他語言的很多痛點(diǎn),并以陡峭的學(xué)習(xí)曲線向前邁出了重要的一步。Rust連續(xù)第六年成為Stack Overflow最受歡迎的語言,這意味著很多人將會有機(jī)會使用它并愛上了它。Rust社區(qū)正在持續(xù)發(fā)展壯大。隨著我們走向未來,Rust似乎正走在無限光明的道路上。祝大家學(xué)習(xí)愉快!

原文鏈接:

https://hackernoon.com/rusts-ownership-and-borrowing-enforce-memory-safety

譯者介紹

仇凱,51CTO社區(qū)編輯,目前就職于北京宅急送快運(yùn)股份有限公司,職位為信息安全工程師。主要負(fù)責(zé)公司信息安全規(guī)劃和建設(shè)(等保,ISO27001),日常主要工作內(nèi)容為安全方案制定和落地、內(nèi)部安全審計(jì)和風(fēng)險(xiǎn)評估以及管理。

責(zé)任編輯:張潔 來源: 51CTO技術(shù)棧
相關(guān)推薦

2024-09-02 10:40:18

2017-07-27 13:34:52

Rust所有權(quán)數(shù)據(jù)

2024-03-19 14:43:55

Rust編譯所有權(quán)

2024-01-10 09:26:52

Rust所有權(quán)編程

2011-01-07 09:19:35

Linux文件權(quán)限

2024-04-24 12:41:10

Rust安全性內(nèi)存

2021-07-30 05:12:54

智能指針C++編程語言

2022-11-03 15:14:43

Linux文件權(quán)限

2022-08-29 07:31:48

HashMap線程擴(kuò)容

2009-09-12 09:46:47

Windows 7所有權(quán)添加

2024-12-23 14:46:24

2009-11-28 20:21:14

2013-08-16 10:46:20

2022-03-18 08:00:00

區(qū)塊鏈代幣以太坊

2011-01-20 07:50:51

Linux文件系統(tǒng)管理所有權(quán)

2021-09-06 10:21:27

JavaScript表單對象 前端

2023-07-14 08:00:00

ORMRust ORMSQL

2022-06-20 09:09:26

IDaaSIAM身份即服務(wù)

2017-10-23 12:42:42

2022-05-30 00:19:13

元宇宙NFTWeb3
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號