Rust 中的 HashMap 實戰指南:理解與最佳化技巧

寻月隐君發表於2024-10-09

Rust 中的 HashMap 實戰指南:理解與最佳化技巧

在 Rust 程式設計中,HashMap 是一個強大的鍵值對資料結構,廣泛應用於資料統計、資訊儲存等場景。在本文中,我們將透過三個實際的程式碼示例,詳細講解 HashMap 的基本用法以及如何在真實專案中充分利用它。此外,我們還將探討 Rust 的所有權系統對 HashMap 的影響,並分享避免常見陷阱的技巧。

本文透過三個 Rust 實戰例子,展示了 HashMap 的基本用法及其在實際場景中的應用。我們將從簡單的水果籃子示例出發,逐步演示如何使用 HashMap 儲存和處理不同資料,並透過新增測試用例來確保程式碼的正確性。此外,我們還會深入探討 Rust 所有權系統對 HashMap 使用的影響,尤其是如何避免所有權轉移的問題。

實操

示例一:使用 HashMap 儲存水果籃子

// hashmaps1.rs
//
// A basket of fruits in the form of a hash map needs to be defined. The key
// represents the name of the fruit and the value represents how many of that
// particular fruit is in the basket. You have to put at least three different
// types of fruits (e.g apple, banana, mango) in the basket and the total count
// of all the fruits should be at least five.
//
// Make me compile and pass the tests!
//
// Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a
// hint.

use std::collections::HashMap;

fn fruit_basket() -> HashMap<String, u32> {
    let mut basket = HashMap::new(); // TODO: declare your hash map here.

    // Two bananas are already given for you :)
    basket.insert(String::from("banana"), 2);

    // TODO: Put more fruits in your basket here.
    basket.insert(String::from("apple"), 3);
    basket.insert(String::from("mango"), 4);
    basket.insert(String::from("orange"), 5);
    basket
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn at_least_three_types_of_fruits() {
        let basket = fruit_basket();
        assert!(basket.len() >= 3);
    }

    #[test]
    fn at_least_five_fruits() {
        let basket = fruit_basket();
        assert!(basket.values().sum::<u32>() >= 5);
    }
}

在 本例中,我們構建了一個水果籃子,並透過 HashMap 來儲存水果種類及其數量。

透過測試,我們驗證了籃子中至少有三種水果,並且總數超過五個。

示例二:不重複新增水果

// hashmaps2.rs
//
// We're collecting different fruits to bake a delicious fruit cake. For this,
// we have a basket, which we'll represent in the form of a hash map. The key
// represents the name of each fruit we collect and the value represents how
// many of that particular fruit we have collected. Three types of fruits -
// Apple (4), Mango (2) and Lychee (5) are already in the basket hash map. You
// must add fruit to the basket so that there is at least one of each kind and
// more than 11 in total - we have a lot of mouths to feed. You are not allowed
// to insert any more of these fruits!
//
// Make me pass the tests!
//
// Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a
// hint.

use std::collections::HashMap;

#[derive(Hash, PartialEq, Eq)]
enum Fruit {
    Apple,
    Banana,
    Mango,
    Lychee,
    Pineapple,
}

fn fruit_basket(basket: &mut HashMap<Fruit, u32>) {
    let fruit_kinds = vec![
        Fruit::Apple,
        Fruit::Banana,
        Fruit::Mango,
        Fruit::Lychee,
        Fruit::Pineapple,
    ];

    for fruit in fruit_kinds {
        // TODO: Insert new fruits if they are not already present in the
        // basket. Note that you are not allowed to put any type of fruit that's
        // already present!
        *basket.entry(fruit).or_insert(1);
        // 如果水果不在籃子中,則插入數量為1的該水果
        // if !basket.contains_key(&fruit) {
        //     basket.insert(fruit, 1);
        // }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Don't modify this function!
    fn get_fruit_basket() -> HashMap<Fruit, u32> {
        let mut basket = HashMap::<Fruit, u32>::new();
        basket.insert(Fruit::Apple, 4);
        basket.insert(Fruit::Mango, 2);
        basket.insert(Fruit::Lychee, 5);

        basket
    }

    #[test]
    fn test_given_fruits_are_not_modified() {
        let mut basket = get_fruit_basket();
        fruit_basket(&mut basket);
        assert_eq!(*basket.get(&Fruit::Apple).unwrap(), 4);
        assert_eq!(*basket.get(&Fruit::Mango).unwrap(), 2);
        assert_eq!(*basket.get(&Fruit::Lychee).unwrap(), 5);
    }

    #[test]
    fn at_least_five_types_of_fruits() {
        let mut basket = get_fruit_basket();
        fruit_basket(&mut basket);
        let count_fruit_kinds = basket.len();
        assert!(count_fruit_kinds >= 5);
    }

    #[test]
    fn greater_than_eleven_fruits() {
        let mut basket = get_fruit_basket();
        fruit_basket(&mut basket);
        let count = basket.values().sum::<u32>();
        assert!(count > 11);
    }

    #[test]
    fn all_fruit_types_in_basket() {
        let mut basket = get_fruit_basket();
        fruit_basket(&mut basket);
        for amount in basket.values() {
            assert_ne!(amount, &0);
        }
    }
}

在上面的示例程式碼中,我們透過 HashMap 儲存多個水果,但避免重複新增已有的水果種類。

測試用例驗證了我們不會修改已存在的水果,並確保總數超過 11 個。

示例三:記錄比賽比分

// hashmaps3.rs
//
// A list of scores (one per line) of a soccer match is given. Each line is of
// the form : "<team_1_name>,<team_2_name>,<team_1_goals>,<team_2_goals>"
// Example: England,France,4,2 (England scored 4 goals, France 2).
//
// You have to build a scores table containing the name of the team, goals the
// team scored, and goals the team conceded. One approach to build the scores
// table is to use a Hashmap. The solution is partially written to use a
// Hashmap, complete it to pass the test.
//
// Make me pass the tests!
//
// Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a
// hint.

use std::collections::HashMap;

// A structure to store the goal details of a team.
struct Team {
    goals_scored: u8,
    goals_conceded: u8,
}

fn build_scores_table(results: String) -> HashMap<String, Team> {
    // The name of the team is the key and its associated struct is the value.
    let mut scores: HashMap<String, Team> = HashMap::new();

    for r in results.lines() {
        let v: Vec<&str> = r.split(',').collect();
        let team_1_name = v[0].to_string();
        let team_1_score: u8 = v[2].parse().unwrap();
        let team_2_name = v[1].to_string();
        let team_2_score: u8 = v[3].parse().unwrap();
        // TODO: Populate the scores table with details extracted from the
        // current line. Keep in mind that goals scored by team_1
        // will be the number of goals conceded from team_2, and similarly
        // goals scored by team_2 will be the number of goals conceded by
        // team_1.

        // 更新 team_1 的資料
        let team_1 = scores.entry(team_1_name.clone()).or_insert(Team {
            goals_scored: 0,
            goals_conceded: 0,
        });
        team_1.goals_scored += team_1_score;
        team_1.goals_conceded += team_2_score;

        // 更新 team_2 的資料
        let team_2 = scores.entry(team_2_name.clone()).or_insert(Team {
            goals_scored: 0,
            goals_conceded: 0,
        });
        team_2.goals_scored += team_2_score;
        team_2.goals_conceded += team_1_score;
    }
    scores
}

#[cfg(test)]
mod tests {
    use super::*;

    fn get_results() -> String {
        let results = "".to_string()
            + "England,France,4,2\n"
            + "France,Italy,3,1\n"
            + "Poland,Spain,2,0\n"
            + "Germany,England,2,1\n";
        results
    }

    #[test]
    fn build_scores() {
        let scores = build_scores_table(get_results());

        let mut keys: Vec<&String> = scores.keys().collect();
        keys.sort();
        assert_eq!(
            keys,
            vec!["England", "France", "Germany", "Italy", "Poland", "Spain"]
        );
    }

    #[test]
    fn validate_team_score_1() {
        let scores = build_scores_table(get_results());
        let team = scores.get("England").unwrap();
        assert_eq!(team.goals_scored, 5);
        assert_eq!(team.goals_conceded, 4);
    }

    #[test]
    fn validate_team_score_2() {
        let scores = build_scores_table(get_results());
        let team = scores.get("Spain").unwrap();
        assert_eq!(team.goals_scored, 0);
        assert_eq!(team.goals_conceded, 2);
    }
}

本示例展示了 HashMap 在複雜場景中的應用,如記錄足球比賽的比分。我們透過 HashMap 將每支球隊的得分和失分進行統計。並透過測試來驗證比分記錄是否正確。

思考

1. 為什麼要用 team_1_name.clone()

在 Rust 中,String 是一個擁有所有權的型別,意味著它的值在預設情況下會被移動,而不是複製。如果你直接使用 team_1_name 作為 HashMap 的鍵,那麼當你呼叫 entry(team_1_name) 時,team_1_name 的所有權會被移動到 entry() 函式中。

之後,如果你還想使用 team_1_name,就無法訪問它了,因為所有權已經被移動了。這時你需要透過 clone() 建立一個新的副本(淺複製),這樣你可以保留原始的 String

使用 .clone() 的目的是避免所有權轉移而導致變數不可用。

示例:

let team_1_name = "England".to_string();
// 所有權被移動給 entry(),你不能再訪問 team_1_name
scores.entry(team_1_name); 

// 如果你還想用 team_1_name,就要使用 clone():
scores.entry(team_1_name.clone());

如果 team_1_name 是一個 &str(即字串切片,通常是不可變引用),那麼你就不需要 clone(),因為引用型別不涉及所有權的移動問題。

2. 為什麼不用結構體直接初始化,而是用累加的方式?

在每場比賽的過程中,某個隊伍可能會多次出現,例如:

  • 比賽1:England 對 France
  • 比賽2:Germany 對 England

我們需要在 HashMap 中更新每個隊伍的進球和失球資訊,而不是每次都覆蓋已有資料。因此,我們不能每次都用新的結構體初始化,而是要先檢查該隊伍是否已經在 HashMap 中存在,然後累加其資料。

這裡用的是 entry() 方法,它的作用是:

  • 如果 team_1_name 還沒有在 HashMap 中出現,就插入一個新的 Team 結構體,並初始化進球和失球為 0。
  • 如果 team_1_name 已經在 HashMap 中了,那麼直接獲取它對應的 Team 結構體,並更新其 goals_scoredgoals_conceded 欄位。

透過這種方式,每次遇到相同隊伍時,不會重新初始化,而是將新的進球和失球數累加到已有資料中。

let team_1 = scores.entry(team_1_name.clone()).or_insert(Team {
    goals_scored: 0,
    goals_conceded: 0,
});

// 累加進球和失球
team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score;

這樣就能保證每個隊伍的分數在不同比賽中是累積的,而不是被覆蓋掉。

思考總結

  • team_1_name.clone() 是為了避免移動所有權導致變數不可用。
  • 累加進球數和失球數 是因為一個隊伍可能會出現在多場比賽中,不能每次都重新初始化資料,而是要在已有的基礎上進行更新。

這兩者結合起來,能確保正確跟蹤每個隊伍的進球和失球情況。

總結

透過這三個 HashMap 的實戰示例,我們不僅掌握瞭如何高效地使用 HashMap 儲存和運算元據,還深入理解了 Rust 的所有權與借用規則在實際開發中的應用。Rust 強調所有權的管理,尤其是在處理複雜資料結構如 HashMap 時,準確掌控所有權的轉移和資料的引用關係至關重要,這不僅能夠提高程式碼的效率,還能保障程式的安全性和穩定性。

這些實踐展示了 HashMap 在解決實際問題中的強大能力,尤其在需要頻繁查詢、插入和更新資料的場景中。熟練掌握 HashMap 的使用技巧,將極大提升我們在 Rust 開發中的資料管理效率與程式效能。

參考

  • https://www.rust-lang.org/zh-CN
  • https://crates.io/
  • https://course.rs/about-book.html
  • https://course.rs/basic/crate-module/module.html
  • https://users.rust-lang.org/
  • https://lab.cs.tsinghua.edu.cn/rust/slides/05-org-lib.pdf
  • https://github.com/rust-lang
  • https://rustmagazine.github.io/rust_magazine_2022/Q1/lang.html
  • https://github.com/QMHTMY/RustBook

相關文章