透過向量儲存列表

我們第一個要來看的集合是 Vec<T> 常稱為向量(vector)。向量允許你在一個資料結構儲存不止一個數值,而且該結構的記憶體會接連排列所有數值。它們很適合用來處理你手上的項目列表,像是一個檔案中每行的文字,或是購物車內每項物品。

建立新的向量

要建立一個新的空向量的話,我們呼叫 Vec::new 函式,如範例 8-1 所示。

fn main() {
    let v: Vec<i32> = Vec::new();
}

範例 8-1 建立一個儲存數值型別為 i32 的空向量

注意到我們在此加了型別詮釋。因為我們沒有對此向量插入任何數值,Rust 不知道我們想儲存什麼類型的元素。這是一項重點,向量是用泛型(generics)實作,我們會在第十章說明如何為你自己的型別使用泛型。現在我們只需要知道標準函式庫提供的 Vec<T> 型別可以持有任意型別,然後當特定向量要持有特定型別時,我們可以在尖括號內指定該型別。在範例 8-1,我們告訴 Rust 在 v 中的 Vec<T> 會持有 i32 型別的元素。

不過通常你在建立 Vec<T> 時只需要給予初始數值,Rust 就能推導出你想儲存的數值型別,所以你不太常會需要指明型別詮釋。Rust 還提供了 vec! 巨集讓我們能方便地建立一個新的向量並取得你提供的數值。在範例 8-2 中,我們建立了一個新的 Vec<i32> 並擁有數值 123。整數型別為 i32 是因為這是預設整數型別,如同我們在第三章的「資料型別」 段落提到的一樣。

fn main() {
    let v = vec![1, 2, 3];
}

範例 8-2:建立一個擁有數值的新向量

因為我們給予了初始的 i32 數值,Rust 可以推導出 v 的型別為 Vec<i32>,所以型別詮釋就不是必要的了。接下來,讓我們看看如何修改向量。

更新向量

要在建立向量之後新增元素的話,我們可以使用 push 方法,如範例 8-3 所示。

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}

範例 8-3:使用 push 方法來新增數值到向量

與其他變數一樣,如果我們想要變更其數值的話,我們需要使用 mut 關鍵字使它成為可變的,如同第三章提到的一樣。我們插入的數值所屬型別均為 i32,然後 Rust 可以從資料推導,所以我們不必指明 Vec<i32>

讀取向量元素

要參考向量儲存的數值有兩種方式。為了更加清楚說明此範例,我們詮釋了函式回傳值的型別。

範例 8-4 顯示了取得向量中數值的方法,可以使用索引語法與 get 方法。

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("第三個元素是 {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("第三個元素是 {third}"),
        None => println!("第三個元素並不存在。"),
    }
}

範例 8-4:使用索引語法或 get 方法來取得向量項目

這邊我們要注意一些地方。我們使用了索引數值 2 來獲取第三個元素:向量可以用數字來索引,從零開始計算。使用 &[] 會給我們索引數值的元素參考,而使用 get 方法加上一個索引作為引數,則會給我們 Option<&T>,我們可以用 match 來配對。

Rust 提供兩種取得元素參考方式,所以當你嘗試使用索引數值取得向量範圍外的元素時,你可以決定程式的行為。讓我們看看一個範例,我們有一個向量擁有五個元素,但我們嘗試用索引 100 來取得對應數值,如範例 8-5 所示。

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}

範例 8-5:嘗試對只有五個元素的向量取得索引 100 的值

當我們執行程式時,第一個 [] 方法會讓程式恐慌,因為它參考了不存在的元素。此方法適用於當你希望一有無效索引時就讓程式崩潰的狀況。

當你使用 get 方法來索取向量不存在的索引時,它會回傳 None 而不會恐慌。如果正常情況下偶而會不小心存取超出向量範圍索引的話,你就會想要只用此方法。你的程式碼就會有個邏輯專門處理 Some(&element)None,如同第六章所述。舉例來說,可能會有由使用者輸入的索引。如果他不小心輸入太大的數字的話,程式可以回傳 None,你可以告訴使用者目前向量有多少項目,並讓他們可以再輸入一次。這會比直接讓程式崩潰還來的友善,他們可能只是不小心打錯而已!

當程式有個有效參考時,借用檢查器(borrow checker)會貫徹所有權以及借用規則(如第四章所述)來確保此參考及其他對向量內容的參考都是有效的。回想一下有個規則是我們不能在同個作用域同時擁有可變與不可變參考。這個規則一樣適用於範例 8-6,在此我們有一個向量第一個元素的不可變參考,然後我們嘗試在向量後方新增元素。如果我們嘗試在此動作後繼續使用第一個參考的話,程式會無法執行:

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("第一個元素是:{first}");
}

範例 8-6:在持有一個項目的參考時,還嘗試對向量新增元素

編譯此程式會得到以下錯誤:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("第一個元素為 {first}");
  |                           ----- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error

範例 8-6 的程式碼看起來好像能執行。為何第一個元素的參考要在意向量的最後端發生了什麼事呢?此錯誤其實跟向量運作的方式有關:由於向量會將元素放在前一位的記憶體位置後方,在向量後方新增元素時,如果當前向量的空間不夠再塞入另一個值的話,可能會需要配置新的記憶體並複製舊的元素到新的空間中。這樣一來,第一個元素的索引可能就會指向已經被釋放的記憶體,借用規則會防止程式遇到這樣的情形。

注意:關於 Vec<T> 型別更多的實作細節,歡迎查閱「The Rustonomicon」

遍歷向量的元素

想要依序存取向量中每個元素的話,我們可以遍歷所有元素而不必用索引一個一個取得。範例 8-7 闡釋了如何使用 for 迴圈來取得一個 i32 向量中每個元素的不可變參考並印出他們。

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}

範例 8-7:使用 for 迴圈遍歷向量中每個元素

我們還可以遍歷可變向量中的每個元素取得可變參考來改變每個元素。像是範例 8-8 就使用 for 迴圈來為每個元素加上 50

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

範例 8-8:遍歷向量中的元素取得可變參考

要改變可變參考指向的數值,在使用 += 運算子之前,我們需要使用 * 解參考運算子來取得 i 的數值。我們會在第十五章的「追蹤指標的數值」段落來講解更多解參考運算子的細節。

當遍歷向量時,無論是不可變或可變地都是安全,因為借用檢查器的規則能確保如此。如果我們嘗試在範例 8-7 或範例 8-8 的 for 迴圈本體插入或刪除項目,我們就會獲得和範例 8-6 程式碼類似的編譯錯誤。for 迴圈持有的向量參考能避免同時修改整個向量。

使用列舉來儲存多種型別

向量只能儲存同型別的數值,這在某些情況會很不方便,一定會有場合是要儲存不同型別到一個列表中。幸運的是,列舉的變體是定義在相同的列舉型別,所以當我們需要在向量儲存不同型別的元素時,我們可以用列舉來定義!

舉例來說,假設我們想從表格中的一行取得數值,但是有些行內的列會包含整數、浮點數以及一些字串。我們可以定義一個列舉,其變體會持有不同的數值型別,然後所有的列舉變體都會被視為相同型別:就是它們的列舉。接著我們就可以建立一個擁有此列舉型別的向量,最終達成持有不同型別。如範例 8-9 所示。

fn main() {
    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("藍色")),
        SpreadsheetCell::Float(10.12),
    ];
}

範例 8-9:用 enum 定義儲存不同型別的列舉並作為向量的型別

Rust 需要在編譯時期知道向量的型別以及要在堆積上用到多少記憶體才能儲存每個元素。我們必須明確知道哪些型別可以放入向量中。如果 Rust 允許向量一次持有任意型別的話,在對向量中每個元素進行處理時,可能就會有一或多種型別會產生錯誤。使用列舉和 match 表達式讓 Rust 可以在編譯期間確保每個可能的情形都已經處理完善了,如同第六章提到的一樣。

如果你無法確切知道執行時程式所處理的所有型別的話,列舉就不管用了。這時使用特徵物件會比較好,我們會在第十七章再來解釋。

現在我們已經講了一些向量常見的用法,有時間的話記得到向量的 API 技術文件瞭解標準函式庫中 Vec<T> 所有實用的方法。舉例來說,除了 push 方法以外,還有個 pop 方法可以移除並回傳最後一個元素。

釋放向量的同時也會釋放其元素

就像其它 struct 一樣,向量會在作用域結束時被釋放,如範例 8-10 所示。

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // 使用 v 做些事情
    } // <- v 在此離開作用域並釋放
}

範例 8-10:顯示向量及其元素在哪裡被釋放

當向量被釋放時,其所有內容也都會被釋放,代表它持有的那些整數都會被清除。這雖然聽起來很直觀,但是當我們開始參考向量中的元素時可能就會變得有點複雜。讓我們看看怎麼處理這種情形吧!

接下來讓我們看看下一個集合型別:String