Время жизни для метода, возвращающего итератор структур с таким же временем жизни

Предположим следующий надуманный пример:

struct Board {
    squares: Vec<i32>,
}

struct Point<'a> {
    board: &'a Board,
    x: i32,
    y: i32,
}

impl<'a> Point<'a> {
    pub fn neighbors(&self) -> impl Iterator<Item = Point<'a>> {
        [(0, -1), (-1, 0), (1, 0), (1, 0)]
            .iter().map(|(dx, dy)| Point {
                board: self.board,
                x: self.x + dx,
                y: self.y + dy,
            })
    }
}

Это не компилируется, потому что, насколько я понимаю, время жизни точек, созданных в лямбде, неверно:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> src/main.rs:14:25
   |
14 |               .iter().map(|(dx, dy)| Point {
   |  _________________________^
15 | |                 board: self.board,
16 | |                 x: self.x + dx,
17 | |                 y: self.y + dy,
18 | |             })
   | |_____________^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 12:5...
  --> src/main.rs:12:5
   |
12 | /     pub fn neighbors(&self) -> impl Iterator<Item = Point<'a>> {
13 | |         [(0, -1), (-1, 0), (1, 0), (1, 0)]
14 | |             .iter().map(|(dx, dy)| Point {
15 | |                 board: self.board,
...  |
18 | |             })
19 | |     }
   | |_____^
   = note: ...so that the types are compatible:
           expected &&Point<'_>
              found &&Point<'a>
note: but, the lifetime must be valid for the lifetime 'a as defined on the impl at 11:1...
  --> src/main.rs:11:1
   |
11 | impl<'a> Point<'a> {
   | ^^^^^^^^^^^^^^^^^^
note: ...so that return value is valid for the call
  --> src/main.rs:12:32
   |
12 |     pub fn neighbors(&self) -> impl Iterator<Item = Point<'a>> {
   |                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Я немного не понимаю, почему это так, потому что кажется, что воплощения здесь имеют смысл. Время жизни Point обусловлено временем жизни ссылки на Board. Таким образом, Point<'a> имеет ссылку на плату с временем жизни 'a, поэтому он должен иметь возможность создавать больше Point<'a>, потому что их ссылки на плату будут иметь такое же время жизни ('a).

Но, если я удалю лямбду, это сработает:

impl<'a> Point<'a> {
    pub fn neighbors(&self) -> [Point<'a>; 4] {
        [
            Point { board: self.board, x: self.x    , y: self.y - 1},
            Point { board: self.board, x: self.x - 1, y: self.y    },
            Point { board: self.board, x: self.x + 1, y: self.y    },
            Point { board: self.board, x: self.x    , y: self.y + 1},
        ]
    }
}

Итак, я подозреваю, что проблема заключается в том, что лямбда может быть запущена по истечении срока жизни 'a. Но означает ли это, что я не могу лениво выдавать эти баллы?

tl; dr Как мне устроить проверку заимствований с помощью метода, который лениво создает новые структуры, время жизни которых привязано к структуре, создающей их?


person Bailey Parker    schedule 15.05.2018    source источник


Ответы (3)


Когда у вас есть такая проблема в методе, неплохо было бы добавить явное время жизни к &self:

pub fn neighbors(&'a self) -> impl Iterator<Item = Point<'a>> {
    [(0, -1), (-1, 0), (1, 0), (1, 0)]
        .iter().map(|(dx, dy)| Point {
            board: self.board,
            x: self.x + dx,
            y: self.y + dy,
        })
}

Ошибка теперь лучше

error[E0373]: closure may outlive the current function, but it borrows `self`, which is owned by the current function
  --> src/main.rs:14:30
   |
14 |             .iter().map(|(dx, dy)| Point {
   |                         ^^^^^^^^^^ may outlive borrowed value `self`
15 |                 board: self.board,
   |                        ---- `self` is borrowed here
help: to force the closure to take ownership of `self` (and any other referenced variables), use the `move` keyword
   |
14 |             .iter().map(move |(dx, dy)| Point {
   |                         ^^^^^^^^^^^^^^^

Затем вам просто нужно добавить ключевое слово move в соответствии с рекомендациями компилятора, чтобы сказать ему, что вы больше не будете использовать &'a self.

Обратите внимание, что время жизни self не должно быть таким же, как время жизни Point. Лучше использовать эту подпись:

fn neighbors<'b>(&'b self) -> impl 'b + Iterator<Item = Point<'a>>
person Boiethios    schedule 15.05.2018
comment
Действительно! Жаль, что сообщение об ошибке для обоих не одно и то же (потому что то, что вы спровоцировали, определенно намного более явное, что другие упомянутые переменные могут быть проблемой). Огромное спасибо! (Также в качестве примечания я смог заставить это работать с массивом, только добавив move и время жизни к self в 1.26; я не уверен, имел ли вы в виду, что мне нужно сохранить вектор) - person Bailey Parker; 15.05.2018
comment
@BaileyParker Я имел в виду это; Я не могу объяснить, почему это компилируется. Тогда мой ответ все еще неполный. - person Boiethios; 15.05.2018
comment
@BaileyParker Я удалил материал о массиве - person Boiethios; 15.05.2018
comment
@BaileyParker FYI, я задал вопрос об этом stackoverflow.com/questions/50345139/ - person Boiethios; 15.05.2018

Оба существующих ответа (Shepmaster, Boiethios) позволяют Points, возвращаемым итератором, пережить сам итератор. Однако должна существовать возможность построить итератор, который даже переживает первоначальный Point, из которого он был создан, путем перемещения в него содержимого Point. Эта версия сохраняет исходную сигнатуру функции:

fn neighbors(&self) -> impl Iterator<Item = Point<'a>> {
    let Point { board, x, y } = *self;
    [(0, -1), (-1, 0), (1, 0), (1, 0)]
        .iter().map(move |(dx, dy)| Point {
            board: board,
            x: x + dx,
            y: y + dy,
        })
}

Копирование содержимого *self в локальные переменные, которые перемещаются в замыкание, приводит к тому, что замыкание - и, следовательно, возвращаемый итератор - больше не содержит ссылок на self.

Вот кое-что, что вы можете сделать с этой версией, что невозможно сделать иначе (игровая площадка):

let mut p = Point {
    board: &b,
    x: 10,
    y: 12,
};
for n in p.neighbors() {
    p = n;
}

Одно из возможных предупреждений состоит в том, что если Point содержит очень большие или не Copy данные, которые нельзя или не следует перемещать в закрытие, это не сработает. В этом случае вам следует использовать другое решение:

fn neighbors<'b>(&'b self) -> impl 'b + Iterator<Item = Point<'a>>
person trentcl    schedule 15.05.2018
comment
@Boiethios Бывают случаи, когда это не сработает, и, возможно, придется вернуться к более строгой подписи. Я добавил отказ от ответственности - person trentcl; 15.05.2018
comment
Это хороший момент. Я не особо задумывался о контексте использования. В моем конкретном случае может иметь смысл не указывать Copy, но если бы это было похоже на то, что вы правы в том, что я должен хранить поля на self, которые мне нужны, в местных. Биоэтиос правильно указал первопричину, но это хороший аргумент. Хотел бы я дать чек обоим :) - person Bailey Parker; 15.05.2018

Я бы назначил время жизни self, отличное от времени жизни Board:

impl<'b> Point<'b> {
    fn neighbors<'a>(&'a self) -> impl Iterator<Item = Point<'b>> + 'a {
        [(0, -1), (-1, 0), (1, 0), (1, 0)]
            .iter().map(move |(dx, dy)| Point {
                board: self.board,
                x: self.x + dx,
                y: self.y + dy,
            })
    }
}

Для этого также требуется пометить закрытие как move, чтобы переместить значение &Self в закрытие, чтобы закрытие все еще могло получить доступ к board, x и y, когда оно продвинуто. Поскольку неизменяемые ссылки могут быть скопированы, это не помешает вызывающей стороне что-либо делать.

Без отдельных времен жизни время жизни Point, возвращаемых итератором, искусственно ограничивается временем жизни Point, который их сгенерировал. Например, этот код не работает, когда времена жизни объединены, но работает, когда они различны:

fn example<'b>(board: &'b Board) {
    let _a = {
        let inner = Point { board, x: 0, y: 0 };
        let mut n = inner.neighbors();
        n.next().unwrap()
    };
}

Смотрите также:

person Shepmaster    schedule 15.05.2018
comment
Интересно, я об этом не подумал. Чтобы быть уверенным, причина того, что ваш пример терпит неудачу, заключается в том, что у платы есть время жизни 'b, но время жизни inner привязано к области, назначенной для _a (которые различны)? - person Bailey Parker; 15.05.2018
comment
@BaileyParker это не удастся, потому что единое время жизни (fn neighbors(&'a self) -> impl Iterator<Item = Point<'a>>) означает, что возвращенные Points могут содержать ссылку на self / источник Point. - person Shepmaster; 15.05.2018