切片(slice)和陣列(array)是Rust中表示相同類別值序列的特殊類別。Rust對這兩種類別的區分非常細微但至關重要。

切片與陣列的核心區別

陣列是固定長度的值序列,而切片是具有任意長度的值序列。這意味著切片可以是在執行時確定的變數長度,而陣列的長度在編譯時就已知。

切片還有一個有趣的特性:可以將切片解構為任意多個不重疊的子切片,這對實作使用分治法或遞迴策略的演算法非常方便。

陣列與切片的實際使用

let array = [0u8; 64];
let slice: &[u8] = &array;

這段程式碼初始化了一個包含64個元素的位元組陣列,所有元素都是零。0u8是一個簡寫,表示一個8位長度的無符號整數類別,值為0。0是值,u8是類別。

第二行中,我們將陣列借用為切片。這還不是特別有趣,但切片可以做一些更有趣的操作,例如多重借用:

let (first_half, second_half) = slice.split_at(32);
println!(
    "first_half.len()={} second_half.len()={}",
    first_half.len(),
    second_half.len()
);

這段程式碼呼叫了split_at()函式,這是Rust核心函式庫部分。這個函式將切片分為兩個不重疊的部分,分割點在指定的索引處(在這個例子中是32)。結果是兩個新的切片:first_half包含原始切片的前32個元素,而second_half包含剩餘的元素。

這種操作在處理大型資料集時特別有用,可以將資料分成更小的塊進行平行處理,或者實作分治演算法。輸出將顯示兩個切片的長度都符合預期:

first_half.len()=32 second_half.len()=32

實用技巧:有效使用Rust字串

在Rust程式設計中,我們大多數時候會使用String&str,而幾乎不會直接使用str。Rust標準函式庫可變字串函式是為&str類別實作的,但可變函式僅為String類別實作。

當設計函式介面時,通常最好使用&str作為引數類別,因為這提供了最大的靈活性。無論呼叫者有String還是字串字面值,都可以輕鬆地將其轉換為&str

// 更靈活的函式設計
fn process_string(s: &str) {
    // 處理字串...
}

// 使用
let literal = "字串字面值";
let owned = String::from("擁有的字串");

process_string(literal);
process_string(&owned);

靜態生命週期的使用時機

在Rust中,'static是一個特殊的生命週期指定符,它定義了在程式的整個生命週期內有效的參考(或借用變數)。雖然有些特殊情況下可能需要顯式的&'static str,但在實踐中,這是不常見的。

當你真正需要&'static str時,通常是在處理編譯時就已知的常數字串,或者需要在整個程式生命週期記憶體的字串。

字串與切片的內部機制

瞭解Rust字串和切片的內部實作,有助於我們更有效地使用它們:

  1. String本質上只是一個UTF-8字元的Vec(向量)。這意味著它具有Vec的所有特性,包括動態大小調整和高效的追加操作。

  2. str只是UTF-8字元的切片。它是一個指向字元序列的指標和一個長度值的組合。

  3. 切片是Rust的一種核心抽象,不僅用於字串,還用於各種序列資料。它們提供了一種高效的方式來參照序列中的一部分,而無需複製資料。

從C到Rust:記憶體管理的正規化轉變

當我從C轉向Rust時,最大的轉變之一是如何思考記憶體管理。在C中,我們必須手動追蹤每個分配和釋放,這為效能最佳化供了極大的靈活性,但也帶來了安全風險。

Rust透過其所有權系統改變了這一點。在Rust中,每個值都有一個「擁有者」,一個值只能有一個擁有者,當擁有者超出作用域時,值會被自動清理。這種系統讓我們能夠在不犧牲安全性的前提下,保持對記憶體的精確控制。

字串處理正是這種正規化轉變的完美例證。C語言中的字串操作自由但危險,而Rust的字串系統雖然一開始看起來較為複雜,但它提供了安全保障,同時保持了高效能。

實戰應用:自定義字串處理函式

讓我們透過一個簡單的例子來展示如何在實際應用中有效處理Rust字串:

fn main() {
    let text = "Hello, Rust world!";
    let owned_text = String::from(text);
    
    // 使用&str引數的函式
    let words_count = count_words(text);
    println!("Words in text: {}", words_count);
    
    // 修改String
    let modified = modify_string(&owned_text);
    println!("Modified: {}", modified);
    
    // 切片操作
    if let Some(first_word) = get_first_word(text) {
        println!("First word: {}", first_word);
    }
}

fn count_words(s: &str) -> usize {
    s.split_whitespace().count()
}

fn modify_string(s: &str) -> String {
    let mut result = String::from(s);
    result.push_str(" - Modified by Rust");
    result
}

fn get_first_word(s: &str) -> Option<&str> {
    s.split_whitespace().next()
}

這個例子展示了三個常見的字串操作:

  1. count_words 函式接受一個 &str 引數並計算其中的單詞數量。使用 &str 作為引數類別使函式能夠接受字串字面值或 String 的參照。

  2. modify_string 函式展示瞭如何從 &str 建立新的 String 並進行修改。注意我們不能直接修改 &str,因為它是不可變的。

  3. get_first_word 函式展示瞭如何使用切片操作從字串中提取部分內容。它回傳 Option<&str> 以安全處理空字串的情況。

這些函式展示瞭如何遵循Rust的最佳實

Rust 的資料結構:效能與安全的完美平衡

在我多年開發經驗中,資料結構的選擇往往決定了程式的效能與可維護性。Rust 語言以其獨特的所有權系統提供了一系列強大與安全的資料結構,讓開發者能夠在不犧牲效能的前提下寫出安全的程式碼。

在這篇文章中,我將探討 Rust 中三種基礎但強大的資料結構:陣列(Array)、切片(Slice)和向量(Vector)。這些結構成了 Rust 程式設計的根本,理解它們的行為對於掌握 Rust 至關重要。

深入理解切片與陣列

切片的解構與記憶體操作

Rust 的切片提供了一種強大的方式來處理連續記憶體區塊。split_at() 方法是切片操作的絕佳範例,它將一個切片解構為兩個不重疊的子切片:

let array = [0; 64];  // 型別簽名為 [u8; 64],初始化為全 0
let slice = &array[..];  // 借用陣列的切片
let (first_half, second_half) = slice.split_at(32);  // 解構切片為兩個不重疊的子切片

這段程式碼展示了 Rust 中切片的基本操作。首先建立了一個包含 64 個元素的陣列並初始化為 0,然後透過 &array[..] 借用整個陣列建立切片。最後使用 split_at() 方法將切片解構為兩個不重疊的子切片,分別代表原始陣列的前半部分和後半部分。這種解構方法在 Rust 中非常重要,因為它允許你安全地借用同一陣列的不同部分,而不會違反所有權規則。

解構的概念在 Rust 中極為重要,因為你可能經常需要借用陣列或切片的一部分。事實上,你可以使用這種模式多次借用同一切片或陣列,因為切片不會重疊。這種模式的常見應用場景是解析或解碼文字或二進位資料:

let wordlist = "one,two,three,four";
for word in wordlist.split(',') {
    println!("word={}", word);
}

這個範例展示瞭如何使用切片處理字串資料。程式碼取一個逗號分隔的字串,使用 split(',') 方法將其分割,然後遍歷並印出每個單詞。值得注意的是,這個過程中沒有堆積積設定發生 - 所有記憶體都分配在堆積積上,具有編譯時已知的固定長度,底層沒有呼叫 malloc()。這相當於使用原始 C 指標,但沒有參考計數或垃圾收集,因此沒有相關的開銷。而與與 C 指標不同,程式碼簡潔、安全與不過於冗長。

執行上述程式碼將輸出:

word=one
word=two
word=three
word=four

切片的效能最佳化

切片還具有處理連續記憶體區域的多種最佳化能。其中一個最佳化 copy_from_slice() 方法,它使用標準函式庫 memcpy() 函式來複製記憶體:

pub fn copy_from_slice(&mut self, src: &[T])
where
    T: Copy,
{
    // 安全性:`self` 根據定義是有效的 `self.len()` 元素,
    // 與 `src` 已檢查具有相同長度。切片不能重疊,
    // 因為可變參照是獨佔的。
    unsafe {
        ptr::copy_nonoverlapping(
            src.as_ptr(),
            self.as_mut_ptr(),
            self.len()
        );
    }
}

這段程式碼來自 Rust 的核心函式庫示了 copy_from_slice() 方法的實作。該方法使用 ptr::copy_nonoverlapping(),這只是 C 函式庫memcpy() 的包裝器。在某些平台上,memcpy() 具有超出普通程式碼能實作的額外最佳化其他最佳函式包括 fill()fill_with(),它們都使用 memset() 來填充記憶體。這些底層最佳化得切片在處理大量資料時極為高效。

讓我們回顧陣列和切片的核心特性:

  • 陣列是固定長度的值序列,長度在編譯時已知。
  • 切片是指向連續記憶體區域的指標,包括長度,表示任意長度的值序列。
  • 切片和陣列都可以遞迴地解構為不重疊的子切片。

向量:Rust 的多功能容器

向量(Vector)可以說是 Rust 中最重要的資料型別(其次是根據 Vec 的 String)。當你需要一個可調整大小的值序列時,你會發現自己經常建立向量。如果你有 C++ 背景,你可能聽過向量這個術語,Rust 的向量型別在許多方面與 C++ 中的非常相似。向量作為通用容器,適用於幾乎任何類別的序列。

向量是 Rust 中在堆積積設定記憶體的方式之一(另一種是智慧指標,如 Box)。向量有一些內部最佳化限制過度設定,例如以區塊設定記憶體。

深入瞭解 Vec

Vec 繼承了切片的方法,因為我們可以從向量取得切片參考。Rust 沒有導向物件程式設計意義上的繼承,而是 Vec 是一種特殊型別,同時是 Vec 和切片。例如,讓我們看一下標準函式庫as_slice() 的實作:

pub fn as_slice(&self) -> &[T] {
    self
}

這段程式碼執行了一個在正常情況下無法工作的特殊轉換。它將 self(在這裡是 Vec<T>)直接回傳為 &[T]。如果你嘗試自己編譯相同的程式碼,它會失敗。這是如何工作的?Rust 提供了一個名為 Deref(及其可變版本 DerefMut)的特徵,編譯器可以使用它隱式地將一種型別強制轉換為另一種型別。一旦為給定型別實作,該型別也將自動實作解參照型別的所有方法。對於 Vec,Rust 標準函式庫作了 Deref 和 DerefMut:

impl<T, A: Allocator> ops::Deref for Vec<T, A> {
    type Target = [T];
    fn deref(&self) -> &[T] {
        unsafe { slice::from_raw_parts(self.as_ptr(), self.len) }
    }
}

impl<T, A: Allocator> ops::DerefMut for Vec<T, A> {
    fn deref_mut(&mut self) -> &mut [T] {
        unsafe { slice::from_raw_parts_mut(self.as_mut_ptr(), self.len) }
    }
}

這兩個實作展示了 Vec 如何實作 Deref 和 DerefMut 特徵。解參照向量會將其從原始指標和長度強制轉換為切片。值得注意的是,這種操作是暫時的 — 切片不能調整大小,長度在解參照時提供給切片。

如果你從向量借用了一個切片,然後調整向量的大小,切片的大小不會改變。不過,這只有在不安全程式碼中才有可能,因為借用檢查器不會讓你同時從向量借用切片並更改向量。以下例子說明瞭這一點:

let mut vec = vec![1, 2, 3];
let slice = vec.as_slice();  // 回傳 &[i32],因為 vec 在此被借用
vec.resize(10, 0);  // 這是一個可變操作
println!("{}", slice[0]);  // 這將無法編譯

這段程式碼展示了 Rust 的借用檢查器如何防止潛在的記憶體安全問題。當你從向量建立一個不可變切片後,就不能再修改原始向量,因為這可能使切片指向無效的記憶體位置。編譯器會產生錯誤:

error[E0502]: cannot borrow `vec` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let slice = vec.as_slice();
| --- immutable borrow occurs here
4 | vec.resize(10, 0);
| ^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{}", slice[0]);
| -------- immutable borrow later used here

這種安全機制是 Rust 記憶體安全的核心,它在編譯時就能捕捉潛在的記憶體錯誤。

包裝向量

Rust 中的某些型別只是包裝了一個 Vec,例如 String。String 型別是一個 Vec,並解參照(使用前面提到的 Deref 特徵)為 str:

pub struct String {
    vec: Vec<u8>,
}

這段程式碼顯示了 String 結構的定義,它本質上只是包裝了一個 Vec。包裝向量是一種常見模式,因為 Vec 是實作任何型別可調整大小序列的首選方式。這種設計使得 String 可以利用 Vec 的所有功能,同時提供特定於字串處理的額外方法。

向量相關型別的選擇

在 90% 的情況下,你會想要使用 Vec。在其他 10% 的情況下,你可能會想要使用 HashMap(將在下一節討論)。除了 Vec 或 HashMap 之外的容器型別在某些情況下可能有意義,或者當你需要特殊最佳化,但很可能 Vec 就足夠了,使用另一種型別不會提供明顯的效能改進。

這讓我想起一句名言:

程式設計師浪費大量時間思考或擔心程式中非關鍵部分的速度,而這些效率嘗試在考慮除錯和維護時實際上產生了強烈的負面影響。我們應該忘記小效率,大約 97% 的時間:過早最佳化萬惡之源。

實際應用中的選擇與考量

在實際開發中,我發現選擇適當的資料結構往往需要考慮多方面因素。對於固定大小與編譯時已知的資料,陣列通常是最佳選擇,它提供了最高的效能和最低的記憶體開銷。但在大多數實際應用中,我們需要處理動態大小的資料,這時向量就成為首選。

在處理大量文字資料時,我經常使用切片操作來高效處理字串,而不需要進行昂貴的記憶體設定。例如,在解析設定檔或處理日誌檔案時,使用切片可以顯著提高程式效能。

值得注意的是,Rust 的這些資料結構不僅提供了高效能,還透過所有權系統保證了記憶體安全。這種安全性不是透過執行時檢查實作的(這會帶來效能開銷),而是在編譯時就由編譯器強制執行的。

在選擇資料結構時,我建議首先考慮使用 Vec 或標準函式庫其他集合,只有在確實需要特定最佳化才考慮自定義實作。過早最佳化實是程式設計中的一大陷阱,而 Rust 的標準函式庫提供了高效與安全的實作,滿足大多數需求。

透過深入理解這些核心資料結構,你將能夠更有效地使用 Rust 語言,寫出既安全又高效的程式碼。在下一篇文章中,我將探討 Rust 中的 HashMap 和其他集合型別,以及它們在解決實際問題中的應用。

Rust 記憶體管理的靈活解決方案

在 Rust 程式設計中,記憶體管理是一個核心考量。當面臨需要設定大區塊連續記憶體或關心記憶體位置的情況時,有一個簡單的解決方案:將 Box 放入 Vec 中(即使用 Vec<Box<T>>)。這種方式能有效避免大型連續記憶體設定的問題,同時保持 Rust 的記憶體安全保證。

Rust 標準函式庫集合類別

Rust 標準函式庫了多種集合類別,每種都有其特定用途和效能特性。除了常用的 Vec,以下是其他值得了解的集合:

雙端佇列與鏈結串列

  • VecDeque:根據 Vec 實作的可調整大小的雙端佇列,適合需要在兩端高效插入和刪除元素的場景
  • LinkedList:雙向鏈結串列,適用於需要在任意位置頻繁插入和刪除元素的情況

對映與集合

  • HashMap:雜湊對映,提供常數時間的查詢效能
  • BTreeMap:根據 B 樹的對映,保持鍵的排序
  • HashSet:根據 HashMap 的雜湊集合
  • BTreeSet:根據 BTreeMap 的 B 樹集合,元素保持排序

優先佇列

  • BinaryHeap:根據二元堆積積作的優先佇列,內部使用 Vec

對於 Rust 核心資料結構的最新效能詳情和建議,可以參考 Rust 標準函式庫參考檔案。值得一提的是,若現有集合不符合需求,在 Vec 上構建自定義資料結構也是合理的做法。Rust 標準函式庫 BinaryHeap 就是一個很好的範例,展示瞭如何根據 Vec 構建複雜資料結構。

深入理解 HashMap

在 Rust 中,HashMapVec 同樣是最常用的容器類別。當需要一個能夠使用鍵以常數時間檢索專案的集合時,HashMap 是首選。

Rust 的 HashMap 實作與其他語言中的雜湊對映相似,但得益於 Rust 的最佳化它通常更快與更安全。

雜湊函式選擇

HashMap 預設使用 Siphash-1-3 函式進行雜湊,這也被 Python(從 3.4 版開始)、Ruby、Swift 和 Haskell 等語言採用。這個函式為常見情況提供了良好的平衡,但對於非常小或非常大的鍵(如整數類別或非常大的字串)可能不太適合。

自訂雜湊函式

對於特定需求,Rust 允許為 HashMap 提供自訂雜湊函式。要使用自訂雜湊函式,需要找到現有實作或編寫實作必要特徵的雜湊函式。HashMap 要求雜湊函式實作 std::hash::BuildHasherstd::hash::Hasherstd::default::Default 特徵。

讓我們看標準函式庫HashMap 的實作:

impl<K, V, S> HashMap<K, V, S>
where
    K: Eq + Hash,
    S: BuildHasher,
{
    // 實作內容被省略
}

這段程式碼展示了 HashMap 的型別簽名和約束條件。它有三個型別引數:K(鍵的型別)、V(值的型別)和 S(雜湊建構器的型別)。where 子句指定了約束:鍵型別 K 必須實作 EqHash 特徵,而雜湊建構器型別 S 必須實作 BuildHasher 特徵。這確保了鍵可以被比較並雜湊化。

進一步探究 BuildHasher,它實際上只是 Hasher 特徵的包裝器:

pub trait BuildHasher {
    /// 將被建立的雜湊器的型別
    type Hasher: Hasher;
    
    // build_hasher() 方法被省略
}

BuildHasher 特徵定義了一個關聯型別 Hasher,它必須實作 Hasher 特徵。這個特徵只要求一個 build_hasher() 方法(程式碼被省略),該方法回傳新的 Hasher 例項。這種設計允許靈活建立不同類別的雜湊器。

BuildHasherHasher API 將大部分實作細節留給雜湊函式的作者。BuildHasher 只需要一個 build_hasher() 方法,該方法回傳新的 Hasher 例項。Hasher 特徵只需要兩個方法:write()finish()write() 接收一個位元組切片 (&[u8]),而 finish() 回傳一個表示計算雜湊的 64 位無符號整數。

Rust 生態系統中有許多實作各種雜湊函式的 crate。例如,我們可以使用 MetroHash(由 J. Andrew Rogers 設計的 SipHash 替代方案)構建一個 HashMap

use metrohash::MetroBuildHasher;
use std::collections::HashMap;

let mut map = HashMap::<String, String, MetroBuildHasher>::default();
map.insert("hello?".into(), "Hello!".into());
println!("{:?}", map.get("hello?"));

這段程式碼示範瞭如何使用自訂雜湊函式建立 HashMap。首先匯入 MetroBuildHasher 和標準 HashMap,然後建立一個使用 MetroBuildHasherHashMap 例項。鍵和值都是 String 型別。接著插入一對鍵值,並使用 get 方法檢索值。.into() 方法將 &str 轉換為 Stringprintln! 巨集中的 {:?} 使用 fmt::Debug 特徵格式化輸出值。

建立可雜湊的型別

HashMap 可以使用任意鍵和值,但鍵必須實作 std::cmp::Eqstd::hash::Hash 特徵。許多特徵(如 EqHash)可以使用 #[derive] 屬性自動派生。考慮以下例子:

#[derive(Hash, Eq, PartialEq, Debug)]
struct CompoundKey {
    name: String,
    value: i32,
}

這段程式碼定義了一個名為 CompoundKey 的結構體,由 name(字串)和 value(32位整數)組成。使用 #[derive] 屬性為這個結構體自動實作了四個特徵:

  1. Hash - 使結構體可雜湊,能作為 HashMap 的鍵
  2. Eq - 實作完全等價關係
  3. PartialEq - 實作部分等價關係(Eq 依賴於它)
  4. Debug - 提供自動的除錯列印方法,方便除錯和測試

#[derive] 屬性是 Rust 中一種自動生成特徵實作的方式。這些特徵實作具有可組合性:只要它們存在於任何型別子集,就可以為型別超集派生它們。這意味著,如果結構體的所有欄位都實作了這些特徵,那麼整個結構體也可以自動實作它們。

Rust 型別系統概覽

作為強型別語言,Rust 提供了多種建模資料的方式。最基本的是原始型別,處理最基本的資料單位,如數值、位元組和字元。其上是結構體和列舉,用於封裝其他型別。最後,別名讓我們可以重新命名和組合其他型別成為新型別。

在 Rust 中,有四類別:

原始型別

原始型別由 Rust 語言和核心函式庫。這些相當於在任何其他強型別語言中找到的原始型別,但有一些例外。核心原始型別包括整數、浮點數、元組和陣列。

整數型別

整數型別可以透過其標誌指定(i 表示有符號,u 表示無符號),後面跟著位元數。大小以 iu 開頭,後面跟著 size。浮點數型別以 f 開頭,後跟位元數。

整數型別可以是有符號或無符號整數,長度從 8 到 128 位元不等(以位元組為單位,即 8 位元)。大小型別是特定於架構的大小型別,可以有符號或無符號。浮點數型別是 32 或 64 位元浮點數。

資料結構選擇的實用考量

在實際應用中,選擇合適的資料結構對程式效能至關重要。以下是我在實際專案中發現的一些關鍵考量:

  1. 記憶體佔用與存取模式:對於小型資料集或需要頻繁隨機存取的場景,Vec 通常是最佳選擇。但當資料量增大時,考慮使用 HashMapBTreeMap 以獲得更好的查詢效能。

  2. 插入/刪除操作頻率:如果應用需要頻繁在集合中間插入或刪除元素,LinkedList 可能比 Vec 更適合,儘管在大多數情況下,Vec 的整體效能仍然更好。

  3. 排序需求:需要維持元素排序的場景中,BTreeMapBTreeSet 比它們的雜湊版本更合適,特別是當需要按範圍查詢時。

  4. 雜湊函式選擇:針對特定資料類別最佳化湊函式可以顯著提升 HashMapHashSet 的效能。例如,對於小整數鍵,FxHash 通常比預設的 SipHash 更快。

  5. 自訂資料結構:有時候,標準函式庫集合無法滿足特定需求。在這種情況下,根據 Vec 或其他基本集合構建自訂資料結構是合理的策略。

在 Rust 中,資料結構的選擇不僅關乎效能,還涉及到記憶體安全和程式正確性。利用 Rust 的所有權系統和型別系統,可以在編譯時捕捉許多潛在錯誤,這是使用 Rust 進行系統程式設計的主要優勢之一。

透過深入理解 Rust 的資料結構,開發者可以編寫既安全又高效的程式,充分利用 Rust 的強大功能,同時避免常見的記憶體和並發問題。在實踐中,經常評估應用的需求並選擇最適合的資料結構組合,是最佳化Rust 應用效能的關鍵。

Rust 資料結構與類別系統深度剖析

序列陣列與基本型別

在 Rust 的類別系統中,序列陣列是指固定長度的值序列,可以進行切片操作。這種結構在處理固定大小的資料集時特別有用。

整數型別識別符

Rust 提供了豐富的整數型別,每種型別都有對應的位元長度和有無正負號的版本:

長度有符號識別符無符號識別符C 語言等效
8 位元i8u8char 和 uchar
16 位元i16u16short 和 unsigned short
32 位元i32u32int 和 unsigned int
64 位元i64u64long, long long 等(平台相關)
128 位元i128u128GCC 和 Clang 中的 _int128 或 _uint128

Rust 中整數字面值的型別可以透過附加型別識別符來指定。例如,0u8 表示一個值為 0 的無符號 8 位元整數。整數值可以加上字首 0b0o0xb 來表示二進位、八進位制、十六進位制或位元組字面值。

let value = 0u8;
println!("value={}, length={}", value, std::mem::size_of_val(&value));
let value = 0b1u16;
println!("value={}, length={}", value, std::mem::size_of_val(&value));
let value = 0o2u32;
println!("value={}, length={}", value, std::mem::size_of_val(&value));
let value = 0x3u64;
println!("value={}, length={}", value, std::mem::size_of_val(&value));
let value = 4u128;
println!("value={}, length={}", value, std::mem::size_of_val(&value));
println!("Binary (base 2) 0b1111_1111={}", 0b1111_1111);
println!("Octal (base 8) 0o1111_1111={}", 0o1111_1111);
println!("Decimal (base 10) 1111_1111={}", 1111_1111);
println!("Hexadecimal (base 16) 0x1111_1111={}", 0x1111_1111);
println!("Byte literal b'A'={}", b'A');

這段程式碼展示了 Rust 的整數型別和字面值表示法。每個變數宣告都使用不同長度和表示基底的整數,並印出其值和記憶體大小。特別注意 std::mem::size_of_val(&value) 函式,它回傳變數在記憶體中的大小(以位元組為單位)。數字間的底線(如 1111_1111)用於增加可讀性,不影響數值。執行結果顯示不同進位制的表示方式和記憶體佔用。

大小型別與算術運算

Rust 提供了平台相關的大小型別:usizeisize。這些型別通常在 32 位元系統上為 32 位元,在 64 位元系統上為 64 位元。usize 相當於 C 語言中的 size_t,而 isize 允許對大小進行有符號算術。在 Rust 標準函式庫回傳或期望長度引數的函式通常使用 usize

基本型別的算術運算

Rust 的算術運算與 C/C++ 等語言不同,預設會進行檢查。例如,在 C 語言中,除以零是未定義行為:

#include <stdio.h>
int main() {
    printf("%d\n", 1 / 0);
}

這段 C 程式碼會編譯透過(雖然有警告),但執行時會產生看似隨機的結果。

而在 Rust 中,所有算術運算預設都會被檢查:

// println!("{}", 1 / 0);  // 不會編譯
let one = 1;
let zero = 0;
// println!("{}", one / zero);  // 不會編譯
let one = 1;
let zero = one - 1;
// println!("{}", one / zero);  // 不會編譯
let one = { || 1 }();
let zero = { || 0 }();
println!("{}", one / zero);  // 這裡會引發 panic

這段程式碼演示了 Rust 的編譯時安全檢查。前三個註解掉的例子在編譯階段就會被 Rust 編譯器捕捉。第四個例子透過閉包間接初始化變數,欺騙了編譯器,但在執行時會引發 panic,輸出明確的錯誤訊息:“thread ‘main’ panicked at ‘attempt to divide by zero’"。這種設計大提高了程式的可靠性。

如果需要更精細地控制算術運算,Rust 的基本型別提供了多種方法:

assert_eq!((100i32).checked_div(1i32), Some(100i32));
assert_eq!((100i32).checked_div(0i32), None);

對於標量型別(整數、大小和浮點數),Rust 提供了一系列方法,以 checked、unchecked、overflowing 和 wrapping 形式提供基本算術運算。

如果想要與 C、C++、Java、C# 等語言相容的行為,通常會使用 wrapping 形式,它執行模運算,與 C 等效操作相容:

assert_eq!(0xffu8.wrapping_add(1), 0);
assert_eq!(0xffffffffu32.wrapping_add(1), 0);
assert_eq!(0u32.wrapping_sub(1), 0xffffffff);
assert_eq!(0x80000000u32.wrapping_mul(2), 0);

這段程式碼展示了 Rust 的 wrapping 算術運算,它模擬了 C 語言中的溢位行為。例如,當 0xff(255) 加 1 時,由於 u8 型別的限制,結果會「環繞」回到 0。同樣,當 0 減 1 時,結果會環繞到最大值 0xffffffff。這些操作在處理特定演算法或與 C 語言互操作時非常有用,但需要謹慎使用,因為它們繞過了 Rust 的安全檢查。

元組的使用

Rust 的元組類別於其他語言中的元組 - 固定長度的值序列,與每個值可以有不同的型別。Rust 中的元組不具有反射性;與陣列不同,你不能對元組進行迭代,不能取元組的切片,也不能在執行時確定其元件的型別。元組本質上是 Rust 中的一種語法糖,雖然有用,但相當有限。

let tuple = (1, 2, 3);
println!("tuple = ({}, {}, {})", tuple.0, tuple.1, tuple.2);

// 使用 match 進行臨時解構
match tuple {
    (one, two, three) => println!("{}, {}, {}", one, two, three),
}

// 解構元組
let (one, two, three) = tuple;
println!("{}, {}, {}", one, two, three);

這段程式碼演示了元組的三種使用方式:直接透過索引存取元素(.0.1.2),使用 match 進行模式比對解構,以及直接解構指定。元組解構是一種強大的功能,可以將元組中的值移動到新的變數中,使程式碼更簡潔清晰。

元組最常見的用途是從函式回傳多個值:

fn swap<A, B>(a: A, b: B) -> (B, A) {
    (b, a)
}

fn main() {
    let a = 1;
    let b = 2;
    println!("{:?}", swap(a, b));
}

這個簡潔的 swap 函式展示了元組作為回傳值的用法。函式接受兩個不同型別的引數,並回傳一個元組,其中引數的順序被交換。泛型引數 AB 允許函式處理任何型別的值,顯示了 Rust 強大的型別系統。值得注意的是,標準函式庫最多 12 個元素的元組提供了 trait 實作,因此建議不要建立超過 12 個引數的元組。

結構體的使用

結構體是 Rust 的主要構建塊。它們是複合資料型別,可以包含任意型別和值的集合。它們在本質上類別於 C 結構體或導向物件語言中的類別它們可以透過類別於 C++ 中的範本或 Java、C#、TypeScript 中的泛型來進行泛型組合。

當需要以下功能時,應該使用結構體:

  • 提供有狀態的函式(即操作僅內部狀態的函式或方法)
  • 控制對內部狀態的存取(即私有變數)
  • 在 API 後面封裝狀態

這裡有幾點需要注意:

  1. 結構體不是必需的,可以只用函式編寫 API,類別於 C API
  2. 結構體只用於定義實作,而不是指定介面,這與物件導向語言不同

Rust 的結構體是組織和封裝資料的主要方式,類別於其他語言中的類別物件。不過,Rust 採用了與傳統物件導向程式設計的方法 - 它將資料(結構體)和行為(trait)分離。結構體用於定義資料結構,而 trait 用於定義介面和行為。這種分離使得程式碼更加模組化和可組合。

當設計 Rust 程式時,玄貓建議先思考資料結構,再考慮這些資料結構應該實作哪些行為。這與物件導向思維有所不同,但能夠產生更清晰、更靈活的設計。

在實際開發中,我發現 Rust 的這種設計哲學特別適合系統程式設計效能應用程式,因為它允許開發者精確控制記憶體佈局和行為實作,同時保持型別安全和記憶體安全。

結構體與元組的比較

結構體和元組都是組合資料的方式,但它們有著不同的用途和特性:

  1. 命名與索引:結構體的欄位有名稱,而元組使用索引
  2. 擴充套件性:結構體更適合隨時間擴充套件的資料結構
  3. 封裝性:結構體提供更好的封裝和存取控制
  4. 介面表達:結構體更適合實作複雜的介面

當需要快速組合幾個值與不需要命名欄位時,元組是理想選擇。而當建立更複雜的資料結構,特別是那些需要方法和行為的結構時,結構體是更好的選擇。

Rust 的型別系統非常豐富與靈活,允許開發者根據具體需求選擇最合適的資料結構。無論是簡單的基本型別、元組還是複雜的結構體,它們都經過精心設計,以確保記憶體安全和高效能。

透過理解這些核心概念,開發者可以充分利用 Rust 強大的型別系統,建立既安全又高效的程式。