【Rust】所有權

目錄

  • 所有權
    • 基本概念
      • 所有權介紹
      • 棧與堆
      • 變量作用域
    • 字符串
      • 字符串字面值(&str)
      • String 類型
      • 相互轉換
      • 所有權 + 內存結構對比
      • 注意事項和常見坑
      • 使用場景
    • 內存與分配
      • 變量與數據交互的方式(一):移動
      • 變量與數據交互的方式(二):克隆
    • 所有權與函數
    • 引用
      • 基本使用
      • 可變引用
      • 懸垂引用
      • Slice 類型

所有權

所有權(系統)是 Rust 最為與眾不同的特性,對語言的其他部分有著深刻含義。它讓 Rust 無需垃圾回收(garbage collector)即可保障內存安全,因此理解 Rust 中所有權如何工作是十分重要的。

基本概念

所有權介紹

所有權ownership)是 Rust 用于如何管理內存的一組規則。所有程序都必須管理其運行時使用計算機內存的方式。一些語言中具有垃圾回收機制,在程序運行時有規律地尋找不再使用的內存;在另一些語言中,程序員必須親自分配和釋放內存。Rust 則選擇了第三種方式:通過所有權系統管理內存,編譯器在編譯時會根據一系列的規則進行檢查。如果違反了任何這些規則,程序都不能編譯。在運行時,所有權系統的任何功能都不會減慢程序。

所有權規則

  1. Rust 中的每一個值都有一個 所有者(owner)。
  2. 值在任一時刻有且只有一個所有者。
  3. 當所有者(變量)離開作用域,這個值將被丟棄。

棧與堆

在 Rust 中,**棧(Stack)堆(Heap)**是內存管理的重要概念,理解它們對于掌握 Rust 的所有權(Ownership)、借用(Borrowing)和生命周期(Lifetime)機制非常關鍵。

棧(Stack):

  • 特點:

    • 后進先出(LIFO):像疊盤子,最后放上去的最先被取出。
    • 分配速度快:因為只需要移動一個指針。
    • 存儲固定大小的數據:如基本數據類型(i32, bool)、元組((i32, bool))等。
  • 在 Rust 中的表現:

    fn main() {let x = 5;     // x 被分配在棧上let y = true;  // y 也在棧上
    }
    

? 這里的 x 和 y 都是已知大小的基本類型,會直接分配在棧上。

堆(Heap):

  • 特點:

    • 動態分配內存:用于大小在編譯時不確定的值(比如 Vec、String)。
    • 分配慢于棧:需要操作操作系統請求內存。
    • 需要手動釋放(Rust 使用所有權機制自動釋放,無需開發者手動調用 free)。
  • 在 Rust 中的表現:

    fn main() {let s = String::from("hello"); // String 的數據部分存在堆上
    }
    

    這里:s 是一個變量,保存在棧上,里面包含一個指向堆中實際字符串內容的指針。字符串 “hello” 的內容是動態分配的,存在堆上。

棧與堆的關系圖解:

在這里插入圖片描述

總結對比:

特性棧(Stack)堆(Heap)
分配速度非常快較慢
管理方式編譯器自動Rust 自動通過所有權管理
數據大小編譯期已知編譯期未知,運行時決定
訪問速度
示例類型i32, bool, charString, Vec<T>, Box<T>

變量作用域

Rust 中的變量作用域是理解程序生命周期、內存管理和所有權系統的一個重要概念。它決定了變量的有效范圍、生命周期以及如何管理內存。

作用域(Scope)是指變量、函數、結構體等在程序中有效的范圍。Rust 使用靜態分析來確保變量在其作用域結束時被銷毀,并且會在作用域結束時自動回收內存(這是 Rust 所獨有的所有權機制的一部分)。

變量作用域的基礎:

  1. 作用域定義:
    變量通常在代碼塊(block)內定義,代碼塊是由 {} 包圍的區域。只要變量處于該塊內,它就是有效的(也就是“可見”的)。
  2. 生命周期:
    Rust 使用 所有權(ownership) 機制來確保每個變量只有一個有效的“所有者”,并且當該變量超出作用域時會被自動銷毀。
fn main() {let x = 5;  // 變量 x 在這里被聲明并初始化println!("x is {}", x);  // x 在作用域內有效
}  // 這里 x 超出作用域,內存被釋放

作用域規則:

  • 變量 x 的作用域從它被創建的地方(let x = 5)開始,一直到所在的代碼塊結束(在這里是 main 函數的結束)。
  • 當作用域結束時,變量 x 會被自動銷毀,并釋放相關內存。

嵌套作用域:

Rust 允許作用域嵌套,外層作用域的變量可以在內層作用域中使用,但內層作用域結束時,外層作用域的變量不會受到影響。

fn main() {let x = 5;{let y = 10;  // y 的作用域在這個內部作用域內println!("x = {}, y = {}", x, y); // x 和 y 都有效}  // y 超出作用域,被銷毀println!("x = {}", x); // x 仍然有效,因為它在外層作用域,而 y 在內層作用域結束后被銷毀
}

輸出結果如下:

x = 5, y = 10
x = 5

字符串

字符串字面值(&str)

示例:

let s: &str = "hello";// let s = "hello";

特點:

  • &str 是字符串切片類型,表示某段 UTF-8 編碼的字符串引用。
  • 字面值 "hello"靜態存在于程序的只讀數據段 中。
  • "hello" 的類型實際上是 &'static str,它有 'static 生命周期,即整個程序運行期間都有效。

內存結構:

&str = {指針:指向字符串首地址,長度:5(字節數)
}

特性:

特性說明
長度固定不能動態添加字符
不可變不支持修改內容
存儲位置靜態分配的只讀內存或堆中其他 String 的切片
無所有權不能擁有資源,只是“借用”
生命周期相關經常和 &'static str 或函數參數一起使用

String 類型

示例:

let mut s: String = String::from("hello");
// let s = String::from("hello");
s.push_str(" world");

特點:

  • String 是擁有所有權的、可變的、堆分配的 UTF-8 字符串。
  • 支持動態增長和修改,常用于處理來自用戶輸入、文件等動態數據。

內部結構:

String = {指針:指向堆上數據,長度:已用字節數,容量:分配的總字節數
}

特性總結:

特性說明
可變可以添加、替換、刪除內容
擁有所有權當變量離開作用域自動釋放內存
堆分配內容存儲在堆中
靈活適合處理動態、拼接的字符串

相互轉換

&str → String(借用到擁有):

let s1 = "hello"; // &str
let s2 = s1.to_string(); // String
let s3 = String::from(s1); // 等價寫法

String → &str(擁有到借用):

let s1 = String::from("hello");
let s2: &str = &s1; // 自動解引用為 &str

所有權 + 內存結構對比

維度&strString
是否擁有數據否(只借用)是(擁有)
是否可變
存儲位置只讀段或堆(借用)
生命周期有生命周期限制生命周期隨變量作用域自動管理
內部結構指針 + 長度(胖指針)指針 + 長度 + 容量(結構體)

let s = "hello"; 這里 s 中的并不是"hello",而是指針指向 "hello" 的地址值和值的長度,也就是說 s 內部是一個胖指針而不是值的本身,String 也是同理,變量內部是一個結構體而不是值的本身。

注意事項和常見坑

字節邊界問題:

let s = "你好";  // UTF-8 每個漢字占 3 字節
// println!("{}", &s[0..1]); // panic:不是合法 UTF-8 邊界
println!("{}", &s[0..3]); // 輸出:你

String 的按索引訪問:

let s = String::from("hello");
// println!("{}", s[1]); // 編譯錯誤,不能用索引訪問 String
let ch = s.chars().nth(1); // 正確方式

使用場景

場景推薦類型
只讀字面值,固定內容&str
動態構建、修改字符串String
需要擁有字符串(函數返回值)String
接收字符串參數&str (更通用)
拼接多個字符串String

&str 是借用的只讀 UTF-8 字符串切片,適合輕量訪問;String 是擁有堆內存的可變字符串,適合需要修改或管理生命周期的場景。

內存與分配

就字符串字面值來說,在編譯時就知道其內容,所以文本被直接硬編碼進最終的可執行文件中。這使得字符串字面值快速且高效。不過這些特性都只得益于字符串字面值的不可變性。不幸的是,不能為了每一個在編譯時大小未知的文本而將一塊內存放入二進制文件中,并且它的大小還可能隨著程序運行而改變。

對于 String 類型,為了支持一個可變,可增長的文本片段,需要在堆上分配一塊在編譯時未知大小的內存來存放內容。這意味著:

  • 必須在運行時向內存分配器(memory allocator)請求內存。
  • 需要一個當我們處理完 String 時將內存返回給分配器的方法。

第一部分由開發者完成:當調用 String::from 時,它的實現請求其所需的內存。

第二部分,Rust 采取了一個策略:內存在擁有它的變量離開作用域后就被自動釋放。當變量離開作用域,Rust 為我們調用一個特殊的函數,這個函數叫做 drop,Rust 在結尾的 } 處自動調用 drop

變量與數據交互的方式(一):移動

在 Rust 中,多個變量可以采取不同的方式與同一數據進行交互。

let x = 5;
let y = x;

將 5 賦值給 x,然后生成 x 的副本并將其拷貝賦值給 y。現在 xy,都等于 5,因為整數是有已知固定大小的簡單值,所以這兩個 5 被放入了棧中。

類似的過程,放在 String 類型上結果完全不一樣。

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

前面提到過 String 類型的變量內部是一個結構體而不是值的本身。

在這里插入圖片描述

s1 賦值給 s2String 的數據被復制了,這意味著從棧上拷貝了它的指針、長度和容量。我們并沒有復制指針指向的堆上數據。

在這里插入圖片描述

之前提到過當變量離開作用域后,Rust 自動調用 drop 函數并清理變量的堆內存。不過圖中展示了兩個數據指針指向了同一位置。這就有了一個問題:當 s2s1 離開作用域,它們都會嘗試釋放相同的內存。這是一個叫做二次釋放的錯誤,也是之前提到過的內存安全性 bug 之一。兩次釋放(相同)內存會導致內存污染,它可能會導致潛在的安全漏洞。

為了確保內存安全,在 let s2 = s1; 之后,Rust 認為 s1 不再有效,因此 Rust 不需要在 s1離開作用域后清理任何東西。

將拷貝指針、長度和容量而不拷貝數據并使第一個變量無效的操作在 Rust 中稱為移動,有一點類似于其他編程語言中的淺拷貝。這樣就解決了二次釋放的錯誤,因為只有 s2 是有效的,當其離開作用域,它就釋放自己的內存。

Rust 永遠也不會自動創建數據的 “深拷貝”。因此,任何 自動的復制都可以被認為是對運行時性能影響較小的。

變量與數據交互的方式(二):克隆

如果確實需要深度復制 String 中堆上的數據,而不僅僅是棧上的數據,可以使用一個叫做 clone 的通用函數。

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

這就相當于把堆上的值也復制了一份給 s2

在這里插入圖片描述

輸出結果如下:

s1 = hello, s2 = hello

回到一開始講解移動中的代碼

let x = 5;
let y = x;

這段代碼明明沒有用 clone 是如何實現的數據拷貝?

Rust 有一個叫做 Copy trait 的特殊注解,可以用在類似整型這樣的存儲在棧上的類型上。如果一個類型實現了 Copy trait,那么一個舊的變量在將其賦值給其他變量后仍然可用。

Rust 中的 Copy 是一種輕量級、無資源所有權的按位拷貝,它只在棧上發生,不會去復制堆內存或執行任何邏輯代碼(比如 Drop)。

Rust 不允許自身或其任何部分實現了 Drop trait 的類型使用 Copy trait。如下是一些 Copy 的類型:

  • 所有整數類型,比如 u32
  • 布爾類型,bool,它的值是 truefalse
  • 所有浮點數類型,比如 f64
  • 字符類型,char
  • 元組,當且僅當其包含的類型也都實現 Copy 的時候。比如,(i32, i32) 實現了 Copy,但 (i32, String) 就沒有。

所有權與函數

將值傳遞給函數與給變量賦值的原理相似。向函數傳遞值可能會移動或者復制,就像賦值語句一樣。

fn main() {let s = String::from("hello");  // s 進入作用域takes_ownership(s);             // s 的值移動到函數里 ...// ... 所以到這里不再有效println!("{}", s);let x = 5;                      // x 進入作用域makes_copy(x);                  // x 應該移動函數里,// 但 i32 是 Copy 的,// 所以在后面可繼續使用 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 移出作用域。沒有特殊之處

該代碼會產生以下錯誤:

error[E0382]: borrow of moved value: `s`--> src/main.rs:6:20|
2  |     let s = String::from("hello");  // s 進入作用域|         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3  |
4  |     takes_ownership(s);             // s 的值移動到函數里 ...|                     - value moved here
5  |     // ... 所以到這里不再有效
6  |     println!("{}", s);|                    ^ value borrowed here after move|
note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary--> src/main.rs:17:33|
17 | fn takes_ownership(some_string: String) { // some_string 進入作用域|    ---------------              ^^^^^^ this parameter takes ownership of the value|    ||    in this function= 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|
4  |     takes_ownership(s.clone());             // s 的值移動到函數里 ...|                      ++++++++

這是由于 s 傳給 takes_ownership 之后,所有權轉移到 takes_ownership 里,隨著 takes_ownership 的結束一起被釋放了,導致后面無法繼續使用 s

這種情況可以通過函數返回值轉移所有權來解決。

fn main() {let s1 = gives_ownership();         // gives_ownership 將返回值// 轉移給 s1let 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 // 并移出給調用的函數// 
}// takes_and_gives_back 將傳入字符串并返回該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入作用域// a_string  // 返回 a_string 并移出給調用的函數
}

變量的所有權總是遵循相同的模式:將值賦給另一個變量時移動它。當持有堆中數據值的變量離開作用域時,其值將通過 drop 被清理掉,除非數據被移動為另一個變量所有。

雖然這樣是可以的,但是在每一個函數中都獲取所有權并接著返回所有權有些繁瑣。幸運的是,Rust 對此提供了一個不用獲取所有權就可以使用值的功能,叫做引用

引用

基本使用

為了能在調用函數后仍能使用變量,就可以使用引用。引用像一個指針,因為它是一個地址,可以由此訪問儲存于該地址的屬于其他變量的數據。 與指針不同,引用確保指向某個特定類型的有效值。使用符號 & 進行引用。

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

引用的原理圖如下:

在這里插入圖片描述

&s1 語法創建一個 指向s1 的引用,但是并不擁有它。因為并不擁有這個值,所以當引用停止使用時,它所指向的值也不會被丟棄。

同理,函數簽名使用 & 來表明參數 s 的類型是一個引用。

變量 s 有效的作用域與函數參數的作用域一樣,不過當 s 停止使用時并不丟棄引用指向的數據,因為 s 并沒有所有權。當函數使用引用而不是實際值作為參數,無需返回值來交還所有權,因為就不曾擁有所有權。

輸出結果如下:

The length of 'hello' is 5.

小提一下:與使用 & 引用相反的操作是 解引用,它使用解引用運算符,*

將創建一個引用的行為稱為借用。正如現實生活中,如果一個人擁有某樣東西,你可以從他那里借來。當你使用完后,必須還回去。因為你并不擁有它的所有權。

如果嘗試修改借用的變量是行不通的。

fn main() {let s = String::from("hello");change(&s);
}fn change(some_string: &String) {some_string.push_str(", world");
}

會產生一個錯誤:

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference--> src/main.rs:8:5|
8 |     some_string.push_str(", world");|     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable|
help: consider changing this to be a mutable reference|
7 | fn change(some_string: &mut String) {|                         +++

正如變量默認是不可變的,引用也一樣。(默認)不允許修改引用的值。

可變引用

通過一個小調整就能修復上述代碼中的錯誤,允許修改一個借用的值,這就是可變引用

fn main() {let mut s = String::from("hello");change(&mut s);
}fn change(some_string: &mut String) {some_string.push_str(", world");
}

必須將 s 改為 mut。然后在調用 change 函數的地方創建一個可變引用 &mut s,并更新函數簽名以接受一個可變引用 some_string: &mut String。這就非常清楚地表明,change 函數將改變它所借用的值。

可變引用有一個很大的限制:如果你有一個對該變量的可變引用,你就不能再創建對該變量的引用。這些嘗試創建兩個 s 的可變引用的代碼會失敗:

fn main() {let mut s = String::from("hello");let r1 = &mut s;let r2 = &mut s;println!("{}, {}", r1, r2);
}

錯誤如下:

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

這個報錯說這段代碼是無效的,因為不能在同一時間多次將 s 作為可變變量借用。第一個可變的借入在 r1 中,并且必須持續到在 println! 中使用它,但是在那個可變引用的創建和它的使用之間,又嘗試在 r2 中創建另一個可變引用,該引用借用與 r1 相同的數據。

這一限制以一種非常小心謹慎的方式允許可變性,防止同一時間對同一數據存在多個可變引用。這個限制的好處是 Rust 可以在編譯時就避免數據競爭。數據競爭類似于競態條件,它可由這三個行為造成:

  • 兩個或更多指針同時訪問同一數據。
  • 至少有一個指針被用來寫入數據。
  • 沒有同步數據訪問的機制。

數據競爭會導致未定義行為,難以在運行時追蹤,并且難以診斷和修復;Rust 避免了這種情況的發生,因為它甚至不會編譯存在數據競爭的代碼。

一如既往,可以使用大括號來創建一個新的作用域,以允許擁有多個可變引用,只是不能同時擁有:

fn main() {let mut s = String::from("hello");{let r1 = &mut s;} // r1 在這里離開了作用域,所以我們完全可以創建一個新的引用let r2 = &mut s;
}

Rust 在同時使用可變與不可變引用時也采用的類似的規則。這些代碼會導致一個錯誤:

fn main() {let mut s = String::from("hello");let r1 = &s; // 沒問題let r2 = &s; // 沒問題let r3 = &mut s; // 大問題println!("{}, {}, and {}", r1, r2, r3);
}

錯誤如下:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable--> src/main.rs:6:14|
4 |     let r1 = &s; // 沒問題|              -- immutable borrow occurs here
5 |     let r2 = &s; // 沒問題
6 |     let r3 = &mut s; // 大問題|              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);|                                -- immutable borrow later used here

不可變引用的借用者可不希望在借用時值會突然發生改變!然而,多個不可變引用是可以的,因為沒有哪個只能讀取數據的引用者能夠影響其他引用者讀取到的數據。

注意一個引用的作用域從聲明的地方開始一直持續到最后一次使用為止。例如,因為最后一次使用不可變引用(println!),發生在聲明可變引用之前,所以如下代碼是可以編譯的:

fn main() {let mut s = String::from("hello");let r1 = &s; // 沒問題let r2 = &s; // 沒問題println!("{r1} and {r2}");// 此位置之后 r1 和 r2 不再使用let r3 = &mut s; // 沒問題println!("{r3}");
}

不可變引用 r1r2 的作用域在 println! 最后一次使用之后結束,這也是創建可變引用 r3 的地方。因為它們的作用域沒有重疊,所以代碼是可以編譯的。編譯器可以在作用域結束之前判斷不再使用的引用。

懸垂引用

在具有指針的語言中,很容易通過釋放內存時保留指向它的指針而錯誤地生成一個懸垂指針,所謂懸垂指針是其指向的內存可能已經被分配給其它持有者。相比之下,在 Rust 中編譯器確保引用永遠也不會變成懸垂狀態:當你擁有一些數據的引用,編譯器確保數據不會在其引用之前離開作用域。

這里嘗試創建一個懸垂引用,Rust 會通過一個編譯時錯誤來避免:

fn main() {let reference_to_nothing = dangle();
}fn dangle() -> &String {let s = String::from("hello");&s
}

錯誤如下:

error[E0106]: missing lifetime specifier--> src/main.rs:5:16|
5 | fn dangle() -> &String {|                ^ expected named lifetime parameter|= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`|
5 | fn dangle() -> &'static String {|                 +++++++
help: instead, you are more likely to want to return an owned value|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {|

因為 s 是在 dangle 函數內創建的,當 dangle 的代碼執行完畢后,s 將被釋放。當嘗試返回它的引用時,意味著這個引用會指向一個無效的 String,Rust 不會允許這么做。

這里的解決方法是直接返回 String

fn no_dangle() -> String {let s = String::from("hello");s
}
  • 在任意給定時間,要么只能有一個可變引用,要么只能有多個不可變引用。
  • 引用必須總是有效的。

Slice 類型

什么是 Slice 類型?

Slice 是對一塊連續內存區域的借用,它本身不擁有數據,只是引用。比如:數組、字符串,都可以通過切片來引用一部分內容。

一句話理解:Slice = 指向連續元素的一段引用 + 長度信息。

let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4];  // 包括arr[1], arr[2], arr[3],不包括arr[4]

這里的 slice 是一個 &[i32] 類型的切片引用,指向數組的一部分。

為什么 Rust 要引入 Slice 類型(切片)?

  • 為了在不復制數據的情況下,安全、靈活地訪問連續內存的一部分。
  • Slice 是一種只借用部分數據的機制,能零拷貝地、安全地處理數據片段。

Slice 類型有什么優勢?

  1. 避免復制,提高性能
    • 如果沒有 Slice,每次想操作一部分數據(比如一個字符串的一小段),就要復制一份,非常浪費內存和時間。
    • Slice 只借用原數據的一部分,不分配新內存,零拷貝。
  2. 安全訪問內存
    • Slice 包含長度信息,Rust 在訪問時能自動檢查邊界,避免訪問越界、野指針等內存錯誤(C 語言就容易出問題)。
  3. 通用性和靈活性
    • Slice 設計得很通用,不管是 StringVec、數組 [T; N],都可以切成 Slice 來操作。
    • 比如:&[T] 是對數組或向量的借用,&str 是對字符串的借用。
  4. 支持共享只讀和可變借用
    • &[T] 是不可變切片(只讀訪問);
    • &mut [T] 是可變切片(可以修改內容)。
  5. Rust 借用檢查器可以追蹤
    • Slice 是一個標準的借用,符合 Rust 的所有權和生命周期系統,可以被編譯器靜態檢查,保證不會懸垂引用。

Slice 的基本使用:

fn main() {let arr = [10, 20, 30, 40, 50];let slice = &arr[1..4]; // 取arr[1], arr[2], arr[3]println!("slice: {:?}", slice);  // 輸出: slice: [20, 30, 40]for x in slice {println!("{}", x);}
}

slice 只是引用,不占用額外空間。切片里保存了兩個信息:指針 + 長度。

切片語法:

&array[start..end]   // 從start開始,到end前結束(左閉右開區間)

&array[..]:整個數組,&array[start..]:從start到末尾,&array[..end]:從開頭到end前,&array[start..=end]:從start到end,包括end(注意用 ..=)。

let arr = [1, 2, 3, 4, 5];// 從2開始到4前
let a = &arr[1..3];   // [2, 3]// 從0到2
let b = &arr[..2];    // [1, 2]// 從2到最后
let c = &arr[2..];    // [3, 4, 5]// 包括索引2到4
let d = &arr[2..=4];  // [3, 4, 5]

&[T] 是不可變切片,有時需要可變的情況,跟前面將不可變改為可變一樣,加上 mut 關鍵字,例如:

fn main() {let mut arr = [1, 2, 3, 4, 5];// 不可變let slice = &arr[1..4];println!("{:?}", slice);// 可變let slice_mut = &mut arr[1..4];slice_mut[0] = 99;println!("{:?}", arr);  // arr變成了 [1, 99, 3, 4, 5];
}

結果輸出如下:

[2, 3, 4]
[1, 99, 3, 4, 5]

Slice 類型是特殊的引用,所以可變切片不能跟不可變引用同時存在,Rust 的借用檢查器保證這一點。

字符串切片的用法跟數組切片的用法一樣,字符串切片其實就是之前提到過的字符串字面值 &strString&str 是不同的:

  • String:堆上的可變字符串

  • &str:字符串切片,不可變的引用(指向 UTF-8 編碼的一段字節序列)

fn main() {let s = String::from("hello world");let hello = &s[0..5];    // "hello"let world = &s[6..];     // "world"println!("{}, {}", hello, world);
}

可以對字符串進行切片,但要注意是按照字節切,不是按字符數
(如果切到中間的UTF-8字符,就會 panic)。來看下面例子:

fn main() {let s = String::from("你好"); // "你"和"好"在UTF-8下每個字占3個字節!println!("{:?}", s.as_bytes());// 輸出: [228, 189, 160, 229, 165, 189]
}

如果寫成 let slice = &s[0..1]; 這是錯的,只有一個字節,UTF-8 破壞了,程序直接 panic

warning: unused variable: `slice`--> src/main.rs:4:9|
4 |     let slice = &s[0..1];|         ^^^^^ help: if this is intentional, prefix it with an underscore: `_slice`|= note: `#[warn(unused_variables)]` on by defaultwarning: `hello_cargo` (bin "hello_cargo") generated 1 warningFinished `dev` profile [unoptimized + debuginfo] target(s) in 0.00sRunning `target/debug/hello_cargo`thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside '你' (bytes 0..3) of `你好`
stack backtrace:0: rust_begin_unwindat /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:695:51: core::panicking::panic_fmtat /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:75:142: core::str::slice_error_fail_rt3: core::str::slice_error_failat /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/str/mod.rs:68:54: core::str::traits::<impl core::slice::index::SliceIndex<str> for core::ops::range::Range<usize>>::indexat /Users/huangruibang/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/str/traits.rs:240:215: <alloc::string::String as core::ops::index::Index<I>>::indexat /Users/huangruibang/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/string.rs:2669:96: hello_cargo::mainat ./src/main.rs:4:197: core::ops::function::FnOnce::call_onceat /Users/huangruibang/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

正確切法應該是按照字符來切到完整的 3 個字節:

fn main() {let s = String::from("你好"); // "你"和"好"在UTF-8下每個字占3個字節!let slice1 = &s[0..3]; // 切到完整的3個字節,才是"你"let slice2 = &s[3..]; // 切到完整的3個字節,才是"好"println!("{}", slice1);println!("{}", slice2);
}

接下來講講 Slice 類型的底層原理,以便更好地理解:

編譯器的角度看,切片是一個胖指針(fat pointer)

它包含兩部分信息:

  • 一個指針(指向數據的起始地址)
  • 一個長度(告訴你有多少個元素)

可以理解成這樣的結構(簡化版偽代碼):

struct Slice<T> {ptr: *const T, // 指向第一個元素的地址len: usize,    // 有多少個元素
}

所以:&[T] 是對 Slice<T>不可變引用&mut [T] 是對 Slice<T>可變引用

let arr = [10, 20, 30, 40, 50];
let slice = &arr[1..4];

內存大概是這樣(數字是地址舉例):

地址內容
0x100010
0x100420
0x100830
0x100C40
0x101050

然后:slice.ptr 指向 0x1004(也就是20所在位置),slice.len3(指向3個元素:20, 30, 40)。所以 slice 能正確訪問 [20, 30, 40]

切片本身是獨立的小結構體,占據一點棧空間,但它指向的數據是在原數組里。

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

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

相關文章

4月29日日記

終于是考完解析幾何了&#xff0c;今天昨天突擊了一下&#xff0c;感覺確實學會了很多之前不會的東西&#xff0c;但是可能距離高分還差很多。這次考試不太理想。大部分原因是前期沒學&#xff0c;吸取教訓&#xff0c;早點開始復習微積分。明天還有一節微積分&#xff0c;但是…

【深度對比】Google Play與IOS 馬甲包處理差異分析

在移動應用發布與推廣過程中&#xff0c;馬甲包&#xff08;Cloned App / Alternate Version&#xff09; 曾被廣泛用于流量測試、風險隔離、多品牌運營等場景中。隨著 Google Play 與 Apple App Store 審核政策不斷收緊&#xff0c;開發者們越來越關注兩個平臺對“馬甲包”的態…

MCP 架構全解析:Host、Client 與 Server 的協同機制

目錄 &#x1f3d7;? MCP 架構全解析&#xff1a;Host、Client 與 Server 的協同機制 &#x1f4cc; 引言 &#x1f9e9; 核心架構組件 1. Host&#xff08;主機&#xff09; 2. Client&#xff08;客戶端&#xff09; 3. Server&#xff08;服務器&#xff09; &#…

記錄一次無界微前端的簡單使用

記錄一次無界微前端使用 無界微前端主應用子應用nginx配置 無界微前端 https://wujie-micro.github.io/doc/ 因為使用的是vue項目主應用和次應用都是 所以用的封裝的。 https://wujie-micro.github.io/doc/pack/ 主應用 安裝 選擇對應的版本 # vue2 框架 npm i wujie-vue2…

LLM應用于自動駕駛方向相關論文整理(大模型在自動駕駛方向的相關研究)

1、《HILM-D: Towards High-Resolution Understanding in Multimodal Large Language Models for Autonomous Driving》 2023年9月發表的大模型做自動駕駛的論文&#xff0c;來自香港科技大學和人華為諾亞實驗室&#xff08;代碼開源&#xff09;。 論文簡介&#xff1a; 本文…

FTP-網絡文件服務器

部署思路 單純上傳下載ftp系統集成間的共享 samba網絡存儲服務器 NFS 網絡文件服務器&#xff1a;通過網絡共享文件或文件夾&#xff0c;實現數據共享 NAS &#xff08; network append storage):共享的是文件夾 FTP&#xff1a;文件服務器samba&#xff1a;不同系統間的文件…

在 Ubuntu 22.04 x64 系統安裝/卸載 1Panel 面板

一、 1Panel 是什么&#xff1f; 1Panel 是一款基于 Go 語言開發的現代化開源服務器管理面板&#xff08;類似寶塔面板&#xff09;&#xff0c;專注于容器化&#xff08;Docker&#xff09;和云原生環境管理&#xff0c;提供可視化界面簡化服務器運維操作。 1. 1Panel主要功…

Redis | Redis集群模式技術原理介紹

關注&#xff1a;CodingTechWork Redis 集群模式概述 Redis 集群&#xff08;Cluster&#xff09;模式是 Redis 官方提供的分布式解決方案&#xff0c;旨在解決單機 Redis 在數據量和性能上的限制。它通過數據分片、高可用性和自動故障轉移等特性&#xff0c;提供了水平擴展和…

Servlet小結

視頻鏈接&#xff1a;黑馬servlet視頻全套視頻教程&#xff0c;快速入門servlet原理servlet實戰 什么是Servlet&#xff1f; 菜鳥教程&#xff1a;Java Servlet servlet&#xff1a; server applet Servlet是一個運行在Web服務器&#xff08;如Tomcat、Jetty&#xff09;或應用…

數據庫進階之MySQL 程序

1.目標 1> 了解mysqlId服務端程序 2> 掌握mysql客戶端程序的使用 3> 了解工具包中的其他程序 2. MySQL程序簡介 本章介紹 MySQL 命令?程序以及在運?這些程序時指定選項的?般語法(如:mysql -uroot -p)。 對常?程序進?詳細的講解(實用工具的使用方法)&#xf…

VS2022 設置 Qt Project Settings方法

本文解決的問題&#xff1a;創建完成后&#xff0c;如需要用到Sql或者Socket等技術&#xff0c;需要設置Qt Project Settings&#xff1b; 1、打開VS2022編譯器&#xff0c;創建QT項目工程 2、創建完成后&#xff0c;點擊 解決方案 →右鍵屬性 3、選擇 Qt Project Settings →…

React:封裝一個評論回復組件

分析 用戶想要一個能夠顯示評論列表&#xff0c;并且允許用戶進行回復的組件。可能還需要支持多級回復&#xff0c;也就是對回復進行再回復。然后&#xff0c;我要考慮組件的結構和功能。 首先&#xff0c;數據結構方面&#xff0c;評論應該包含id、內容、作者、時間&#xf…

wx讀書某sign算法詳解

未加固 版本&#xff1a;9.2.3 前置知識&#xff1a; (v41 & 0xFFFFFFFFFFFFFFFELL) 是一種高效的奇偶檢查方法&#xff0c;用于判斷數值 v41 是否為奇數。 std::sort<std::lessstd::string,std::string &,std::string>(a1, v6, s); 排序算法 # 完全等價的字…

Django的異步任務隊列管理_Celery

1 基本原理 Celery 是一個異步任務隊列&#xff0c;能夠將耗時操作&#xff08;如發郵件、處理圖片、網絡爬蟲等&#xff09;從 Django 主線程中分離出來&#xff0c;由后臺的 worker 處理&#xff0c;避免阻塞請求。Celery 作為獨立運行的后臺進程&#xff08;Worker&#xf…

【計算機網絡】Linux網絡的幾個常用命令

&#x1f4da; 博主的專欄 &#x1f427; Linux | &#x1f5a5;? C | &#x1f4ca; 數據結構 | &#x1f4a1;C 算法 | &#x1f152; C 語言 | &#x1f310; 計算機網絡 相關文章&#xff1a;計算機網絡專欄 目錄 ping&#xff08;檢測網絡連通性&#xff09;…

全開源、私有化部署!輕量級用戶行為分析系統-ClkLog

ClkLog是一款支持私有化部署的全開源埋點數據采集與分析系統&#xff0c;兼容Web、App、小程序多端埋點&#xff0c;快速洞察用戶訪問路徑、行為軌跡&#xff0c;并生成多維用戶畫像。助力中小團隊搭建輕量靈活的用戶行為分析平臺。 為什么需要一款私有化的埋點分析系統&#x…

golang定時器的精度

以 go1.23.3 linux/amd64 為例。 定時器示例代碼&#xff1a; package mainimport ("context""fmt""time" )var ctx context.Contextfunc main() {timeout : 600 * time.Secondctx, _ context.WithTimeout(context.Background(), timeout)dea…

svn 遠程服務搜索功能

svn服務器沒有遠程搜索功能&#xff0c;靠人工檢索耗時耗力&#xff0c;當服務器文件過多時&#xff0c;全部checkout到本地檢索&#xff0c;耗時太久。 1. TortoiseSVN 安裝注意事項 下載官網地址&#xff1a;https://tortoisesvn.en.softonic.com/download 安裝時選中 co…

uniapp-商城-39-shop 購物車 選好了 進行訂單確認4 配送方式2 地址頁面

上面講基本的樣式和地址信息&#xff0c;但是如果沒有地址就需要添加地址&#xff0c;如果有不同的地址就要選地址。 來看看處理方式&#xff0c; 1、回顧 在delivery-layout中 methods:{goAddress(){uni.navigateTo({url:"/pagesub/pageshop/address/addrlist"})…

Linux命令-iostat

iostat 命令介紹 iostat 是一個用于監控 Linux 系統輸入/輸出設備加載情況的工具。它可以顯示 CPU 的使用情況以及設備和分區的輸入/輸出統計信息&#xff0c;對于診斷系統性能瓶頸&#xff08;如磁盤或網絡活動緩慢&#xff09;特別有用。 語法&#xff1a; iostat [options…