ruststructhashmapdereference

My Rust OR_INSERT Hashmap code to update a struct content works without dereferencing. Why?


From Rust documentation, this count variable wouldn't work without dereferencing (*)

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);

However, I have the following code which tries to update a u8 variable (i.e team_val.goals_scored ) in a Struct if the key is found in a hashmap. It works without dereferencing. My understanding from the above Rust documentation was I need to dereference the team_val.goals_scored in order to update the content of the struct which is also a value for the hash map. Whelp!

My code:

#[derive(Debug)]
struct Team {
    name: String,
    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 number of goals conceded from team_2, and similarly
        // goals scored by team_2 will be the number of goals conceded by
        // team_1.

        let mut team_1_struct= Team {
            name: team_1_name.clone(),
            goals_scored: team_1_score,
            goals_conceded: team_2_score
        };

        let mut team_2_struct= Team {
            name: team_2_name.clone(),
            goals_scored: team_2_score,
            goals_conceded: team_1_score
        };

        if scores.contains_key(&team_1_name) {
            let team_val = scores.entry(team_1_name.clone()).or_insert(team_1_struct);
            println!("Yooo {:#?}",team_val);

            team_val.goals_scored +=team_1_score;
            team_val.goals_conceded += team_2_score;
        } else {
            scores.insert(team_1_name,team_1_struct);
        }

        if scores.contains_key(&team_2_name) {
            let team_val = scores.entry(team_2_name.clone()).or_insert(team_2_struct);
            println!("Yooo {:#?}",team_val);

            team_val.goals_scored +=team_2_score;
            team_val.goals_conceded += team_1_score;
        } else {
            scores.insert(team_2_name,team_2_struct);
        }

        
    }
    scores
}

Solution

  • Rust does some automatic dereferencing, described here. We can see the difference between the documentation code and what you wrote:

    // This
    *count += 1
    // versus this
    team_val.goals_scored += team_1_score
            ^--- Causes an automatic dereferencing
    

    If you're coming from C I think this documentation may be even clearer.

    Answering the follow-up question 'can you use entry() without using clone() on the key' - you cannot. entry() consumes what you send it, and doesn't give it back, so the borrow checker will prevent you from doing this. It's currently a 'limitation' on the API - but if you're dealing with something as cheap as a short string to copy, then this shouldn't impact you much.

    You can do a fair amount to slim down your code, though (with the caveat that I only did this for one team - it's easily extensible):

    use std::collections::HashMap;
    
    struct Team {
        name: String,
        goals: u8
    }
    
    type Scorecard = HashMap<String, Team>;
    
    fn scoring(records: String) -> Scorecard {
        let mut scores : Scorecard = HashMap::new();
    
        for r in records.lines() {
            let record: Vec<&str> = r.split(',').collect();
            let team_name = record[0].to_string();
            let team_score: u8 = record[1].parse().unwrap();
    
            // Note that we do not need to create teams here on every iteration. 
            // There is a chance they already exist, and we can create them only if 
            // the `or_insert()` clause is activated.
    
            // Note that the `entry()` clause when used with `or_insert()`
            // gives you an implicit 'does this key exist?' check.
            let team = scores.entry(team_name.clone()).or_insert(Team {
                name: team_name,
                goals: 0,
            });
            team.goals += team_score;
    
        }
    
        scores
    }
    
    fn main() {
        let record = "Thunderers,1\nFlashians,1\nThunderers,2";
        let scores = scoring(record.to_string());
        for (key, value) in &scores {
            println!("{}: The mighty {} scored {} points.", key, value.name, value.goals)
        }
    
    
        // Flattening Some Options!
        // let o = Some(Some(5));
        // let p = Some(5);
        // println!("o: {:#?}", o);
        // println!("p: {:#?}", p);
        // println!("flattened o: {:#?}", o.flatten());
        // println!("flattened p: {:#?}", p.flatten());
    }