返回文章列表

Rust 型別系統與記憶體管理

本文探討 Rust 的型別系統,包含結構體、列舉、別名等,以及 Rust 的記憶體管理機制,包含堆積疊與堆積的分配、所有權語義,並說明如何使用參考計數指標、智慧指標及自定義分配器,幫助讀者更有效地運用 Rust 開發程式。

程式語言 系統程式設計

Rust 的型別系統提供結構體和列舉等自定義型別,結構體整合相關資料,列舉定義命名變數。搭配 DebugCloneDefault 等衍生特徵,方便程式碼的除錯、複製和初始化。impl 關鍵字則允許為結構體定義方法,擴充套件其功能。列舉型別定義一組互斥變數,並可透過 From 特徵實作數字到列舉值的轉換。別名簡化複雜型別,提升程式碼可讀性。Rust 的錯誤處理仰賴 Result 列舉,搭配自定義錯誤型別和 From 特徵,能更精確地處理錯誤。? 運算元簡化錯誤傳遞流程。FromInto 特徵實作不同型別間的轉換,TryFromTryInto 則處理可能失敗的轉換。在與 C 程式碼互動時,#[repr(C)] 屬性確保 Rust 結構與 C 結構體記憶體佈局相容。Rust 的記憶體管理區分堆積疊和堆積,堆積用於動態分配,堆積疊則與函式作用域繫結。BoxVecString 等型別在堆積上分配記憶體,而基本型別和字串切片則在堆積疊上分配。

Rust 型別系統:原始型別、結構體、列舉和別名

在 Rust 中,結構體(struct)和列舉(enum)是兩種重要的自定義型別。結構體用於將多個相關的值組合在一起,而列舉則用於定義一組命名的變體。

結構體的可見性和派生特徵

結構體的可見性與其成員元素一樣受到可見性語義的影響。若要在 crate 外部使用某個結構體(即從函式庫中使用),則必須使用 pub struct MyStruct { ... } 宣告。未明確宣告為公開的結構體將無法在 crate 外部存取。

在宣告結構體時,通常會希望派生一些標準的特徵實作,例如 DebugCloneDefault

#[derive(Debug, Clone, Default)]
struct DebuggableStruct {
    string: String,
    number: i32,
}

這些特徵提供了以下功能:

  • Debug:提供 fmt() 方法,用於格式化輸出結構體內容
  • Clone:提供 clone() 方法,用於建立結構體的副本
  • Default:提供 default() 方法,用於傳回結構體的預設例項

派生這些特徵後,可以使用以下程式碼:

let debuggable_struct = DebuggableStruct::default();
println!("{:?}", debuggable_struct);
println!("{:?}", debuggable_struct.clone());

內容解密:

  1. #[derive(Debug, Clone, Default)]:自動為 DebuggableStruct 實作 DebugCloneDefault 特徵。
  2. DebuggableStruct::default():呼叫 Default 特徵的 default() 方法,建立一個預設的 DebuggableStruct 例項。
  3. println!("{:?}", debuggable_struct):使用 Debug 特徵的 fmt() 方法輸出 debuggable_struct 的內容。
  4. debuggable_struct.clone():呼叫 Clone 特徵的 clone() 方法,建立 debuggable_struct 的副本。

為結構體實作方法

可以使用 impl 關鍵字為結構體實作方法:

impl DebuggableStruct {
    fn increment_number(&mut self) {
        self.number += 1;
    }
}

這個方法接受一個可變的 self 參照,並將 number 欄位加 1。

內容解密:

  1. impl DebuggableStruct:為 DebuggableStruct 結構體實作方法。
  2. fn increment_number(&mut self):定義一個名為 increment_number 的方法,接受一個可變的 self 參照。
  3. self.number += 1:將 number 欄位加 1。

使用列舉

列舉是一種特殊的結構體,包含了一組互相排斥的變體。列舉可以用於定義一組命名的常數值。

#[derive(Debug)]
enum JapaneseDogBreeds {
    AkitaKen,
    HokkaidoInu,
    KaiKen,
    KishuInu,
    ShibaInu,
    ShikokuKen,
}

對於上述列舉,JapaneseDogBreeds 是列舉型別的名稱,每個元素都是一個單位型別。可以使用以下程式碼:

println!("{:?}", JapaneseDogBreeds::ShibaInu);
println!("{:?}", JapaneseDogBreeds::ShibaInu as u32);

內容解密:

  1. #[derive(Debug)]:自動為 JapaneseDogBreeds 實作 Debug 特徵。
  2. enum JapaneseDogBreeds { ... }:定義一個名為 JapaneseDogBreeds 的列舉型別。
  3. JapaneseDogBreeds::ShibaInu:存取 ShibaInu 變體。
  4. JapaneseDogBreeds::ShibaInu as u32:將 ShibaInu 變體轉換為 u32 型別。

將數字轉換為列舉值

可以實作 From 特徵來將數字轉換為列舉值:

impl From<u32> for JapaneseDogBreeds {
    fn from(other: u32) -> Self {
        match other {
            other if JapaneseDogBreeds::AkitaKen as u32 == other => JapaneseDogBreeds::AkitaKen,
            other if JapaneseDogBreeds::HokkaidoInu as u32 == other => JapaneseDogBreeds::HokkaidoInu,
            // ...
            _ => panic!("Unknown breed!"),
        }
    }
}

內容解密:

  1. impl From<u32> for JapaneseDogBreeds { ... }:為 JapaneseDogBreeds 實作 From<u32> 特徵。
  2. fn from(other: u32) -> Self { ... }:定義一個名為 from 的方法,接受一個 u32 引數並傳回一個 JapaneseDogBreeds 例項。
  3. match other { ... }:使用模式比對來將數字轉換為對應的列舉值。

使用別名與錯誤處理

在 Rust 程式設計中,別名(Aliases)與錯誤處理(Error Handling)是兩個重要的概念。別名提供了一種簡便的方式來指代複雜的型別,而錯誤處理則是確保程式穩健性的關鍵。

4.5.5 使用別名

別名是一種特殊的型別,允許為任何其他型別提供一個替代名稱。它們等同於 C 和 C++ 中的 typedef 或 C++ 中的 using 關鍵字。定義別名並不會建立新的型別。

別名的常見用途

  1. 提供公開型別的別名定義:出於對函式庫使用者的便利和易用性考慮。
  2. 提供複雜型別組合的簡寫型別

例如,可以為常用的雜湊對映(Hash Map)建立一個型別別名:

pub(crate) type MyMap = std::collections::HashMap<String, MyStruct>;

這樣一來,就可以使用 MyMap 來代替冗長的 std::collections::HashMap<String, MyStruct>

對於函式庫來說,匯出具有合理預設值的公開型別別名是一種常見的做法,尤其是在使用泛型的情況下。這可以幫助使用者更容易地確定給定介面所需的型別。

dryoc 函式庫中,提供了多個型別別名以方便使用。例如:

/// 用於金鑰衍生的堆積疊分配金鑰型別別名。
pub type Key = StackByteArray<CRYPTO_KDF_KEYBYTES>;

/// 用於金鑰衍生的堆積疊分配上下文型別別名。
pub type Context = StackByteArray<CRYPTO_KDF_CONTEXTBYTES>;

內容解密:

  1. pub type 定義:使用 pub type 來定義公開的型別別名,使得使用者可以方便地使用這些型別而無需關心實作細節。
  2. StackByteArray 使用:這裡使用了 StackByteArray 來定義金鑰和上下文的型別,這是一種用於堆積疊分配的陣列型別,能夠確保資料在堆積疊上安全地儲存。

4.6 使用 Result 進行錯誤處理

Rust 提供了多種功能來簡化錯誤處理,這些功能根據一個名為 Result 的列舉。

Result 列舉

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Result 表示一個可能成功(傳回結果)或失敗(傳回錯誤)的操作。許多 Rust 函式的傳回型別都是 Result

自定義錯誤型別

通常需要為自己的函式庫建立自定義的錯誤型別。這可以是一個包含所有預期錯誤種類別的列舉,也可以是一個簡單的結構體,包含有用的資訊,如錯誤訊息。

#[derive(Debug)]
struct Error {
    message: String,
}

實作 From 特徵

為了使自定義的錯誤型別能夠與其他函式的錯誤型別相互轉換,需要實作 From 特徵。這使得在使用 ? 運算元時,能夠自動進行錯誤型別的轉換。

impl From<std::io::Error> for Error {
    fn from(other: std::io::Error) -> Self {
        Self {
            message: other.to_string(),
        }
    }
}

使用 ? 運算元進行錯誤處理

在函式中使用 ? 運算元,可以在操作成功時傳回結果,在失敗時立即傳回錯誤。

fn read_file(name: &str) -> Result<String, Error> {
    use std::fs::File;
    use std::io::prelude::*;
    let mut file = File::open(name)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

內容解密:

  1. ? 運算元的使用:在 File::openread_to_string 方法後使用 ?,可以在發生錯誤時自動傳回錯誤。
  2. From 特徵的實作:實作了 From<std::io::Error>,使得 std::io::Error 可以自動轉換為自定義的 Error 型別。

4.7 使用 From/Into 進行型別轉換

Rust 的標準函式庫提供了 FromInto 特徵,用於在不同型別之間進行標準化的轉換。

From 特徵

只需要實作 From 特徵,因為 IntoFrom 的互逆操作,通常由編譯器自動推導。

pub trait From<T>: Sized {
    /// Performs the conversion.
    fn from(_: T) -> Self;
}

示例:實作 From 特徵

struct StringWrapper(String);

impl From<&str> for StringWrapper {
    fn from(other: &str) -> Self {
        Self(other.into())
    }
}

fn main() {
    println!("{}", StringWrapper::from("Hello, world!").0);
}

內容解密:

  1. From 特徵的實作:為 StringWrapper 實作了 From<&str>,使得可以從 &str 建立 StringWrapper 例項。
  2. into() 方法的使用:在 from 方法內部使用了 other.into()&str 轉換為 String

Rust 中的型別轉換與外部函式介面處理

在 Rust 程式設計中,型別轉換是一項常見的需求,尤其是在處理錯誤和與外部函式庫互動時。本章將探討如何使用 FromInto 特徵進行型別轉換,以及如何處理與 C 語言函式庫的相容性問題。

使用 FromInto 進行型別轉換

Rust 提供了 FromInto 兩個特徵來處理型別之間的轉換。當你需要將一個型別轉換成另一個型別時,可以實作 From 特徵。Rust 會自動為你實作對應的 Into 特徵。

範例:實作自訂錯誤型別的 From 特徵

假設我們有一個自訂的錯誤型別 Error,並希望將 std::io::Error 轉換成我們的自訂錯誤型別。我們可以實作 From 特徵來完成這項任務。

use std::{fs::File, io::Read};

struct Error(String);

impl From<std::io::Error> for Error {
    fn from(other: std::io::Error) -> Self {
        Self(other.to_string())
    }
}

fn read_file(name: &str) -> Result<String, Error> {
    let mut f = File::open(name)?;
    let mut output = String::new();
    f.read_to_string(&mut output)?;
    Ok(output)
}

內容解密:

  1. 我們定義了一個自訂錯誤型別 Error,它包含一個字串。
  2. 實作了 From<std::io::Error> 特徵,將 std::io::Error 轉換成我們的 Error 型別。
  3. read_file 函式中,我們使用了 ? 運算元來自動進行錯誤轉換。

TryFromTryInto 特徵

除了 FromInto,Rust 還提供了 TryFromTryInto 特徵,用於可能失敗的型別轉換。

處理 FFI 相容性

當需要與 C 語言函式庫互動時,我們需要確保 Rust 結構體與 C 結構體的記憶體佈局相容。為此,我們可以使用 #[repr(C)] 屬性,並使用來自 libc 套件的 C 型別。

範例:對映 C 結構體到 Rust

假設我們有一個 C 結構體定義如下:

struct gzFile_s {
    unsigned have;
    unsigned char *next;
    z_off64_t pos;
};

對應的 Rust 結構體應該這樣定義:

#[repr(C)]
struct GzFileState {
    have: c_uint,
    next: *mut c_uchar,
    pos: i64,
}

內容解密:

  1. 使用 #[repr(C)] 屬性確保 Rust 結構體的記憶體佈局與 C 結構體相容。
  2. 使用來自 libc 套件的 C 型別,如 c_uintc_uchar
  3. 定義了外部 C 函式的介面,使用 extern "C" 區塊。

最佳實踐

  1. 實作 From 特徵:當需要將一個型別轉換成另一個型別時,實作 From 特徵。
  2. 使用 TryFromTryInto:當型別轉換可能失敗時,使用這些特徵。
  3. 使用 rust-bindgen:當需要與 C 語言函式庫互動時,考慮使用 rust-bindgen 自動生成繫結。

透過遵循這些最佳實踐和,你可以更有效地在 Rust 中進行型別轉換和處理外部函式介面。

深入理解Rust的記憶體管理

在前一章中,我們討論了Rust的資料結構。為了完整理解Rust的資料結構,還需要進一步探討記憶體管理和它與Rust資料結構之間的關係。Rust的核心資料結構提供了良好的抽象來管理記憶體的分配和釋放,但某些應用可能需要更進階的功能,例如自定義分配器、參考計數、智慧指標或系統層級的功能,這些都超出了Rust語言本身的範疇。

雖然不需要深入理解記憶體管理就能有效地使用Rust,但在很多情況下,瞭解背後發生的事情是非常有益的。本章將探討Rust的記憶體管理。

本章重點

 學習Rust中堆積疊和堆積的記憶體管理細節  理解Rust的所有權語義  使用參考計數指標  有效利用智慧指標  為特定使用場景實作自定義分配器

記憶體管理:堆積疊與堆積

Rust具有非常強大且細粒度的記憶體管理語義。初學者可能會覺得Rust的記憶體管理有些難以捉摸。例如,當使用字串或向量時,通常不會太在意記憶體是如何被分配的。在某些方面,這與Python或Ruby等指令碼語言相似,記憶體管理被高度抽象化,很少需要考慮。

在底層,Rust的記憶體管理與C或C++等語言並無太大差異。然而,Rust試圖在不需要擔心記憶體管理時將其隱藏起來。當需要時,語言提供了所需的工具,可以根據嘗試完成的任務調整複雜度。讓我們快速回顧一下堆積疊和堆積之間的區別。

堆積

堆積是一塊用於動態分配的記憶體區域。通常,這是記憶體中保留用於可調整大小的資料結構或在執行時期才知道大小的資料的位置。並不是說不能在堆積中儲存靜態資料;然而,對於靜態資料,通常最好使用堆積疊(編譯器通常會將靜態資料放入程式的靜態記憶體段中作為最佳化,因此它實際上並未被推入堆積疊)。堆積通常由底層作業系統或核心語言函式函式倉管理;然而,程式設計師可以選擇實作自己的堆積。對於像嵌入式系統這樣記憶體受限的系統,通常會編寫不使用堆積的程式碼。

堆積疊

堆積疊是一個與函式作用域繫結的執行緒區域性記憶體空間。堆積疊使用後進先出(LIFO)順序分配。進入函式時,記憶體會被分配並推入堆積疊。離開函式時,記憶體會被釋放並從堆積疊中彈出。對於堆積疊分配的資料,大小需要在編譯時期就已知。在堆積疊上分配記憶體通常比使用堆積快得多。每個執行緒都有自己的堆積疊。

範例程式碼

// 在堆積上分配的值
let heap_integer = Box::new(1);
let heap_integer_vec = vec![0; 100];
let heap_string = String::from("heap string");

// 在堆積疊上分配的值
let stack_integer = 69420;
let stack_allocated_string = "stack string";

內容解密:

  1. Box::new(1):使用Box在堆積上分配了一個整數1。Box是一個智慧指標,用於在堆積上分配值。
  2. vec![0; 100]:建立了一個包含100個零的向量,儲存在堆積上。向量是一個動態大小的集合。
  3. String::from(“heap string”):從字串切片建立了一個String例項,儲存在堆積上。String是根據Vec的,因此它是一個堆積分配的字串。
  4. let stack_integer = 69420;:在堆積疊上分配了一個整數69420。
  5. let stack_allocated_string = “stack string”;:在堆積疊上分配了一個字串切片"stack string"。字串切片是靜態分配的,因此實際上是儲存在程式的靜態記憶體段中。

許多語言抽象了堆積疊和堆積的概念,因此不需要擔心它們。在C和C++中,通常使用malloc()new關鍵字在堆積上分配記憶體,並在函式內宣告變數以將其分配在堆積疊上。Java也有new關鍵字用於在堆積上分配記憶體;然而,在Java中,記憶體會被垃圾收集,因此不需要管理堆積的清理。

在Rust中,堆積疊由編譯器和平台實作細節管理。在另一方面,在堆積上分配資料可以自定義以滿足需求(本章後面將討論自定義分配器),這與C或C++中的情況類別似。