Rust學習筆記(三)|所有權機制 Ownership

本篇文章包含的內容

  • 1 重新從堆和棧開始考慮
  • 2 所有權規則
  • 3 變量和數據(值)的交互方式
    • 3.1 移動 Move
    • 3.2 克隆 Clone
    • 3.3 復制 Copy
  • 4 函數與所有權
    • 4.1 參數傳遞時的所有權轉移
    • 4.2 函數返回時的所有權轉移
  • 5 引用和借用
  • 6 切片

前面兩篇僅僅介紹了一些Rust的語法以及一些程序書寫特點。如果是其他語言,其實已經可以說完成了六成以上的學習,可以開始著手項目,以實踐驅動學習了。但所有權和生命周期才是Rust的魅力所在,真正的難點現在才剛剛開始(噔噔咚)。


1 重新從堆和棧開始考慮

所有權是Rust最獨特的特性之一,使得它與Java、C#等語言相比不需要GC(Garbage Collector,垃圾收集器)就可以保證內存安全,同時也不需要像C/C++一樣手動釋放內存。為了理解所有權,我們必須了解Rust的內存分配機制,這是在之前學習的語言中基本不會注意的點。

無論哪種語言編寫的程序,都必須考慮他們運行時對計算機內存的操作方式。Rust并不相信程序員,但是也摒棄了GC算法這種低效的方式,取而代之的是引入所有權的概念,使程序中的內存操作錯誤在編譯時就基本解決,并且這種做法不會造成任何的運行時開銷。

在程序運行時,堆(Heap)和棧(Stack)都是程序可用的內存,它們的本質區別是內存組織的方式不同。棧內存先入后出,永遠有一個指針指向棧頂,內存的存儲是連續的,所有存儲在棧中的數據必須有已知的或者固定的大小;而堆內存相對比較混亂,程序使用的內存是碎片化的,一般在運行時申請的動態內存都屬于堆內存,操作系統在申請Heap時,需要申請一個足夠大的空間,并返回一個額外的指針變量記錄變量的存儲位置(并且需要做好記錄和管理方便下次分配),這導致程序運行時的指針可能存在大范圍的跳轉。總之,棧內存效率更高,堆內存以犧牲效率為代價換取了更多的靈活性。

所有權解決了以下問題:

  • 跟蹤代碼的哪些部分正在使用Heap的哪些數據;
  • 最小化Heap上的重復數據量;
  • 及時清理Heap上未使用的數據以避免空間不足。

2 所有權規則

Rust中所有權有以下三條規則(它很重要,先記下來再慢慢理解):

  1. 每個值都有一個變量,這個變量就是這個值的所有者;
  2. 每個值同時只能有一個所有者;
  3. 當所有者超出作用域(Scope)時,該值將被刪除。

下面是一個關于作用域(Scope)的簡單例子。作用域的概念在其他編程語言中也有,這里需要理解的是,s是變量,“hello”就是這個變量的值(一個字符串字面值)。

// s 無效
fn main() {// s 無效let s = "hello";	// s 可用// s 繼續有效
}	// s 的作用域從這里結束

通過第一部分的解釋,這里就比較好理解變量s的存儲方式了。它的值在編譯時就已經全部確定,并且不會隨之變化(如果需要變化則需要引入String類型),所以這個變量和它的值在編譯時就會被全部寫入可執行文件中。

與之相比,String類型在堆上分配,這使得它可以存儲在編譯時未知數量的文本。下面的例子中,s超出作用域時會自動調用一個特殊的名為drop的函數來釋放內存。所以String類型是一個實現了Drop trait(trait,接口)的類型。

fn main() {let mut s = String::from("Hello");s.push_str(", world!");println!("{}", s);		
}	// s 會自動調用一個drop函數

看到這里你可能依然一頭霧水(這家伙在說什么呢.jpg),這些概念和C/C++以及其他語言難道做不到嗎?超出作用域釋放內存難道不是理所當然的嗎?既然如此我還為什么要學Rust?Rust究竟好在哪?所謂的內存安全就這?
?
別急,這個Drop方法看似人畜無害,但是它會導致一個非常嚴重的bug。

3 變量和數據(值)的交互方式

3.1 移動 Move

首先看下面這個例子,創建了兩個簡單的整數變量,由于它們的大小是確定的,所以兩個變量都將被壓入棧中,值發生了復制。像整數這樣完全存放在棧上的數據實現了Copy trait。

let x = 5;
let y = x;		// value copied here

但是下面這個例子不同,s1在內存中的索引信息存儲在棧中,s1所對應的內容需要被存放在堆中(出于值的長度可變的需要)。棧中包含一個指向字符串存儲位置的指針,一個字符串實際長度,一個從操作系統中獲得的內存的總字節數。

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

在這里插入圖片描述
如果接下來接著執行這一語句,那么棧中s1的信息會被復制一份,但是堆中字符串的值不會復制(有點像淺拷貝),s1的所有權將會直接被遞交給s2,同時s1會直接失效,這時我們說值的所有權發生了移動(Move)。這樣做的目的是避免兩個字符串離開作用域時調用兩次drop函數,從而導致嚴重的Double Free錯誤。

let s2 = s1;			// value moved here
println!("{}", s1);		// 編譯直接報錯

請添加圖片描述

3.2 克隆 Clone

對于上面的s1s2的例子,如果想同時拷貝棧和堆中的信息,可以使用clone()方法。這樣的操作明顯是比較浪費資源的。

在這里插入圖片描述

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

3.3 復制 Copy

總之,如果一個變量存在Copy trait,那么舊變量在“移動”后依然可用;如果一個類型或者該類型的一部分實現了Drop triait(例如定義的元組的一部分是String的情況),那么Rust就不允許它再實現Copy trait了,編譯時就會進行檢查,在移動后舊變量就不再可用,除非使用了clone()方法。

4 函數與所有權

Rust中的變量總是遵循下面的規則:

  • 把一個變量賦值給其他變量就會發生移動(除非變量存在Copy trait);
  • 當變量超出其作用域后,存儲在Heap上的數據就會被銷毀(Drop trait),除非它的所有權已經被轉移。

4.1 參數傳遞時的所有權轉移

在Rust中,如果函數參數的類型是一個實現了Drop trait的類型(例如String類型),把值傳遞給函數中往往伴隨著所有權的轉移,也就是說舊變量對值的所有權會發生丟失,這里發生的事情和把變量賦值給另一個變量是類似的。看下面這個例子:

fn main() {let s1 = String::from("hello");take_ownership(s1);// println!("{}", s1);      // 編譯報錯let x = 1;makes_copy(x);println!("the x is {}", x);
}fn take_ownership(some_string: String) {println!("{}", some_string);
}fn makes_copy(some_integer: i32) {println!("{}", some_integer);
}

對于String這種類型的變量,直接將其作為函數參數時,傳入參數時helloString的所有權會從s1轉換到函數內部的some_string,程序運行到take_ownership函數之外時會自動調用Drop trait,字符串的值的內存會被釋放。但是對于實現了Copy trait的類型,例如i32,參數傳遞時會發生copy,而不是move,這樣在函數調用后x變量依然是可用的。

4.2 函數返回時的所有權轉移

這個比較好理解,看下面一個例子:

fn main() {let s1 = gives_ownership();let s2 = String::from("hello");let s3 = takes_and_gives_back(s2);
}fn gives_ownership() -> String {let s = String::from("hello");s
}fn takes_and_gives_back(a_string: String) -> String {a_string
}

對于gives_ownership函數,在函數內部創建了一個新的String,函數返回時不會將其銷毀,而是把它的所有權交給主函數的s1;而takes_and_gives_back函數獲取到s2到的所有權,s2之后會失效,返回時將String的所有權交還給主函數的s3

5 引用和借用

但有些時候,我們只想獲得變量的值,而不想它的所有權發生轉移(甚至丟失),這時候就可以使用引用(Reference)。

fn main() {let s1 = String::from("hello");let lenth = calculate_length(&s1);println!("The length of '{}' is {}.", s1, lenth);
}fn calculate_length(s: &String) -> usize {s.len()
}

在上面的例子中,calculate_length函數使用了String的引用作為參數,函數計算返回字符串長度后s1仍然是可用的。引用相當于一個指針,它可以獲取到變量對應的值,但是不擁有它,所以當其離開作用域時也無法銷毀它。像這樣,把引用作為函數參數這個行為稱為借用(Borrow)
在這里插入圖片描述

在Rust中,引用和變量類似,也分為可變的引用和不可變的引用,創建的引用默認同樣是不可變的。下面是一個使用可變引用的例子。

fn main() {let mut s1 = String::from("hello");let lenth = calculate_length(&mut s1);println!("The length of '{}' is {}.", s1, lenth);
}fn calculate_length(s: &mut String) -> usize {s.push_str(", world!");s.len()
}

需要注意引用的特殊限制:在特定的作用域內,一個變量只能同時擁有一個可變的引用;并且不能同時存在可變的引用和不可變的引用。一個變量可以擁有多個不可變的引用。Rust從編譯層面解決了數據競爭的問題。

let mut s = String::from("hello");let s1 = &mut s;
let s2 = &mut s;	// 非法
let mut s = String::from("hello");
{let s1 = &mut s;
}
let s2 = &mut s;	// 合法

這樣的做法還帶來了另一個好處,即永遠不會存在“懸空引用”(Dangling Reference,一個引用或者指針指向一塊內存,但是這一塊內存可能已經被釋放或者被其他人使用了)或者“野指針”。

總之,引用一定滿足下面的規則

  • 引用一定有效;
  • 引用一定滿足下列條件之一,不可能同時滿足:
    • 存在一個可變引用;
    • 存在任意數量的不可變引用。

6 切片

切片(Slice)是指一段數據的引用。這里的一段數據可以是String類型,也可以是數組。字符串切片的寫法如下所示,類型名在程序中是&str

let s = String::from("hello world");let hello = &s[0..5];	// 左閉右開,此時相當于 &s[..5]
let world = &s[6..11]	// 此時相當于 &s[6..]let whole = &s[..]		// 整個字符串的切片

在這里插入圖片描述

需要注意,字符串切片的索引必須發生在有效的UTF-8字符邊界內(就是不能把字符切“壞”了),否則程序就會報錯退出。

為什么要使用切片?看下面這個例子:獲取字符串中的各個單詞,如果字符串中沒有空格,則返回整個字符串。

fn main() {let s = String::from("hello");let word_index = first_word(&s);println!("{}", word_index);
}fn first_word(s: &String) -> usize {let bytes = s.as_bytes();   // 將String轉換為字符數組for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return i;}}s.len()
}

上面這個程序雖然能完成一部分功能(獲取第一個空格的位置),但是這個程序存在一個重要的結構性缺陷:變量word_index和Strings之間沒有任何聯系,即使s被釋放,或者被修改,word_index也無法感知。

使用字符串切片重寫上面的例子:

fn main() {let s = String::from("hello world");let word = first_word(&s);      // 把s作為不可變的引用發生借用,之后s都不可變// s.clear();       // s不可變println!("{}", word);
}fn first_word(s: &String) -> &str {let bytes = s.as_bytes();   // 將String轉換為字符數組for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

請添加圖片描述

字符串子面值也是切片。利用這一特點,我們可以將函數的參數類型改為字符串切片&str,使得函數可以直接接收字符串子面值作為參數,這樣函數就可以同時接收String和字符串切片兩種類型的變量作為參數了

fn main() {let word = first_word("hello world");      println!("{}", word);
}fn first_word(s: &str) -> &str {let bytes = s.as_bytes();   // 將String轉換為字符數組for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

其他數組類型也存在切片,例如使用下面的方法創建一個i32類型的切片,程序中用&[i32]表示該類型。

let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice = &a[1..3];		// slice類型是&[i32]

??原創筆記,碼字不易,歡迎點贊,收藏~ 如有謬誤敬請在評論區不吝告知,感激不盡!博主將持續更新有關嵌入式開發、FPGA方面的學習筆記。


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

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

相關文章

Redis 知識點與應用場景

1. Redis 簡介與核心特性Redis(Remote Dictionary Server)是一款開源的內存數據存儲系統,支持多種數據結構,兼具高性能、持久化、分布式等特性,廣泛用于緩存、數據庫、消息中間件等場景。其核心特性包括:高…

日常反思總結

1.group by和order by的區別

易貝 (eBay (eBay) 關鍵字搜索 API 實戰:從認證到商品列表獲取全流程解析

在跨境電商開發領域,eBay 作為全球最大的在線交易平臺之一,其開放 API 為開發者提供了豐富的商品數據獲取能力。本文將聚焦 eBay 關鍵字搜索商品列表接口的實現,涵蓋 OAuth2.0 認證、高級搜索參數配置、分頁策略及完整代碼實現,幫…

敏捷數據開發實踐:基于 Amazon Q Developer + Remote MCP 構建本地與云端 Amazon Redshift 交互體系

敏捷數據開發實踐:基于 Amazon Q Developer Remote MCP 構建本地與云端 Amazon Redshift 交互體系 新用戶可獲得高達 200 美元的服務抵扣金 亞馬遜云科技新用戶可以免費使用亞馬遜云科技免費套餐(Amazon Free Tier)。注冊即可獲得 100 美元的…

【SpringBoot】11 概念理解 - 深入理解 Java 和 Spring 中的容器、組件、類、對象與 Bean

文章目錄引言1. 基本概念解析1.1 類(Class)1.2 對象(Object)1.3 組件(Component)1.4 Bean 實例(Bean Instance)1.5 容器(Container)2. 運行時 vs. 非運行時的…

【學習嵌入式day-25-線程】

exec函數族exec函數族利用進程空間執行另一份代碼#include "../head.h"int main(void) {char *parg[5] {"./hello","how","are","you",NULL,};printf("execl-up\n");//execl("./hello", "./hello…

Rust 中 Box 的深度解析:作用、原理與最佳實踐

Rust 中 Box 的深度解析:作用、原理與最佳實踐 Box 是 Rust 中最基礎且最重要的智能指針類型,它在 Rust 的內存管理和所有權系統中扮演著核心角色。以下是關于 Box 的全面解析: Box 的核心作用 #mermaid-svg-m6liFZlmqOHRfIZB {font-family:&…

【測試用例】

需求背景部分金融/政企等行業客戶,企業內部安全要求較高,且因為某些原因未接入 sso 登錄,會要求 MG 提供較為復雜的密碼規則甚至提供強更機制;且每個客戶的安全要求不一樣目前 MG 線上密碼規則: 8 位以上,包…

Klipper-probe模塊

配置信息[probe] pin: !PD4 x_offset: 0 y_offset: 0 z_offset: -0.20 #the distance between nozzle and level switch speed: 10 samples: 2 #probe one point three times get an average samples_result: average sample_retract_dist: 5 samples_tolerance: 0.05 # …

Excel多級數據結構導入導出工具

Excel多級數據結構導入導出工具 這是一個功能強大的Excel導入導出工具庫,專門用于處理復雜的多級嵌套數據結構。通過自定義注解配置,可以輕松實現Java對象與Excel文件之間的雙向轉換。 核心功能特性 1. 多級數據結構支持 嵌套對象處理: 支持任意層級的對…

基于UniApp的新大陸物聯網平臺溫濕度檢測系統開發方案

新大陸物聯網平臺對接要點 認證方式: 使用AccessToken進行API認證 Token存儲在本地緩存中 數據格式: 溫度數據單位:攝氏度(C) 濕度數據單位:百分比(%) 時間格式:ISO 8601或時間戳 設備狀態: online:…

Git、JSON、MQTT

GIT簡介:Git是什么?Git是目前世界上最先進的分布式版本控制系統作用:版本控制(版本的備份--->版本的回溯和前進)多人協作優勢:SVN(集中式)劣勢:過度依賴服務器和網絡,容災性差Git…

yolo目標檢測技術之yolov11項目實戰(三)

yolo目標檢測技術之yolov11項目實戰(三) 文章目錄yolo目標檢測技術之yolov11項目實戰(三)一、 基于 YOLO11 的火焰與煙霧檢測系統(實戰代碼)項目目標環境搭建創建虛擬環境安裝依賴1.1 數據集準備1. 下載地址…

CF思維小訓練(二)

清晰的繽紛的都可以 臟兮兮的甜的也都有轉機 不想太小心 錯過第一百零一場美麗 CF思維小訓練(二) 書接上回CF思維小訓練-CSDN博客 雖然代碼很短,都是每一道題的背后都思維滿滿; 目錄CF思維小訓練(二)Arbo…

分布式鎖:從理論到實戰的深度指南

1. 分布式鎖是啥?為什么它比單機鎖更“硬核”?分布式鎖,聽起來高大上,其實核心問題很簡單:在多個機器、進程或服務同時搶奪資源時,怎么保證不打架? 想象一下,你在雙十一搶購限量款球…

基于UniApp的智能在線客服系統前端設計與實現

了解更多,搜索“程序員老狼”一、引言在當今數字化時代,客戶服務已成為企業競爭力的重要組成部分。本文將詳細介紹一款基于UniApp框架開發的跨平臺智能客服系統前端實現方案,該系統不僅具備傳統客服功能,還融入了現代即時通訊和人…

react與vue的對比,來實現標簽內部類似v-for循環,v-if等功能

前言:在vue中我們提供了很多標簽方法,比如用的比較多的v-for循環內容,v-if/v-show等判斷,可以直接寫在標簽中,大大提高了我們的開發效率,那么在react中有沒有類似的方法呢?我們這里來說一說。re…

PCB工藝-四層板制作流程(簡單了解下)

一)流程:四層板的內層芯板,是由一張雙面覆銅板PP*2銅箔*2覆銅板蝕刻好線路,就是我們的芯板了PP全名叫半固化片,主體是玻璃纖維布環氧樹脂,是絕緣介質銅箔片,是單獨一張銅箔,很薄&…

無人機三維路徑規劃

文章目錄 1、引言 2、背景知識 3、核心算法 4、挑戰與優化 5、初始效果 6、需要改進地方 7、水平方向優化路線 8、垂直方向優化路線 9、與經過路線相交的網格都繪制出來 1、引言 介紹三維路徑規劃的定義和重要性:在無人機、機器人導航、虛擬現實等領域的應用。 概述文章目標和…

Spring-解決項目依賴異常問題

一.檢查項目的Maven路徑是否正確在確保新項目中的依賴在自己的電腦中已經存在的情況下:可以檢查項目的Maven路徑是否正確在拿到一個新項目時,要檢查這個項目的Maven路徑是自己電腦上設置好的Maven路徑嗎?如果不是,項目依賴會出問題…