iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Rust

大家一起跟Rust當好朋友吧!系列 第 8

Day 8: 集合型別:Vectors, Strings, Hash Maps - 理解動態的資料結構

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第八天!

恭喜你進入第二週!經過第一週的紮實基礎訓練,相信你對 Rust 的核心概念已經有了不錯的理解。今天我們要進入一個非常實用的主題:集合型別

在第一週,我們學過陣列 [1, 2, 3, 4],但它有個限制:大小必須在編譯時確定。在實際開發中,我們經常需要處理大小會動態變化的資料,比如用戶輸入的文字、從資料庫查詢的結果、或是網路 API 回傳的資料List。

今天我們將學習 Rust 標準函式庫提供的三個最重要的集合型別:

  • Vec<T>:可動態增長的陣列
  • String:可變長度的 UTF-8 字串
  • HashMap<K, V>:Key Value 的 雜湊表

這些集合型別的共同特點是:它們的資料都儲存在Heap上,可以在執行時動態調整大小。掌握它們是寫出實用 Rust 程式的關鍵!

Vector (Vec<T>):最實用的動態陣列

建立 Vector

Vector 是一個泛型集合,可以儲存相同型別的多個值:

fn main() {
    // 建立空 vector
    let mut v1: Vec<i32> = Vec::new();
    
    // 使用 vec! 巨集初始化
    let v2 = vec![1, 2, 3, 4, 5];
    
    // 推論型別
    let mut v3 = Vec::new();
    v3.push(5);  // 編譯器現在知道 v3 是 Vec<i32>
    
    println!("v2: {:?}", v2);
}

💡 小提示:什麼是巨集?

在使用 vec! 的時候,你可能注意到它後面有個驚嘆號 !。這是因為 vec! 不是一個普通的函式,而是一個巨集 (Macro)

巨集是在編譯時期展開的程式碼模板,它們以 ! 結尾來標識。vec! 會在編譯時自動展開成建立並初始化 Vector 的程式碼,讓我們能寫出更簡潔的程式碼。

除了 vec!,你可能還會遇到其他常用的巨集,如 println!format!panic! 等等等等。
在之後我們會有一天來教如何自訂巨集!!!請大家敬請期待XD

更新 Vector

fn main() {
    let mut v = Vec::new();
    
    // 新增元素
    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
    
    println!("向量內容: {:?}", v);
    
    // 移除最後一個元素
    let last = v.pop();
    println!("移除的元素: {:?}", last);  // Some(8)
    println!("移除後: {:?}", v);
    
    // 插入到指定位置
    v.insert(1, 99);  // 在索引 1 插入 99
    println!("插入後: {:?}", v);
    
    // 移除指定位置的元素
    let removed = v.remove(1);  // 移除索引 1 的元素
    println!("移除的元素: {}", removed);  // 99
    println!("最終結果: {:?}", v);
}

讀取 Vector 元素

Vector 提供兩種讀取元素的方式,各有其使用場景:

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    
    // 方法 1: 使用索引(可能 panic)
    let third = &v[2];
    println!("第三個元素是: {}", third);
    
    // 方法 2: 使用 get 方法(安全)
    let third = v.get(2);
    match third {
        Some(value) => println!("第三個元素是: {}", value),
        None => println!("沒有第三個元素"),
    }
    
    // 處理越界情況
    let out_of_bounds = v.get(10);
    match out_of_bounds {
        Some(value) => println!("元素: {}", value),
        None => println!("索引越界了!"),
    }
    
    // 這會導致 panic!
    // let does_not_exist = &v[100];
}

💡溫馨提醒:當你確定索引有效時,可以使用 &v[index];但如果索引可能無效,務必使用 v.get(index) 來安全地處理。

迭代 Vector

fn main() {
    let v = vec![100, 32, 57];
    
    // 不可變參考迭代
    for item in &v {
        println!("值: {}", item);
    }
    
    // 可變參考迭代
    let mut v2 = vec![100, 32, 57];
    for item in &mut v2 {
        *item += 50;  // 解參考並修改值
    }
    println!("修改後: {:?}", v2);
    
    // 取得所有權的迭代
    for item in v {
        println!("擁有: {}", item);
    }
    // println!("{:?}", v);  // 錯誤!v 已被移動
}

Vector 與所有權

理解 Vector 的所有權行為非常重要:

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let first = &v[0];  // 不可變借用
    
    // v.push(6);  // 錯誤!不能在有不可變借用時進行可變操作
    
    println!("第一個元素: {}", first);
    
    // 現在可以修改了
    v.push(6);
    println!("完整向量: {:?}", v);
}

為什麼會這樣?因為當 Vector 空間不夠時,push 可能會重新分配記憶體,導致現有的參考失效。

儲存不同型別:使用枚舉

Vector 只能儲存相同型別的值,但我們可以用枚舉來變通:

#[derive(Debug)]
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}
fn main() {
    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];
    
    for cell in &row {
        match cell {
            SpreadsheetCell::Int(value) => println!("整數: {}", value),
            SpreadsheetCell::Float(value) => println!("浮點數: {}", value),
            SpreadsheetCell::Text(value) => println!("文字: {}", value),
        }
    }
}

💡 溫馨小提醒

雖然技術上可以用枚舉讓 Vector 儲存不同型別的值,但在實際開發中要謹慎使用!這種設計會讓程式碼變得複雜,每次操作都需要 match 來處理不同的型別,極度不好維護。

可以的話盡量不要這樣使用XD,太可怕了

我覺得更好的做法是設計的資料結構,或者考慮使用 trait 等等其他方案。記住:能做到 ≠ 應該這樣做!

String:處理文字的正確方式

在 Rust 中,字串比你想像的更複雜!Rust 區分了字串字面值 (&str) 和 String 型別。

建立 String

fn main() {
    // 從字串字面值建立
    let data = "initial contents";
    let s1 = data.to_string();
    let s2 = String::from("initial contents");
    
    // 空字串
    let mut s3 = String::new();
    
    // 從 &str 建立
    let s4 = "hello".to_string();
    
    println!("s1: {}, s2: {}, s4: {}", s1, s2, s4);
}

更新 String

String 可以動態增長和修改:

fn main() {
    let mut s = String::from("foo");
    
    // 附加字串切片
    s.push_str("bar");
    println!("push_str 後: {}", s);  // foobar
    
    // 附加單一字元
    s.push('!');
    println!("push 後: {}", s);  // foobar!
    
    // 使用 + 運算子連接
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2;  // s1 被移動,不能再使用
    println!("連接結果: {}", s3);
    // println!("{}", s1);  // 錯誤!s1 已被移動
    
    // 使用 format! 巨集(推薦)
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");
    let s = format!("{}-{}-{}", s1, s2, s3);
    println!("format! 結果: {}", s);
    // s1, s2, s3 都還可以使用!
}

字串切片與索引

重要:Rust 不允許直接用索引存取字串!

fn main() {
    // let hello = String::from("hello");
    // let h = hello[0];  // 編譯錯誤!
    
    // 正確的方式:使用切片
    let hello = "hello";  // "hello"
    let s = &hello[0..4];  // 取前 4 個位元組(注意不是字元!)
    println!("切片: {}", s);  // Зд
    
    // 安全的字元迭代
    for c in "hello".chars() {  // "hello"
        println!("字元: {}", c);
    }
    
    // 位元組迭代
    for b in "hello".bytes() {
        println!("位元組: {}", b);
    }
}

字串的實用方法

fn main() {
    let mut s = String::from("  Hello, Rust!  ");
    
    // 去除前後空白
    let trimmed = s.trim();
    println!("去空白: '{}'", trimmed);
    
    // 分割字串
    let data = "apple,banana,orange";
    let fruits: Vec<&str> = data.split(',').collect();
    println!("水果: {:?}", fruits);
    
    // 替換
    let replaced = s.replace("Rust", "World");
    println!("替換後: {}", replaced);
    
    // 檢查包含
    if s.contains("Rust") {
        println!("包含 Rust!");
    }
    
    // 轉換大小寫
    println!("大寫: {}", s.to_uppercase());
    println!("小寫: {}", s.to_lowercase());
    
    // 長度(位元組數,不是字元數!)
    println!("長度: {} 位元組", s.len());
    
    // 字元數
    println!("字元數: {}", s.chars().count());
}

HashMap:

HashMap 讓我們可以透過Key來查找Value,類似其他語言的字典或物件。

建立與使用 HashMap

use std::collections::HashMap;

fn main() {
    // 建立空的 HashMap
    let mut scores = HashMap::new();
    
    // 插入Key跟Value
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    
    println!("分數: {:?}", scores);
    
    // 使用 collect 從 vec 中建立出HashMap
    let teams = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];
    let scores: HashMap<_, _> = teams.into_iter()
        .zip(initial_scores.into_iter())
        .collect();
    
    println!("透過 collect 建立: {:?}", scores);
}

存取 HashMap 的值

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
    
    // 取得值
    let team_name = String::from("Blue");
    let score = scores.get(&team_name);
    match score {
        Some(s) => println!("Blue 隊得分: {}", s),
        None => println!("Blue 隊沒有分數"),
    }
    
    // 迭代HashMap
    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
}

HashMap 的所有權

HashMap 會取得其鍵和值的所有權:

use std::collections::HashMap;

fn main() {
    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");
    
    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // field_name 和 field_value 已被移動,不能再使用
    
    println!("地圖: {:?}", map);
    // println!("{}", field_name);  // 錯誤!已被移動
    
    // 如果使用參考,則需要確保生命週期
    let mut map2 = HashMap::new();
    let key = String::from("key");
    let value = String::from("value");
    map2.insert(&key, &value);  // 只要 key 和 value 的生命週期夠長就可以
    
    println!("key: {}, value: {}", key, value);  // 可以繼續使用
}

更新 HashMap

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    
    // 覆蓋值
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);  // 覆蓋前面的值
    println!("覆蓋後: {:?}", scores);
    
    // 只在Key不存在時插入
    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);  // Blue 已存在,不會插入
    println!("條件插入後: {:?}", scores);
    
    // 根據舊值更新
    let text = "hello world wonderful world";
    let mut map = HashMap::new();
    
    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }
    
    println!("單字計數: {:?}", map);
}

今天的收穫

今天我們深入學習了 Rust 的三大集合型別:

Vector (Vec<T>)

  • 動態大小的陣列,資料存放在堆積上
  • 支援 pushpopinsertremove 等操作
  • 兩種存取方式:索引 v[i] 與安全的 v.get(i)
  • 理解借用檢查在 Vector 操作中的影響

String

  • 可變長度的 UTF-8 字串
  • 與字串切片 &str 的區別與轉換
  • 字串連接的多種方式:+push_strformat!
  • 不能用索引直接存取,需要用迭代器處理字元

HashMap (HashMap<K, V>)

  • Key Value的雜湊表實現
  • Key和Value的所有權規則
  • entry API 的強大功能
  • 迭代與查詢操作

這些集合型別是 Rust 程式設計的基石,掌握它們的使用方式與特性,將大大提升你寫 Rust 程式的效率與品質。

今天的小挑戰

為了鞏固今天的學習,嘗試完成以下挑戰:

挑戰目標:建立一個簡單的圖書館管理系統

功能需求

  1. 書籍結構:包含 ISBN、書名、作者、出版年份、庫存數量
  2. 借閱記錄:追蹤誰借了哪本書、借閱日期
  3. 會員管理:會員 ID、姓名、聯絡方式、借閱歷史
  4. 核心功能
    • 新增/移除書籍
    • 註冊會員
    • 借書/還書操作
    • 查詢可借閱的書籍
    • 查詢會員借閱記錄
    • 產生統計報告(最受歡迎的書、最活躍的會員等)

技術要求

  • 使用 HashMap 管理書籍和會員資料
  • 使用 Vec 儲存借閱記錄
  • 使用 String 處理文字資料
  • 實現適當的錯誤處理
  • 撰寫測試函式驗證功能

加分項目

  • 實現書籍搜尋功能(按書名、作者、ISBN)
  • 支援批次操作
  • 實現資料的序列化與反序列化(提示:使用 serde

小提示:

use std::collections::HashMap;

#[derive(Debug, Clone)]
struct Book {
    isbn: String,
    title: String,
    author: String,
    year: u32,
    stock: u32,
}

#[derive(Debug)]
struct Member {
    id: u32,
    name: String,
    email: String,
    borrowed_books: Vec<String>, // 存放 ISBN
}

#[derive(Debug)]
struct BorrowRecord {
    member_id: u32,
    isbn: String,
    borrow_date: String, // 簡化為字串
    return_date: Option<String>,
}

struct Library {
    books: HashMap<String, Book>,     // ISBN -> Book
    members: HashMap<u32, Member>,    // MemberID -> Member
    borrow_records: Vec<BorrowRecord>,
}

// 你來實現 Library 的方法!

這個挑戰會讓你綜合運用今天學到的所有集合型別,並練習設計更複雜的資料結構。記住,重點不是完美的實現,而是理解如何選擇和組合不同的集合型別來解決實際問題。

明天我們將學習 Rust 的錯誤處理機制,探討如何優雅地處理程式執行中可能遇到的各種錯誤情況。這對建立健壯、可靠的應用程式至關重要!

如果在實作過程中遇到任何問題,歡迎大家在留言區一起來討論!

那我們明天見!


上一篇
Day 7: 第一週回顧與 Cargo 工具鏈:掌握 Rust 開發的瑞士軍刀
下一篇
Day 9: 錯誤處理 (Error Handling):從 `panic!` 到 `Result`
系列文
大家一起跟Rust當好朋友吧!18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言