透過向量儲存列表

我們第一個要來看的集合是 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 型別的元素。

在更實際的程式碼中,當你插入數值時,Rust 通常都能推導出型別來。所以你不太常會需要指明型別詮釋。建立 Vec<T> 的同時進行初始化是很常見的,為此 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>

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

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

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

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

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

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

讀取向量元素

現在你知道如何建立、更新與刪除向量,接下來就是要知道如何讀取他們的內容了。要引用向量儲存的數值有兩種方式。為了更加清楚說明此範例,我們詮釋了函式回傳值的型別。

範例 8-5 顯示了取得向量中數值的方法,可以是用索引語法或者 get 方法。

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

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

    match v.get(2) {
        Some(third) => println!("第三個元素為 {}", third),
        None => println!("第三個元素並不存在。"),
    }
}

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

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

Rust 有兩種取得元素引用的方式,所以能以此決定程式的行為。像是當你使用了一個索引但向量卻沒有對應的元素的情況。讓我們看看一個範例,我們有一個向量擁有五個元素,但我們嘗試用索引 100 來取得對應數值,如範例 8-6 所示。

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

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

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

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

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

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

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

    let first = &v[0];

    v.push(6);

    println!("第一個元素為 {}", first);
}

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

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

$ 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

error: aborting due to previous error

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

To learn more, run the command again with --verbose.

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

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

遍歷向量的元素

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

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

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

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

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

範例 8-9:遍歷向量中的元素取得可變引用

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

使用枚舉來儲存多種型別

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

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

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-10:用 enum 定義儲存不同型別的枚舉並作為向量的型別

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

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

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