Guillaume
Guillaume

Reputation: 33

Pass mutable reference to an element of a vector as parameter of function alongside with that vector

I'm coding a little game; I have a vector of monsters and a monster can deal damage to all other monsters. I need a function that takes the vector of all monsters and a reference to the monster that is attacking.

I understand why this is not possible with the type system of Rust: I need a mutable reference to the vector and a mutable reference to the monster, but this is not possible because the monster belongs to the vector. I can not find a workaround.

struct Monster {
    life: i32,
}

// give 1 damage to all except me
// increase my life by 1
fn give_1_damage(me: &mut Monster, all: &mut Vec<Monster>) {

    for m in all {
        m.life -= 1;
    }
    me.life += 2;
}

fn main() {
    let mut a = vec![Monster { life: 3 }, Monster { life: 3 }];
    let ref mut b = &mut a[0];
    give_1_damage(b, &mut a);
}

Lukas Kalbertodt proposes to pass the offset of the monster in the vector. This works great, thanks! But in fact my code is more complicated:

struct Monster {
    life: i32,
}

struct Game {
    player1: Vec<Monster>,
    player2: Vec<Monster>,
}

fn give_1_damage(player_idx: usize, monster_idx: usize, game: &mut Game) {
    // ...
}

I know that it's possible to pass the index of the player, and the index of the monster, but I found this is ugly. Do I really need to pass the player_idx and the monster_idx whereas I previously know which monster is attacking and I can have a reference to it?

Upvotes: 3

Views: 1858

Answers (2)

Lukas Kalbertodt
Lukas Kalbertodt

Reputation: 88636

Welcome to Rust! As you already noticed, the Rust type system (more specifically: the borrow checker) won't let you do that. In Rust, you have to think about certain problems in a different way than you might be used to. And this is actually a good idea.

For example, let's take a look at your code: in order to increase the "life" by 1, you increase it by 2 outside the loop, because the loop will decrease it by 1. This is not a good way to write code like that, because now different parts of your code are semantically connected, although they shouldn't be.

So how can you do it in Rust?

There are multiple ways to do that, but what about this (explanation in the comments):

#[derive(Debug)]
struct Monster {
    life: i32,
}

// Instead of passing a mutable reference, we just pass the index of the 
// attacking monster in the vector.
// Note that I also pass `&mut [...]` instead of `&mut Vec<...>`. This is
// sufficient as long as you only want to mutate single elements.
fn give_1_damage(me_idx: usize, all: &mut [Monster]) {
    // Here we split the vector in three parts: the part from the
    // beginning to the attacking monster, the attacking monster itself,
    // and the rest of the slice.
    let (front, me, back) = {
        // This is done by using those two special methods of slices.
        let (first, second) = all.split_at_mut(me_idx);
        let (me, rest) = second.split_first_mut().unwrap();
        (first, me, rest)
    };

    // Here we chain together the two parts and iterate over all monsters
    // except for the attacking one!
    for m in front.into_iter().chain(back) {
        m.life -= 1;
    }
    me.life += 1;
}

fn main() {
    let mut a = vec![Monster { life: 3 }, Monster { life: 3 }];
    give_1_damage(0, &mut a);
    println!("{:?}", a);
}

You can try it here.

Upvotes: 3

DK.
DK.

Reputation: 59005

The simplest way would be to pass an index rather than a reference:

#[derive(Debug)]
struct Monster {
    life: i32,
}

// give 1 damage to all except me
// increase my life of 1
fn give_1_damage(monsters: &mut [Monster], me: usize) {
    for (i, m) in monsters.iter_mut().enumerate() {
        if i == me {
            m.life += 1;
        } else {
            m.life -= 1;
        }
    }
}

fn main() {
    let mut a = vec![Monster{life: 3}, Monster{life: 3}];
    println!("a: {:?}", a);
    let b = 0;
    give_1_damage(&mut a, b);
    println!("a: {:?}", a);
}

Also, since you're not adding anything to the list of monsters, you should take a &mut [Monster] rather than &mut Vec<Monster>; it's more general that way.

Upvotes: 2

Related Questions