How to properly update struct values in a Vec field

use std::time::Instant;
use std::collections::HashMap;
use core::cell::RefCell;

struct Focus(/*start*/ Instant, /*end*/ Instant);

#[derive(Default)]
struct Session {
    pid: u32,
    had_focus: Vec<Focus>,
    run_time: u64,
}

#[derive(Default)]
struct Game {
    name: String,
    focus_time_total: u64,
    focus_time_7_days: u64,
    run_time_total: u64,
    run_time_7_days: u64,
    sessions: RefCell<Vec<Session>>,
}

fn main() {
    let mut games: HashMap<String, Game> = HashMap::new();
    
    // ...
    
    let name = "some game";
    let pid = 123;
    
    if !games.contains_key(name) {
        let g = Game {
            name: name.to_string(),
            focus_time_total: 0,
            focus_time_7_days: 0,
            run_time_total: 0,
            run_time_7_days: 0,
            sessions: Default::default(),
        };

        games.insert(name.to_string(), g);
    }
    
    // if there's no session for this pid, create one, then update the session info
    let mut sessions = games.get(name).unwrap().sessions.borrow_mut();
    let last_session = sessions.last();

    if last_session.is_none() || last_session.unwrap().pid != pid {
        let session = Session {
            pid: pid,
            had_focus: vec![Focus(Instant::now(), Instant::now())],
            run_time: 0,
        };

        sessions.push(session);
    }
    
    let mut current_session = &mut sessions.last().unwrap();
    current_session.run_time = 123; // breaks
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0594]: cannot assign to `current_session.run_time`, which is behind a `&` reference
  --> src/main.rs:60:5
   |
60 |     current_session.run_time = 123;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign

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

I'm new to Rust and have been struggling to get struct vector fields to do what I want them to do. I have a struct, Game with a vector of Sessions. I want to push a Session if one doesn't exist and update one if it does exist.

The use of RefCell on Game::sessions also feels wrong to me for some reason. I feel like I'm trying to force Rust to operate under the paradigms I'm used to instead of doing things the "Rust way".

Any help?

Many thanks in advance.

The error is because of a common mistake that it will be useful to learn to spot: in order to mutate something (when not using interior mutability), the entire “path” of operations from the original owned value must not involve any & references, only &mut references. The common way this shows up obviously is borrowing a field with & somewhere, but in your case, you called sessions.last() (which returns an & reference) when you needed sessions.last_mut().

With this change, your program compiles:

let current_session = sessions.last_mut().unwrap();

Note that no muts are needed to make this successfully mutable, except for last_mut() itself. The &mut you had before doesn't change anything about the mutability; you can only ever remove mutability by omitting a mut, not add it by adding &mut.


Similarly, in order to fix the situation where you used RefCell, you need to write get_mut() and last_mut() instead of get() and last():

struct Game {
    ...
    sessions: Vec<Session>,
}

...

let sessions = &mut games.get_mut(name).unwrap().sessions;
let last_session = sessions.last_mut();

As to “get or insert if absent” patterns, for a HashMap you can use Entry to do that; for a Vec the easiest thing will probably be to just check is_empty().

3 Likes

@kpreid My code just got so much cleaner--more importantly, it works. Thank you!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.