Временная ссылка на внешний объект

Привет.

Есть некоторая игра, состояние которой описывается структурой GameState. Искусственные игроки (боты) ходят по-очереди, что-то вычисляют и результатом вычислений является какой-то Move, поведение ботов описывается простым трейтом Bot:

#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Move {
    Right, Up, Left, Down, Stop,
}

// the bot is mutable agent that makes a decision about the move
trait Bot {
    fn reset(&mut self);
    fn do_move(&mut self, game_state: &GameState) -> Move;
}

Реализация do_move может быть достаточно сложной, и в этой реализации множеству различных функций нужен доступ к этому game_state. Теоретически можно протянуть game_state как параметр во все функции, но это очень громоздко. Поэтому хочется сохранить game_state в состоянии бота пока работает функция do_move. Делаю так:

struct Bot1 {
    my_gs: *const GameState,
}

impl Bot for Bot1 {
    fn reset(&mut self) {
        self.my_gs = null();
        println!("reset gs={:?}", self.my_gs);
    }

    fn do_move(&mut self, gs: &GameState) -> Move {
        ///////////////////////////////////////
        self.my_gs = gs; // setup the reference
        ///////////////////////////////////////

        // some bot logic goes here
        let the_move = if self.find_closest_empty(&Point(0, 0)).is_some() {
            Move::Left
        } else {
            Move::Right
        };

        ///////////////////////////////////////
        self.my_gs = null(); // clear it back
        ///////////////////////////////////////
        the_move
    }
}

Теперь внутри алгоритма выбора хода (в данном случае это find_closest_empty) доступ к состянию игры можно сделать просто:

    fn find_closest_empty(&self, _src: &Point) -> Option<Point> {
        // guaranteed to exist since we have initialized
        let my_gs = unsafe { &*self.my_gs }; // OMG unsafe

        println!("inside find_closest_empty gs={:?}", my_gs);
        None
    }

Но это решение мне ужасно не нравится из-за unsafe. :unamused:
Передавать в do_move Rc<GameState> не хотелось бы, поскольку я хочу сохранить свободу у вызывающего кода решать какое именно состояние туда передать - тот же объект, или может какой-то другой (хотя может я что-то не учёл и Rc тут годится).
Какие у меня есть варианты?
Спасибо.

Этот пример на playground
Расширенный пример с рандомами и GameState как матрицей ячеек

Теоретически можно протянуть game_state как параметр во все функции, но это очень громоздко.

Я для Zemeroth в итоге решил не париться и таки просто везде протащить состояние:

$ git grep 'state: &State' | wc -l
98

Громоздко, но зато любимая растом “явность” во все поля. Пока живой, код в целом нахожу вполне поддерживаемым.

1 лайк

Альтернативное предложение из чата:

Roman Akberov @RomanAkberov 16:59
Почему бы не сделать прокси-тип BotMove и все методы дать ему?
<…>
Он будет жить только на время хода.

Nick Linker @nlinker 17:01
его инициализировать GameState…

Roman Akberov @RomanAkberov 17:01
Угу

Получилось неплохо. Вкратце выкусил спец структуру для алгоритма Bot1Alg:

struct Bot1Alg<'a> {
    gs: &'a GameState,
}

impl<'a> Bot1Alg<'a> {
    fn find_closest_empty(&self, src: &Point) -> Option<Point> {
        // guaranteed to exist since we have initialized
        let m = self.gs.height;
        let n = self.gs.width;

        let Point(oi, oj) = src;

        let bounded = |p: &Point| {
            let Point(mut i, mut j) = *p;
            if 0 <= i && i < m && 0 <= j && j < n { *p } else {
                i = if i < 0 { 0 } else if i > m - 1 { m - 1 } else { i };
                j = if j < 0 { 0 } else if j > n - 1 { n - 1 } else { j };
                Point(i, j)
            }
        };

        // explore the field around the point with radius 0, 1, etc
        for r in 1..(m + n) {
            for k in 0..r {
                let ps = [
                    Point(oi - k, oj + r - k),
                    Point(oi - r + k, oj - k),
                    Point(oi + k, oj - r + k),
                    Point(oi + r - k, oj + k),
                ];
                let opt = ps.iter()
                    .map(bounded)
                    .find(|p| self.gs.cells[p.0 as usize][p.1 as usize] == Cell::Empty);
                if opt.is_some() {
                    return opt;
                }
            }
        }
        None
    }
}

И в do_move просто делаем так:

    fn do_move(&mut self, gs: &GameState) -> Move {
        let alg = Bot1Alg { gs };

        // some bot logic goes here
        ...
        alg.find_closest_empty(&Point(i, j))
        ...
    }

Полный код можно запустить здесь

3 лайка