rust-所有權

什么是所有權
所有權是一組規則,它決定了 Rust 程序如何管理內存。所有運行中的程序都必須管理它們對計算機內存的使用方式。某些語言使用垃圾回收(GC),在程序運行時定期查找不再使用的內存;另一些語言則要求程序員顯式地分配和釋放內存。Rust 采用第三種方式:通過一套編譯期檢查的“所有權系統”來管理內存。一旦違反這些規則,程序就無法通過編譯。所有權機制的任何特性都不會在運行時拖慢程序。

對許多開發者來說,所有權是一個全新概念,確實需要一定時間適應。好消息是:隨著你對 Rust 及所有權規則愈發熟悉,你會自然而然寫出既安全又高效的代碼。堅持下去!

理解了所有權,你就掌握了理解 Rust 獨特特性的基石。本章將通過一種非常常見的數據結構——字符串(String)——的示例來學習所有權。

棧(Stack)與堆(Heap)
很多高級語言很少要求你關注棧和堆。但在像 Rust 這樣的系統編程語言里,值位于棧還是堆,會直接影響語言的行為以及你為何必須做出某些決策。后面講解所有權時,會結合棧和堆的概念,因此先簡要說明。

棧和堆都是可供代碼在運行時使用的內存區域,但組織方式不同。

  • 棧按“后進先出”順序存取值:就像一摞盤子,后放的在上,先取最上面的。往棧里加數據叫“壓棧”(push),移除叫“彈棧”(pop)。棧上所有數據的大小必須在編譯期已知且固定。
  • 堆則沒那么有序:把數據放入堆時,先向內存分配器申請一塊足夠大的空間,分配器標記該空間為“已用”,并返回指向此位置的指針。這個過程叫“堆分配”(簡稱分配),壓棧不算分配。由于指針大小固定,可以把指針存在棧上;真正訪問數據時,必須順著指針去堆里拿。就像進餐廳時,告訴服務員你們幾個人,他找一張空桌領你們過去,后來者問服務員即可找到你們。

壓棧比堆分配更快,因為無需尋找空閑位置;棧頂永遠是下一個位置。堆分配需要找到足夠大的空間,并記錄元數據以備后續分配,工作量更大。

訪問堆數據也比棧慢,需要一次指針跳轉。現代處理器在內存連續時更快。類似地,服務員若逐桌收齊一桌的訂單再換下一桌,效率最高;若來回穿插,則慢得多。同理,處理器處理棧上緊密排布的數據更高效。

當函數被調用,傳入的值(可能包括指向堆數據的指針)以及局部變量都會壓棧;函數結束時,這些值被彈棧。

追蹤哪些代碼正在使用堆上的哪些數據、減少堆上重復數據、及時清理不再使用的數據以免耗盡內存——這些正是所有權要解決的問題。理解所有權后,你無須時刻惦記棧和堆,但明白“所有權主要用來管理堆數據”有助于理解其設計初衷。

所有權規則
先記住三條核心規則,后面示例會逐一闡釋:

  1. Rust 中每個值都有且僅有一個所有者(owner)。
  2. 同一時間只能有一個所有者。
  3. 所有者離開作用域(scope)時,該值被丟棄(drop)。

變量的作用域
我們不再在每個示例中寫 fn main() { ... },請自行把代碼放進 main 函數。先看變量作用域:作用域指一個項在程序中有效的范圍。例如:

{                      // s 尚未聲明,不可用let s = "hello";   // 從這里開始 s 有效// 使用 s
}                      // 作用域結束,s 失效

重點:

  • s 進入作用域時生效。
  • 離開作用域時失效。

這與多數語言類似。接下來引入 String 類型,以進一步說明所有權。

String 類型
為了展示所有權規則,我們需要比第 3 章更復雜的數據類型。之前提到的類型大小已知,可放棧上,作用域結束時彈出,且易于按位復制出獨立副本。現在我們想研究存放在堆上的數據,以及 Rust 如何決定何時清理它們——String 是很好的例子。

我們已見過字符串字面量("hello"),其值在編譯期已知并直接寫入可執行文件,速度快、效率高,但不可變,且無法在編譯期確定所有文本(如用戶輸入)。于是 Rust 提供第二種字符串類型 String,它在堆上管理數據,允許存儲編譯期大小未知的文本。可用 String::from 由字面量創建:

let s = String::from("hello");

:: 語法把 from 置于 String 命名空間下,第 5 章與第 7 章會再談。
String 可被修改:

let mut s = String::from("hello");
s.push_str(", world!");
println!("{s}"); // 輸出 `hello, world!`

為何 String 可變,而字面量不行?關鍵在于二者內存處理方式不同。

內存與分配

  • 字面量內容在編譯期已知,直接嵌入可執行文件,因此不可變。
  • String 需支持可增長文本,于是:
    1. 在運行時向內存分配器申請未知大小的堆內存。
    2. 用完后需將此內存歸還(釋放)。

第一步由 String::from 完成,與多數語言相同。第二步則不同:

  • 有 GC 的語言由 GC 清理;
  • 無 GC 的語言通常需程序員顯式釋放,易出錯:忘了解放會泄漏,過早釋放為懸垂指針,重復釋放是 bug。

Rust 的做法:變量離開作用域時自動歸還內存。例如:

{let s = String::from("hello"); // 申請內存// 使用 s
} // 作用域結束,Rust 自動調用 drop 釋放內存

C++ 中類似模式叫 RAII(資源獲取即初始化)。Rust 的 drop 函數即此思想的體現。

變量與數據:移動(Move)
Rust 中多個變量可與同一數據交互。先看整數示例:

let x = 5;
let y = x;

整數大小固定,直接復制值壓棧,于是 xy 均為 5。

再看 String

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

看起來相似,實則不然。如圖 4-1 所示,String 由三部分組成(存棧上):指向堆內容的指針、長度、容量;右側堆上才是真正的字符數據。
4-1
圖 4-1:變量 s1 綁定到值為 "hello"String 在內存中的表示

  • length(長度)表示該 String 的內容當前占用的字節數。
  • capacity(容量)表示該 String 從分配器處獲得的堆內存總字節數。
    二者有區別,但在本節并不重要,可先忽略容量。

當我們執行 let s2 = s1; 時,復制的是棧上的那三部分數據(指針、長度、容量),而不會復制指針所指向的堆上的實際內容。換句話說,內存中的數據表示如圖 4-2 所示。
4-2
圖 4-2:變量 s2 復制了 s1 的指針、長度和容量后的內存示意圖
(并沒有復制堆上的實際數據)

這種表示并不是圖 4-3 所展示的情況——圖 4-3 表示的是“連堆上的數據也一并深拷貝”后的內存布局。
如果 Rust 真的那樣做,當堆上的數據很大時,s2 = s1 這一操作在運行時就會變得非常昂貴。
4-3
圖4-3:如果Rust也復制堆數據,s2 = s1可能的另一種行為

我們之前提到,當一個變量超出作用域時,Rust會自動調用drop函數并清理該變量的堆內存。但圖4-2顯示兩個數據指針指向同一個位置。這是一個問題:當s2和s1超出作用域時,它們都會嘗試釋放相同的內存。這被稱為雙重釋放錯誤,是我們之前提到的內存安全漏洞之一。釋放內存兩次可能導致內存損壞,進而可能引發安全漏洞。

為了確保內存安全,在執行let s2 = s1;這行代碼后,Rust認為s1不再有效。因此,當s1超出作用域時,Rust不需要釋放任何東西。看看在創建s2之后嘗試使用s1會發生什么;它不會工作:

這段代碼無法編譯!

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

你會得到這樣的錯誤,因為Rust阻止你使用無效的引用:

$ cargo runCompiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`--> src/main.rs:5:15|
2 |     let s1 = String::from("hello");|         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;|              -- value moved here
4 |
5 |     println!("{s1}, world!");|               ^^^^ value borrowed here after move|= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable|
3 |     let s2 = s1.clone();|                ++++++++For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

如果你在使用其他語言時聽說過淺拷貝和深拷貝的概念,那么只復制指針、長度和容量而不復制數據的想法聽起來可能像是淺拷貝。但由于Rust還會使第一個變量失效,因此它不被稱為淺拷貝,而是被稱為移動(move)。在這個例子中,我們會說s1被移動到了s2。因此,實際發生的情況如圖4-4所示。
4-4
圖4-4:s1失效后內存中的表示

這解決了我們的問題!只有s2是有效的,當它超出作用域時,它將獨自釋放內存,任務完成。

此外,這里還隱含了一個設計選擇:Rust永遠不會自動創建數據的“深拷貝”。因此,任何自動拷貝都可以假設在運行時性能方面是廉價的。

作用域與賦值

這一規則的反面也適用于作用域、所有權以及通過drop函數釋放內存之間的關系。當你將一個全新的值賦給一個已存在的變量時,Rust會立即調用drop并釋放原始值的內存。考慮以下代碼,例如:

let mut s = String::from("hello");
s = String::from("ahoy");println!("{s}, world!");

我們最初聲明了一個變量s,并將其綁定到一個值為"hello"的String。然后我們立即創建了一個值為"ahoy"的新String,并將其賦給s。此時,沒有任何東西引用堆上的原始值了。
4-5
圖 4-5:初始值被完全替換后在內存中的表示。

因此,原始字符串會立即超出作用域。Rust 會調用 drop 函數來釋放它的內存。當我們打印最終的值時,它將是“ahoy, world!”。

變量和數據的克隆操作
如果我們確實需要深度復制 String 的堆數據,而不僅僅是棧數據,我們可以使用一個常見的方法,稱為 clone。我們將在第 5 章討論方法的語法,但由于方法是許多編程語言中的常見特性,你可能之前已經見過。

以下是一個 clone 方法的示例:

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

這可以正常工作,并明確地表現出圖 4-3 中所示的行為,即堆數據確實被復制了。

當你看到對 clone 的調用時,你應該知道正在執行一些任意代碼,而這些代碼可能代價高昂。它是一個視覺提示,表明正在發生一些不同的事情。

僅在棧上的數據:Copy 特性
我們還沒有提到的另一個細節是,使用整數的代碼——其中一部分在清單 4-2 中展示過——可以正常工作且有效:

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

但這段代碼似乎與我們剛剛學到的內容相矛盾:我們沒有調用 clone,但 x 仍然有效,并且沒有被移動到 y 中。

原因是像整數這樣在編譯時已知大小的類型完全存儲在棧上,因此實際值的副本可以快速生成。這意味著我們沒有理由阻止在創建變量 yx 仍然有效。換句話說,在這里淺拷貝和深拷貝沒有區別,因此調用 clone 與通常的淺拷貝沒有什么不同,我們可以省略它。

Rust 有一個特殊的注解,稱為 Copy 特性,我們可以將其應用于存儲在棧上的類型,例如整數(我們將在第 10 章中更多地討論特性)。如果一個類型實現了 Copy 特性,使用它的變量不會被移動,而是被簡單地復制,這使得它們在賦值給另一個變量后仍然有效。

如果類型本身或其任何部分實現了 Drop 特性,Rust 不會允許我們為該類型添加 Copy 注解。如果類型在值超出作用域時需要執行一些特殊操作,而我們為該類型添加了 Copy 注解,那么我們將會得到一個編譯時錯誤。要了解如何為你的類型添加 Copy 注解以實現該特性,請參閱附錄 C 中的“可派生特性”。

那么,哪些類型實現了 Copy 特性呢?你可以查看給定類型的文檔來確認,但一般來說,任何一組簡單的標量值都可以實現 Copy,而任何需要分配內存或是一種資源的類型都不能實現 Copy。以下是一些實現了 Copy 的類型:

  • 所有整數類型,例如 u32
  • 布爾類型 bool,其值為 truefalse
  • 所有浮點數類型,例如 f64
  • 字符類型 char
  • 如果元組只包含也實現了 Copy 的類型,則元組也實現 Copy。例如,(i32, i32) 實現了 Copy,但 (i32, String) 則沒有。

所有權和函數
將值傳遞給函數的機制與將值賦給變量時的機制類似。將變量傳遞給函數會移動或復制,就像賦值一樣。清單 4-3 有一個帶有注釋的示例,顯示了變量進入和超出作用域的位置。

文件名:src/main.rs

fn main() {let s = String::from("hello");  // s 進入作用域takes_ownership(s);             // s 的值被移動到函數中...// ...因此在這里不再有效let x = 5;                      // x 進入作用域makes_copy(x);                  // 因為 i32 實現了 Copy 特性,// x 沒有被移動到函數中,println!("{}", x);              // 因此之后仍然可以使用 x} // 這里,x 超出作用域,然后是 s。但由于 s 的值被移動了,所以沒有// 特殊的事情發生。fn takes_ownership(some_string: String) { // some_string 進入作用域println!("{some_string}");
} // 這里,some_string 超出作用域,并且調用 `drop`。后端// 內存被釋放。fn makes_copy(some_integer: i32) { // some_integer 進入作用域println!("{some_integer}");
} // 這里,some_integer 超出作用域。沒有特殊的事情發生。

清單 4-3:帶有所有權和作用域注釋的函數

如果我們試圖在調用 takes_ownership 之后使用 s,Rust 會在編譯時拋出錯誤。這些靜態檢查可以保護我們免于犯錯。嘗試在 main 中添加使用 sx 的代碼,看看你可以在哪里使用它們,以及所有權規則阻止你在哪里使用它們。

返回值和作用域
返回值也可以轉移所有權。清單 4-4 展示了一個返回某些值的函數的示例,其注釋與清單 4-3 中的類似。

文件名:src/main.rs

fn main() {let s1 = gives_ownership();        // gives_ownership 將其返回值移動到 s1 中let s2 = String::from("hello");    // s2 進入作用域let s3 = takes_and_gives_back(s2); // s2 被移動到// takes_and_gives_back 中,該函數也// 將其返回值移動到 s3 中
} // 這里,s3 超出作用域并被釋放。s2 被移動了,因此沒有// 發生任何事情。s1 超出作用域并被釋放。fn gives_ownership() -> String {       // gives_ownership 將其返回值移動到調用它的函數中let some_string = String::from("yours"); // some_string 進入作用域some_string                        // some_string 被返回并移動到調用函數中
}// 這個函數接收一個 String 并返回一個 String。
fn takes_and_gives_back(a_string: String) -> String {// a_string 進入作用域a_string  // a_string 被返回并移動到調用函數中
}

清單 4-4:返回值的所有權轉移

變量的所有權每次遵循相同的模式:將值賦給另一個變量會移動它。當包含堆數據的變量超出作用域時,除非數據的所有權被移動到另一個變量,否則值將通過 drop 被清理。

雖然這可以工作,但每次函數都獲取所有權然后再返回所有權會有些繁瑣。如果我們想讓函數使用一個值但不獲取所有權怎么辦?我們傳遞的任何東西都需要再次返回,這相當煩人,尤其是當我們還想返回函數體中可能產生的任何數據時。

幸運的是,Rust 允許我們使用元組返回多個值,如清單 4-5 所示。

文件名:src/main.rs

fn main() {let s1 = String::from("hello");let (s2, len) = calculate_length(s1);println!("'{}' 的長度是 {}。", s2, len);
}fn calculate_length(s: String) -> (String, usize) {let length = s.len(); // len() 返回一個 String 的長度(s, length)
}

清單 4-5:返回參數的所有權

但這仍然過于繁瑣,對于一個應該很常見的概念來說,工作量太大了。幸運的是,Rust 有一個特性,可以在不轉移所有權的情況下使用值,稱為引用。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/90333.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/90333.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/90333.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

破解哈希極化:基于主動路徑規劃的智算網絡負載均衡方案

如今人工智能(AI)和大模型訓練的蓬勃發展,大規模AI算力集群(智算集群)已成為關鍵基礎設施。這類集群對網絡性能,特別是高吞吐、低延遲和無損特性有著嚴苛要求,RoCE因此被廣泛應用。然而&#xf…

Vue工程化 ElementPlus

一、Vue工程化1、環境準備create-vue是Vue官方提供的最新的腳手架工具,用于快速生成一個工程化的Vue項目。提供了以下功能:統一的目錄結構本地調試熱部署單元測試集成打包上線依賴環境:Node JS 是一個免費、開源、跨平臺的JavaScript運行時環…

深入解析TCP:可靠傳輸的核心機制與實現邏輯

Linux 系列 文章目錄Linux 系列前言一、TCP協議的概念1.1 TCP協議的特點1.2 TCP又叫做傳輸控制協議二、TCP協議段格式2.1、TCP的流量控制----------窗口大小(16位)2.2 TCP的確認應答機制2.2.1 什么是確認應答機制2.2.2 確認應答機制的優化2.3 超時重傳機…

通縮浪潮中的 “測量防線”:新啟航如何用國產 3D 白光干涉儀筑牢半導體成本護城河?

一、通縮浪潮下半導體行業的成本困局在通縮浪潮沖擊下,半導體行業面臨市場需求疲軟、產品價格下滑的嚴峻挑戰。為維持競爭力,降低生產成本成為企業生存發展的關鍵。而 3D 白光干涉儀作為半導體晶圓檢測、制程監控的核心設備,傳統進口產品價格…

[網安工具] 自動化威脅檢測工具 —— D 盾 · 使用手冊

🌟想了解其它網安工具?看看這個:[網安工具] 網絡安全工具管理 —— 工具倉庫 管理手冊 D盾防火墻D盾,D盾_防火墻,D盾_IIS防火墻,D盾_web查殺,IIS防火墻,webshell查殺,https://www.d99net.net/ 0x01:D 盾 —— 工具簡介 D 盾防火…

Spring AI 系列之二十二 - ImageModel

之前做個幾個大模型的應用,都是使用Python語言,后來有一個項目使用了Java,并使用了Spring AI框架。隨著Spring AI不斷地完善,最近它發布了1.0正式版,意味著它已經能很好的作為企業級生產環境的使用。對于Java開發者來說…

Redis集群高可用與性能優化實戰指南

Redis集群高可用與性能優化實戰指南 一、業務場景描述 在大型分布式系統中,Redis不僅承擔緩存職責,還常用于限流、排行榜、會話管理等高并發場景。隨著訪問量的激增和集群規模的擴展,如何保證Redis服務的高可用性與高性能,成為后端…

基于SpringBoot+Vue的高校特長互助系統(WebSocket實時聊天、協同過濾算法、ECharts圖形化分析)

“ 🎈系統亮點:WebSocket實時聊天、協同過濾算法、ECharts圖形化分析”01系統開發工具與環境搭建前后端分離架構項目架構:B/S架構運行環境:win10/win11、jdk17前端:技術:框架Vue.js;UI庫&#x…

于縱橫交錯的矩陣間:二維數組與多維數據的默契和鳴

大家好啊,我是小象?(?ω?)? 我的博客:Xiao Xiangζ????? 很高興見到大家,希望能夠和大家一起交流學習,共同進步。* 接著上節課的內容,這一節我們來學習二維數組,學習二維數組的概念和創建,明白二維數組的初始化,學會不完全初始化,完全初始化,按照行初始化的…

SHA-3算法詳解

SHA-3(Secure Hash Algorithm 3)是美國國家標準與技術研究院(NIST)于 2015 年發布的新一代密碼哈希算法標準,其核心基于比利時密碼學家團隊設計的Keccak 算法。SHA-3 的誕生旨在應對 SHA-1 和 SHA-2 系列算法可能面臨的…

前端筆記:同源策略、跨域問題

只有前端才會有跨域問題后端不受限制 一、什么是“同源策略”(Same-Origin Policy) ? 定義: 瀏覽器的 同源策略 是一種 安全機制,限制一個源的 JavaScript 訪問另一個源的資源,以防止惡意網站竊取用戶敏感信息。 ? “…

java通過com進行pdf轉換docx丟失

使用,通過com調用,發現pdf轉換成docx后,沒有看到docx輸出到指定目錄。直接說解決方案:關閉的保護模式即可,打開工具,編輯->首選項 找到安全性(增強),關閉啟動時啟用保護模式關閉后,docx正常輸…

SQL基礎? | 視圖篇

0 序言 本文將系統講解數據庫中視圖的相關知識,包括視圖的定義、作用、創建(單表、多表、基于視圖創建)、查看、更新、修改與刪除操作,以及視圖的優缺點。 通過學習,你能夠掌握視圖的基本概念,理解何時及如…

移動云×華為昇騰:“大EP+PD分離”架構實現單卡吞吐量跨越式提升!

在面向下一代AI基礎設施的關鍵技術攻關中,移動云與華為昇騰計算團隊深度協同,實現了大模型推理引擎的架構級突破。雙方基于昇騰AI基礎軟硬件平臺,針對DeepSeek大模型完成了大規模專家并行(Expert Parallelism,簡稱“大…

配電自動化終端中電源模塊的設計

配電自動化終端中電源模塊的設計 引言 配電終端設備的可靠性和自動化程度,直接影響到整個配電自動化系統的可靠性和自動化水平。由于配電終端設備一般安裝于戶外或比較偏僻的地方,不可能有直流電源提供,因此,配電網終端設備的直流供電方式成為各配網自動化改造中必須要研究…

性能測試-groovy語言1

課程:B站大學 記錄軟件測試-性能測試學習歷程、掌握前端性能測試、后端性能測試、服務端性能測試的你才是一個專業的軟件測試工程師 Jmeter之Groovy語言Groovy簡介為何性能測試中選擇Groovywindows下載Groovy進入官網配置環境變量Groovy的數據類型groovy的保留字字符…

天邑TY1613_S905L3SB_安卓9-高安非-高安版-通刷-TTL線刷固件包

天邑TY1613_S905L3SB_安卓9-高安非-高安版-通刷-TTL線刷固件包刷機說明:本固件為TTL刷機方式,需要準備如下工具;電烙鐵TTL線刷機優盤TTL接觸點位于處理器左側,從上往下數第二腳GND、3TXD、4RXD跑碼工具-【工具大全】-putty跑碼工具…

【硬件-筆試面試題】硬件/電子工程師,筆試面試題-7,(知識點:晶體管放大倍數計算)

目錄 1、題目 2、解答 3、相關知識點 晶體管的電流分配關系 直流電流放大系數\(\overline{\beta}\) 交流電流放大系數\(\beta\) 晶體管的放大條件 總結 【硬件-筆試面試題】硬件/電子工程師,筆試面試題匯總版,持續更新學習,加油&…

力扣-152.乘積最大子數組

題目鏈接 152.乘積最大子數組 class Solution {public int maxProduct(int[] nums) {int[] dpMax new int[nums.length]; //包括nums[i]的乘積最大值int[] dpMin new int[nums.length]; //包括nums[i]的乘積最小值int res nums[0];dpMax[0] nums[0];dpMin[0] nums[0];fo…

HTTP/1.0、HTTP/1.1 和 HTTP/2.0 主要區別

一句話總結 HTTP/1.0: 短連接,每次請求都需要建立一個新的 TCP 連接,性能較差。HTTP/1.1: 長連接,默認開啟 Keep-Alive,連接可復用,解決了 1.0 的大部分問題,是目前使用最廣泛的版本。HTTP/2.0: 二進制、多…