Rust 的結構體提供了一種組織資料的方式,可以根據需求選擇具名欄位、元組或單元結構體。元組結構體常用於建立新型別,例如包裝單一元件以提高型別安全性。單元結構體則不包含欄位,常用於標記或與特徵搭配使用。具名欄位結構體允許自定義欄位名稱和型別,方便資料的組織和存取。此外,Rust 也支援泛型結構體,可以根據需要處理不同型別的資料,提高程式碼的彈性和重用性。生命週期引數的引入則確保了結構體中參考的安全性,避免懸垂指標等問題。而內部可變性機制,例如 Cell 和 RefCell,則允許在保持結構體外部不可變性的前提下,修改其內部資料,兼顧了安全性和靈活性。
結構體的型別與應用
在 Rust 程式語言中,結構體(struct)是一種自定義的資料型別,用於將多個相關的值組合成一個單元。結構體可以根據不同的需求設計成不同的形式,包括具名欄位結構體、元組結構體和單元結構體。
元組結構體的應用
元組結構體是一種特殊的結構體,它類別似於元組,但具有命名的型別。這種結構體特別適用於建立新型別(newtype),即包含單一元件的結構體,用於獲得更嚴格的型別檢查。例如,在處理純 ASCII 文字時,可以定義一個新型別,如下所示:
struct Ascii(Vec<u8>);
使用 Ascii 型別來表示 ASCII 字串,比直接傳遞 Vec<u8> 並在註解中說明其用途更為明確和安全。這種新型別有助於 Rust 編譯器捕捉錯誤,例如將其他型別的位元組緩衝區傳遞給預期 ASCII 文字的函式。
程式碼範例:使用新型別進行型別轉換
struct Ascii(Vec<u8>);
impl Ascii {
fn new(data: Vec<u8>) -> Self {
// 假設有驗證邏輯確保 data 是有效的 ASCII
Ascii(data)
}
}
fn process_ascii(ascii: Ascii) {
// 處理 ascii 資料
}
fn main() {
let data = vec![72, 101, 108, 108, 111]; // "Hello" 的 ASCII 碼
let ascii = Ascii::new(data);
process_ascii(ascii);
}
內容解密:
- 定義了一個名為
Ascii的新型別,它包裝了一個Vec<u8>。 - 實作了
Ascii的new方法,用於建立新的Ascii例項,並假設進行了驗證以確保資料是有效的 ASCII。 process_ascii函式接受一個Ascii引數,確保只有有效的 ASCII 資料才能被處理。- 在
main函式中,建立了一個Ascii例項並傳遞給process_ascii函式。
單元結構體的特點
單元結構體是一種不包含任何欄位的結構體,它在記憶體中不佔用空間。單元結構體的值可以用於實作特定的邏輯或與特徵(trait)一起使用。例如:
struct Onesuch;
這種結構體的值不佔用記憶體空間,Rust 不會為其生成實際的儲存或操作程式碼,因為它的型別已經足以提供所有必要的資訊。
程式碼範例:單元結構體的使用
struct Marker;
fn main() {
let _marker = Marker;
// 可以利用 Marker 實作特定的邏輯或標記功能
}
內容解密:
- 定義了一個名為
Marker的單元結構體。 - 在
main函式中建立了一個Marker的例項。 - 可以利用
Marker這種單元結構體來實作特定的標記或邏輯功能。
結構體的記憶體佈局
Rust 中的具名欄位結構體和元組結構體在記憶體中的表示是相似的,都是將多個值按照特定的方式排列在記憶體中。例如,對於以下結構體:
struct GrayscaleMap {
pixels: Vec<u8>,
size: (usize, usize)
}
GrayscaleMap 的值在記憶體中的佈局取決於 Rust 編譯器的實作,但 Rust 保證欄位的值直接儲存在結構體的記憶體區塊中。
GrayscaleMap 結構示意圖
此圖示展示了 GrayscaleMap 結構在記憶體中的佈局。
內容解密:
GrayscaleMap結構包含兩個欄位:pixels和size。pixels是一個向量,擁有堆積分配的緩衝區。- 圖表展示了
GrayscaleMap的記憶體佈局,其中pixels和size直接儲存在GrayscaleMap的記憶體區塊中。
使用 impl 定義方法
在 Rust 中,可以為自定義的結構體型別定義方法。方法是與特定型別相關聯的函式,它們在 impl 區塊中定義。例如:
struct Queue {
older: Vec<char>,
younger: Vec<char>
}
impl Queue {
fn push(&mut self, c: char) {
self.younger.push(c);
}
fn pop(&mut self) -> Option<char> {
// pop 的實作邏輯
}
}
這裡定義了一個 Queue 結構體,並為其實作了 push 和 pop 方法。
程式碼範例:Queue 的使用
fn main() {
let mut q = Queue { older: Vec::new(), younger: Vec::new() };
q.push('0');
// 可以繼續呼叫 q 的方法進行操作
}
內容解密:
- 定義了一個名為
Queue的結構體,包含兩個向量欄位。 - 為
Queue實作了push和pop方法,用於操作佇列。 - 在
main函式中建立了一個Queue例項並呼叫了其push方法。
Rust 中的方法定義與泛型結構體
在 Rust 中,方法是用於定義與特定型別相關聯的函式。這些方法可以對型別的例項進行操作,並且可以存取和修改例項的資料。Rust 中的方法定義使用 impl 關鍵字。
方法定義
Rust 中的方法可以分為三種型別,分別是:
- 取得
self的所有權:這種方法會取得self的所有權,並且在方法呼叫後,self將不再有效。 - 可變借用
self:這種方法會借用self的可變參照,允許方法修改self的資料。 - 不可變借用
self:這種方法會借用self的不可變參照,不允許方法修改self的資料。
內容解密:
- 方法定義使用
impl關鍵字,並且與特定的型別相關聯。 - 方法可以根據需要取得
self的所有權,或是借用self的可變或不可變參照。
例子:Queue 結構體的方法
impl Queue {
pub fn is_empty(&self) -> bool {
self.older.is_empty() && self.younger.is_empty()
}
}
內容解密:
is_empty方法借用了self的不可變參照,因此可以存取self的資料,但不能修改它。- 方法傳回一個布林值,指示 Queue 是否為空。
泛型結構體
Rust 中的結構體可以是泛型的,這意味著它們的定義是一個範本,可以用於多種不同的型別。
例子:泛型 Queue 結構體
pub struct Queue<T> {
older: Vec<T>,
younger: Vec<T>
}
內容解密:
Queue<T>是一個泛型結構體,可以用於儲存任意型別的資料。T是一個型別引數,可以被替換為任意型別。
泛型結構體的方法
impl<T> Queue<T> {
pub fn new() -> Queue<T> {
Queue { older: Vec::new(), younger: Vec::new() }
}
pub fn push(&mut self, t: T) {
self.younger.push(t);
}
pub fn is_empty(&self) -> bool {
self.older.is_empty() && self.younger.is_empty()
}
}
內容解密:
new方法建立一個新的、空的 Queue。push方法將一個新的元素新增到 Queue 中。is_empty方法檢查 Queue 是否為空。
使用靜態方法
靜態方法是與型別本身相關聯的方法,而不是與型別的例項相關聯。靜態方法通常用作建構函式。
例子:Queue 的靜態方法
impl Queue {
pub fn new() -> Queue {
Queue { older: Vec::new(), younger: Vec::new() }
}
}
內容解密:
new是一個靜態方法,用於建立一個新的 Queue 例項。- 呼叫靜態方法使用
Queue::new()的語法。
分離方法和資料的優點
Rust 將方法和資料分開定義有幾個優點:
- 更容易找到型別的資料成員。
- 統一的語法適用於所有型別的結構體,包括 tuple-like 和 unit-like 結構體。
- 同樣的
impl語法也可用於實作 trait。
泛型結構體與生命週期引數
在 Rust 中,結構體(struct)可以是泛型的,這意味著它們可以持有任意型別的資料。泛型結構體的定義與普通結構體類別似,但需要在結構體名稱後面加上型別引數。
泛型結構體
考慮一個簡單的佇列(Queue)結構體,它可以持有任意型別的元素。我們可以這樣定義它:
pub struct Queue<T> {
older: Vec<T>,
younger: Vec<T>,
}
在這個定義中,T 是一個型別引數,它代表佇列中元素的型別。由於 T 是泛型的,Queue 結構體可以持有任何型別的元素,如 i32、String 等。
靜態方法與型別推斷
對於泛型結構體,我們可以定義靜態方法,如 new 方法,用於建立新的例項:
impl<T> Queue<T> {
pub fn new() -> Self {
Queue { older: Vec::new(), younger: Vec::new() }
}
}
在這個例子中,Rust 的型別推斷機制允許我們省略 Queue 結構體例項化時的型別引數,只要 Rust 能夠從上下文中推斷出型別即可。例如:
let mut q = Queue::new();
q.push("CAD");
在這個例子中,Rust 推斷出 q 的型別是 Queue<&str>。
生命週期引數
當結構體持有參考(reference)時,我們需要指定這些參考的生命週期。考慮以下例子:
struct Extrema<'elt> {
greatest: &'elt i32,
least: &'elt i32,
}
在這個例子中,'elt 是一個生命週期引數,它代表 greatest 和 least 參考的生命週期。
生命週期引數的推斷
Rust 允許我們在某些情況下省略生命週期引數。例如,在以下函式中:
fn find_extrema(slice: &[i32]) -> Extrema {
// ...
}
Rust 推斷出傳回的 Extrema 例項的生命週期與輸入 slice 的生命週期相同。
自動派生常見特徵
Rust 允許我們自動為結構體派生某些常見的特徵(trait),如 Copy、Clone、Debug 和 PartialEq。這可以透過在結構體定義上新增 #[derive] 屬性來實作:
#[derive(Copy, Clone, Debug, PartialEq)]
struct Point {
x: f64,
y: f64,
}
這使得我們的 Point 結構體支援複製、克隆、除錯輸出和相等比較。
內部可變性
有時,我們希望結構體的某些欄位是可變的,即使結構體本身是不可變的。這可以透過使用內部可變性(interior mutability)來實作。
內部可變性的例子
考慮以下例子:
pub struct SpiderRobot {
species: String,
web_enabled: bool,
leg_devices: [fd::FileDesc; 8],
// ...
}
在這個例子中,SpiderRobot 結構體包含了一些不可變的欄位,但可能需要內部可變性來實作某些功能。
內容解密:
- 泛型結構體:定義結構體時使用型別引數,使其能夠持有任意型別的資料。
- 靜態方法:為泛型結構體定義靜態方法,如
new,用於建立新的例項。 - 生命週期引數:當結構體持有參考時,需要指定這些參考的生命週期,以確保記憶體安全。
- 自動派生特徵:使用
#[derive]屬性自動為結構體派生常見的特徵,如Copy和Debug。 - 內部可變性:允許結構體的某些欄位在結構體本身不可變的情況下仍然是可變的。
圖表示例
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Rust 結構體型別與應用
package "安全架構" {
package "網路安全" {
component [防火牆] as firewall
component [WAF] as waf
component [DDoS 防護] as ddos
}
package "身份認證" {
component [OAuth 2.0] as oauth
component [JWT Token] as jwt
component [MFA] as mfa
}
package "資料安全" {
component [加密傳輸 TLS] as tls
component [資料加密] as encrypt
component [金鑰管理] as kms
}
package "監控審計" {
component [日誌收集] as log
component [威脅偵測] as threat
component [合規審計] as audit
}
}
firewall --> waf : 過濾流量
waf --> oauth : 驗證身份
oauth --> jwt : 簽發憑證
jwt --> tls : 加密傳輸
tls --> encrypt : 資料保護
log --> threat : 異常分析
threat --> audit : 報告生成
@enduml此圖示說明瞭泛型結構體的不同方面之間的關係,包括靜態方法、生命週期引數和自動派生特徵等。
內部可變性:突破 Rust 不可變性的限制
在 Rust 程式設計中,我們經常遇到需要在不可變的結構中包含可變資料的情況。這種需求在處理複雜資料結構或需要在多個地方分享資料時尤為常見。Rust 提供了 Cell 和 RefCell 兩種型別來實作內部可變性(Interior Mutability),讓我們能夠在不可變的外部結構中存取或修改內部資料。
為什麼需要內部可變性?
考慮一個機器人控制系統的例子,其中 SpiderRobot 結構體包含了多個子系統,如感測器、運動控制等。這些子系統都需要存取 SpiderRobot 的分享資料,但 Rust 的借用規則要求在同一時間內,分享資料要麼被多個不可變借用,要麼被一個可變借用。這就導致了在某些情況下,我們無法直接修改分享資料中的某些部分。
使用 Cell 實作簡單的內部可變性
Cell 提供了一種簡單的方式來實作內部可變性。它允許你在不可變的 Cell 中存取和修改內部的值,但有以下限制:
Cell只能用於實作了Copy特徵的型別,因為它透過複製值來提供內部可變性。- 使用
Cell::new(value)建立一個新的Cell。 - 使用
cell.get()取得內部值的副本。 - 使用
cell.set(value)設定內部的新值。
舉個例子,如果我們想在 SpiderRobot 中加入一個簡單的錯誤計數器,可以使用 Cell<u32>:
use std::cell::Cell;
pub struct SpiderRobot {
hardware_error_count: Cell<u32>,
// 其他欄位...
}
impl SpiderRobot {
pub fn add_hardware_error(&self) {
let n = self.hardware_error_count.get();
self.hardware_error_count.set(n + 1);
}
pub fn has_hardware_errors(&self) -> bool {
self.hardware_error_count.get() > 0
}
}
使用 RefCell 實作更靈活的內部可變性
當需要對非 Copy 型別進行可變操作時,RefCell 成為更好的選擇。它提供了動態借用檢查,允許在執行時檢查借用規則是否被違反。
- 使用
RefCell::new(value)建立一個新的RefCell。 - 使用
ref_cell.borrow()取得對內部值的不可變參照。 - 使用
ref_cell.borrow_mut()取得對內部值的可變參照。
如果嘗試同時進行不可變和可變借用,程式將在執行時 panic。
例如,為了在 SpiderRobot 中加入日誌功能,我們可以使用 RefCell<File>:
use std::cell::RefCell;
use std::fs::File;
use std::io::Write;
pub struct SpiderRobot {
log_file: RefCell<File>,
// 其他欄位...
}
impl SpiderRobot {
pub fn log(&self, message: &str) {
let mut file = self.log_file.borrow_mut();
writeln!(file, "{}", message).unwrap();
}
}
內容解密:
RefCell<File>:使用RefCell包裝File,使得即使在不可變的SpiderRobot例項中,也能獲得對File的可變參照。log_file.borrow_mut():動態地借用log_file的可變參照。如果已經有其他借用(不可變或可變),這將導致 panic。writeln!:將訊息寫入檔案。這裡假設File已正確開啟並準備好寫入。