目錄
- 所有權
- 基本概念
- 所有權介紹
- 棧與堆
- 變量作用域
- 字符串
- 字符串字面值(&str)
- String 類型
- 相互轉換
- 所有權 + 內存結構對比
- 注意事項和常見坑
- 使用場景
- 內存與分配
- 變量與數據交互的方式(一):移動
- 變量與數據交互的方式(二):克隆
- 所有權與函數
- 引用
- 基本使用
- 可變引用
- 懸垂引用
- Slice 類型
所有權
所有權(系統)是 Rust 最為與眾不同的特性,對語言的其他部分有著深刻含義。它讓 Rust 無需垃圾回收(garbage collector)即可保障內存安全,因此理解 Rust 中所有權如何工作是十分重要的。
基本概念
所有權介紹
所有權(ownership)是 Rust 用于如何管理內存的一組規則。所有程序都必須管理其運行時使用計算機內存的方式。一些語言中具有垃圾回收機制,在程序運行時有規律地尋找不再使用的內存;在另一些語言中,程序員必須親自分配和釋放內存。Rust 則選擇了第三種方式:通過所有權系統管理內存,編譯器在編譯時會根據一系列的規則進行檢查。如果違反了任何這些規則,程序都不能編譯。在運行時,所有權系統的任何功能都不會減慢程序。
所有權規則:
- Rust 中的每一個值都有一個 所有者(owner)。
- 值在任一時刻有且只有一個所有者。
- 當所有者(變量)離開作用域,這個值將被丟棄。
棧與堆
在 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 , char | String , Vec<T> , Box<T> |
變量作用域
Rust 中的變量作用域是理解程序生命周期、內存管理和所有權系統的一個重要概念。它決定了變量的有效范圍、生命周期以及如何管理內存。
作用域(Scope)是指變量、函數、結構體等在程序中有效的范圍。Rust 使用靜態分析來確保變量在其作用域結束時被銷毀,并且會在作用域結束時自動回收內存(這是 Rust 所獨有的所有權機制的一部分)。
變量作用域的基礎:
- 作用域定義:
變量通常在代碼塊(block)內定義,代碼塊是由{}
包圍的區域。只要變量處于該塊內,它就是有效的(也就是“可見”的)。 - 生命周期:
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
所有權 + 內存結構對比
維度 | &str | String |
---|---|---|
是否擁有數據 | 否(只借用) | 是(擁有) |
是否可變 | 否 | 是 |
存儲位置 | 只讀段或堆(借用) | 堆 |
生命周期 | 有生命周期限制 | 生命周期隨變量作用域自動管理 |
內部結構 | 指針 + 長度(胖指針) | 指針 + 長度 + 容量(結構體) |
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
。現在 x
和 y
,都等于 5,因為整數是有已知固定大小的簡單值,所以這兩個 5 被放入了棧中。
類似的過程,放在 String
類型上結果完全不一樣。
let s1 = String::from("hello");
let s2 = s1;
前面提到過 String
類型的變量內部是一個結構體而不是值的本身。
當 s1
賦值給 s2
,String
的數據被復制了,這意味著從棧上拷貝了它的指針、長度和容量。我們并沒有復制指針指向的堆上數據。
之前提到過當變量離開作用域后,Rust 自動調用 drop
函數并清理變量的堆內存。不過圖中展示了兩個數據指針指向了同一位置。這就有了一個問題:當 s2
和 s1
離開作用域,它們都會嘗試釋放相同的內存。這是一個叫做二次釋放的錯誤,也是之前提到過的內存安全性 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
,它的值是true
和false
。 - 所有浮點數類型,比如
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}");
}
不可變引用 r1
和 r2
的作用域在 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 類型有什么優勢?
- 避免復制,提高性能
- 如果沒有 Slice,每次想操作一部分數據(比如一個字符串的一小段),就要復制一份,非常浪費內存和時間。
- Slice 只借用原數據的一部分,不分配新內存,零拷貝。
- 安全訪問內存
- Slice 包含長度信息,Rust 在訪問時能自動檢查邊界,避免訪問越界、野指針等內存錯誤(C 語言就容易出問題)。
- 通用性和靈活性
- Slice 設計得很通用,不管是
String
、Vec
、數組[T; N]
,都可以切成 Slice 來操作。 - 比如:
&[T]
是對數組或向量的借用,&str
是對字符串的借用。
- Slice 設計得很通用,不管是
- 支持共享只讀和可變借用
&[T]
是不可變切片(只讀訪問);&mut [T]
是可變切片(可以修改內容)。
- 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 的借用檢查器保證這一點。
字符串切片的用法跟數組切片的用法一樣,字符串切片其實就是之前提到過的字符串字面值 &str
,String
和 &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];
內存大概是這樣(數字是地址舉例):
地址 | 內容 |
---|---|
0x1000 | 10 |
0x1004 | 20 |
0x1008 | 30 |
0x100C | 40 |
0x1010 | 50 |
然后:slice.ptr
指向 0x1004
(也就是20所在位置),slice.len
是 3
(指向3個元素:20, 30, 40)。所以 slice
能正確訪問 [20, 30, 40]
。
切片本身是獨立的小結構體,占據一點棧空間,但它指向的數據是在原數組里。