楔子 所有權(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ī)則記下來,我們隨后會通過示例來解釋它們:
作為所有權(quán)的第一個示例,我們先來了解一下變量的作用域。簡單來講,作用域是一個對象在程序中有效的范圍,假設(shè)有這樣一個變量:
因此要注意兩個關(guān)鍵點:
所以 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 實例:
這里的雙冒號運算符允許我們調(diào)用置于 String 命名空間下面的特定函數(shù) from,而不需要使用類似于 string_from 這樣的名字。我們會在后續(xù)著重講解這個語法,并討論基于模塊的命名空間。 上面定義的字符串對象是可以動態(tài)變化的:
所以 String 是可變的,但是字符串字面量不允許改變:
但是問題來了,為什么 String 是可變的,而字符串字面量不是?這是因為它們采用了不同的內(nèi)存處理方式。 內(nèi)存與分配 對于字符串字面量而言,由于我們在編譯時就知道內(nèi)容,所以這部分硬編碼的文本被直接嵌入到了最終的可執(zhí)行文件中。這就是訪問字符串字面量異常高效的原因,而這些性質(zhì)完全得益于字符串字面量的不可變性。 但不幸的是,我們沒有辦法將那些未知大小的文本在編譯期統(tǒng)統(tǒng)放入二進(jìn)制文件中,更何況這些文本的大小還可能隨著程序的運行而發(fā)生改變。 對于 String 類型而言,為了支持一個可變的、可增長的文本類型,我們需要在堆上分配一塊在編譯時未知大小的內(nèi)存來存放數(shù)據(jù)。這同時也意味著:
這里的第一步由我們,也就是程序的編寫者,在調(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)行釋放。
審視上面的代碼,會發(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)行交互。
這段代碼的執(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ù)制的效率也是非常高的。
可如果是堆區(qū)的數(shù)據(jù)就不一定了,比如 String,我們將上面的代碼改一下。
以上兩段代碼非常相似,你也許會假設(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)生潛在的安全隱患。
但問題是堆上的內(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)移,那么之前的變量就不能再用了,我們舉個例子:
因此一定要理解所有權(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)在其它語言中見過類似的東西。
當(dāng)你看到某處調(diào)用了 clone 時,你就應(yīng)該知道某些特定的代碼將會被執(zhí)行,而且這些代碼可能會相當(dāng)消耗資源。 棧上數(shù)據(jù)的復(fù)制 這些概念上面已經(jīng)說過了,但是提到了深淺拷貝,所以我們將兩者結(jié)合起來再說一下。
這與我們剛剛學(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 的類型:
所有權(quán)與函數(shù) 將值傳遞給函數(shù)在語義上類似于對變量進(jìn)行賦值,而將變量傳遞給函數(shù)等價于變量的傳遞,因此同樣會觸發(fā)移動或復(fù)制。
總而言之,函數(shù)里面的參數(shù)也是一個變量,所以把變量傳到函數(shù)里面,和把變量賦值給另一個變量是等價的。既然等價,那么表現(xiàn)出的行為也是一致的。 返回值與作用域 函數(shù)在返回的過程中也會發(fā)生所有權(quán)的轉(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é)果。我們舉個栗子:
但這種寫法未免太過笨拙了,因為類似的概念在編程工作中相當(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é)束。
所以對于那些已經(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ù)。 |
|