Rust 的結構體提供了一種組織資料的方式,可以根據需求選擇具名欄位、元組或單元結構體。元組結構體常用於建立新型別,例如包裝單一元件以提高型別安全性。單元結構體則不包含欄位,常用於標記或與特徵搭配使用。具名欄位結構體允許自定義欄位名稱和型別,方便資料的組織和存取。此外,Rust 也支援泛型結構體,可以根據需要處理不同型別的資料,提高程式碼的彈性和重用性。生命週期引數的引入則確保了結構體中參考的安全性,避免懸垂指標等問題。而內部可變性機制,例如 CellRefCell,則允許在保持結構體外部不可變性的前提下,修改其內部資料,兼顧了安全性和靈活性。

結構體的型別與應用

在 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);
}

內容解密:

  1. 定義了一個名為 Ascii 的新型別,它包裝了一個 Vec<u8>
  2. 實作了 Asciinew 方法,用於建立新的 Ascii 例項,並假設進行了驗證以確保資料是有效的 ASCII。
  3. process_ascii 函式接受一個 Ascii 引數,確保只有有效的 ASCII 資料才能被處理。
  4. main 函式中,建立了一個 Ascii 例項並傳遞給 process_ascii 函式。

單元結構體的特點

單元結構體是一種不包含任何欄位的結構體,它在記憶體中不佔用空間。單元結構體的值可以用於實作特定的邏輯或與特徵(trait)一起使用。例如:

struct Onesuch;

這種結構體的值不佔用記憶體空間,Rust 不會為其生成實際的儲存或操作程式碼,因為它的型別已經足以提供所有必要的資訊。

程式碼範例:單元結構體的使用

struct Marker;

fn main() {
    let _marker = Marker;
    // 可以利用 Marker 實作特定的邏輯或標記功能
}

內容解密:

  1. 定義了一個名為 Marker 的單元結構體。
  2. main 函式中建立了一個 Marker 的例項。
  3. 可以利用 Marker 這種單元結構體來實作特定的標記或邏輯功能。

結構體的記憶體佈局

Rust 中的具名欄位結構體和元組結構體在記憶體中的表示是相似的,都是將多個值按照特定的方式排列在記憶體中。例如,對於以下結構體:

struct GrayscaleMap {
    pixels: Vec<u8>,
    size: (usize, usize)
}

GrayscaleMap 的值在記憶體中的佈局取決於 Rust 編譯器的實作,但 Rust 保證欄位的值直接儲存在結構體的記憶體區塊中。

GrayscaleMap 結構示意圖

此圖示展示了 GrayscaleMap 結構在記憶體中的佈局。

內容解密:

  1. GrayscaleMap 結構包含兩個欄位:pixelssize
  2. pixels 是一個向量,擁有堆積分配的緩衝區。
  3. 圖表展示了 GrayscaleMap 的記憶體佈局,其中 pixelssize 直接儲存在 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 結構體,並為其實作了 pushpop 方法。

程式碼範例:Queue 的使用

fn main() {
    let mut q = Queue { older: Vec::new(), younger: Vec::new() };
    q.push('0');
    // 可以繼續呼叫 q 的方法進行操作
}

內容解密:

  1. 定義了一個名為 Queue 的結構體,包含兩個向量欄位。
  2. Queue 實作了 pushpop 方法,用於操作佇列。
  3. main 函式中建立了一個 Queue 例項並呼叫了其 push 方法。

Rust 中的方法定義與泛型結構體

在 Rust 中,方法是用於定義與特定型別相關聯的函式。這些方法可以對型別的例項進行操作,並且可以存取和修改例項的資料。Rust 中的方法定義使用 impl 關鍵字。

方法定義

Rust 中的方法可以分為三種型別,分別是:

  1. 取得 self 的所有權:這種方法會取得 self 的所有權,並且在方法呼叫後,self 將不再有效。
  2. 可變借用 self:這種方法會借用 self 的可變參照,允許方法修改 self 的資料。
  3. 不可變借用 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 結構體可以持有任何型別的元素,如 i32String 等。

靜態方法與型別推斷

對於泛型結構體,我們可以定義靜態方法,如 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 是一個生命週期引數,它代表 greatestleast 參考的生命週期。

生命週期引數的推斷

Rust 允許我們在某些情況下省略生命週期引數。例如,在以下函式中:

fn find_extrema(slice: &[i32]) -> Extrema {
    // ...
}

Rust 推斷出傳回的 Extrema 例項的生命週期與輸入 slice 的生命週期相同。

自動派生常見特徵

Rust 允許我們自動為結構體派生某些常見的特徵(trait),如 CopyCloneDebugPartialEq。這可以透過在結構體定義上新增 #[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 結構體包含了一些不可變的欄位,但可能需要內部可變性來實作某些功能。

內容解密:

  1. 泛型結構體:定義結構體時使用型別引數,使其能夠持有任意型別的資料。
  2. 靜態方法:為泛型結構體定義靜態方法,如 new,用於建立新的例項。
  3. 生命週期引數:當結構體持有參考時,需要指定這些參考的生命週期,以確保記憶體安全。
  4. 自動派生特徵:使用 #[derive] 屬性自動為結構體派生常見的特徵,如 CopyDebug
  5. 內部可變性:允許結構體的某些欄位在結構體本身不可變的情況下仍然是可變的。

圖表示例

@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 提供了 CellRefCell 兩種型別來實作內部可變性(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();
    }
}

內容解密:

  1. RefCell<File>:使用 RefCell 包裝 File,使得即使在不可變的 SpiderRobot 例項中,也能獲得對 File 的可變參照。
  2. log_file.borrow_mut():動態地借用 log_file 的可變參照。如果已經有其他借用(不可變或可變),這將導致 panic。
  3. writeln!:將訊息寫入檔案。這裡假設 File 已正確開啟並準備好寫入。