嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第八天!
恭喜你進入第二週!經過第一週的紮實基礎訓練,相信你對 Rust 的核心概念已經有了不錯的理解。今天我們要進入一個非常實用的主題:集合型別。
在第一週,我們學過陣列 [1, 2, 3, 4]
,但它有個限制:大小必須在編譯時確定。在實際開發中,我們經常需要處理大小會動態變化的資料,比如用戶輸入的文字、從資料庫查詢的結果、或是網路 API 回傳的資料List。
今天我們將學習 Rust 標準函式庫提供的三個最重要的集合型別:
Vec<T>
:可動態增長的陣列String
:可變長度的 UTF-8 字串HashMap<K, V>
:Key Value 的 雜湊表這些集合型別的共同特點是:它們的資料都儲存在Heap上,可以在執行時動態調整大小。掌握它們是寫出實用 Rust 程式的關鍵!
Vec<T>
):最實用的動態陣列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
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 提供兩種讀取元素的方式,各有其使用場景:
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)
來安全地處理。
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 的所有權行為非常重要:
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 等等其他方案。記住:能做到 ≠ 應該這樣做!
在 Rust 中,字串比你想像的更複雜!Rust 區分了字串字面值 (&str
) 和 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 可以動態增長和修改:
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 讓我們可以透過Key來查找Value,類似其他語言的字典或物件。
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);
}
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 會取得其鍵和值的所有權:
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); // 可以繼續使用
}
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>
):
push
、pop
、insert
、remove
等操作v[i]
與安全的 v.get(i)
String:
&str
的區別與轉換+
、push_str
、format!
HashMap (HashMap<K, V>
):
entry
API 的強大功能這些集合型別是 Rust 程式設計的基石,掌握它們的使用方式與特性,將大大提升你寫 Rust 程式的效率與品質。
為了鞏固今天的學習,嘗試完成以下挑戰:
挑戰目標:建立一個簡單的圖書館管理系統
功能需求:
技術要求:
HashMap
管理書籍和會員資料Vec
儲存借閱記錄String
處理文字資料加分項目:
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 的錯誤處理機制,探討如何優雅地處理程式執行中可能遇到的各種錯誤情況。這對建立健壯、可靠的應用程式至關重要!
如果在實作過程中遇到任何問題,歡迎大家在留言區一起來討論!
那我們明天見!