Почему я не могу вернуть изменяемую ссылку на внешнюю переменную из замыкания? - PullRequest
0 голосов
/ 11 октября 2018

Я играл с замыканиями Rust, когда столкнулся с этим интересным сценарием:

fn main() {
    let mut y = 10;

    let f = || &mut y;

    f();
}

Это выдает ошибку:

error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
 --> src/main.rs:4:16
  |
4 |     let f = || &mut y;
  |                ^^^^^^
  |
note: first, the lifetime cannot outlive the lifetime  as defined on the body at 4:13...
 --> src/main.rs:4:13
  |
4 |     let f = || &mut y;
  |             ^^^^^^^^^
note: ...so that closure can access `y`
 --> src/main.rs:4:16
  |
4 |     let f = || &mut y;
  |                ^^^^^^
note: but, the lifetime must be valid for the call at 6:5...
 --> src/main.rs:6:5
  |
6 |     f();
  |     ^^^
note: ...so type `&mut i32` of expression is valid during the expression
 --> src/main.rs:6:5
  |
6 |     f();
  |     ^^^

Даже если компилятор пытается объяснить это строкойпострочно, я до сих пор не понял, на что именно он жалуется.

Он пытается сказать, что изменяемая ссылка не может пережить закрывающее закрытие?

Компилятор не жалуется, если я уберу вызов f().

Ответы [ 3 ]

0 голосов
/ 11 октября 2018

(На данный момент это только обоснованное предположение. Если люди сошлись во мнении, что этот ответ правильный, я уберу этот отказ от ответственности)


Существует два основныхчто здесь происходит:

  1. Закрытия не могут возвращать ссылки на свое окружение
  2. Изменяемая ссылка на изменяемую ссылку может использовать только время жизни внешней ссылки (в отличие от неизменяемых ссылок)

1.Замыкания, возвращающие ссылки на среду

Замыкания не могут возвращать никаких ссылок со временем жизни self (объект замыкания).Это почему?Каждое замыкание можно назвать FnOnce, поскольку это супер-черта FnMut, которая, в свою очередь, является супер-чертой Fn.FnOnce имеет такой метод:

fn call_once(self, args: Args) -> Self::Output;

Обратите внимание, что self передается по значению.Так как self потребляется (и теперь живет в функции call_once), мы не можем возвращать ссылки на него - это было бы эквивалентно возвращению ссылок на локальную переменную функции.

Теоретически,call_mut позволит вернуть ссылки на self (поскольку он получает &mut self).Но поскольку call_once, call_mut и call реализованы с одним и тем же телом, замыкания в целом не могут возвращать ссылки на self (то есть на их захваченное окружение).

Просто чтобы бытьконечно: замыкания могут захватывать ссылки и возвращать их!И они могут захватить по ссылке и вернуть эту ссылку.Эти вещи являются чем-то другим.Это как раз то, что хранится в типе замыкания.Если в типе есть ссылка, она может быть возвращена.Но мы не можем вернуть ссылки на что-либо, хранящееся в типе замыкания.

Вложенные изменяемые ссылки

Рассмотрим эту функцию (обратите внимание, что тип аргумента подразумевает, что 'inner: 'outer; 'outer короче, чем'inner):

fn foo<'outer, 'inner>(x: &'outer mut &'inner mut i32) -> &'inner mut i32 {
    *x
}

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

Впрочем, нормально возвращать &'outer mut i32.Но невозможно получить прямую ссылку с более длинным (внутренним) временем жизни.

Ручная запись замыкания

Давайте попробуем написать код замыкания, которое ты пытался написать:

let mut y = 10;

struct Foo<'a>(&'a mut i32);
impl<'a> Foo<'a> {
    fn call<'s>(&'s mut self) -> &'??? mut i32 { self.0 }
}

let mut f = Foo(&mut y);
f.call();

Какое время жизни должна иметь возвращаемая ссылка?

  • Это не может быть 'a, потому что у нас в основном &'s mut &'a mut i32.И, как обсуждалось выше, в такой вложенной изменяемой эталонной ситуации мы не можем извлечь более длинное время жизни!
  • Но это также не может быть 's, так как это означало бы, что замыкание возвращает что-то со временем жизни'self ("заимствовано из self").Как уже говорилось выше, замыкания не могут этого сделать.

Таким образом, компилятор не может генерировать значения замыкания для нас.

0 голосов
/ 11 октября 2018

Короткая версия

Закрытие f хранит изменяемую ссылку на y.Если бы было разрешено возвратить копию этой ссылки, вы бы в итоге получили две изменяемые одновременные ссылки на y (одна в закрытии, одна возвращенная), что запрещено правилами безопасности памяти Rust.

Длинная версия

Замыкание можно рассматривать как

struct __Closure<'a> {
    y: &'a mut i32,
}

Поскольку оно содержит изменяемую ссылку, замыкание называется FnMut, по сути с определением

fn call_mut(&mut self, args: ()) -> &'a mut i32 { self.y }

Поскольку у нас есть только изменяемая ссылка на само замыкание, мы не можем переместить поле y из заимствованного контекста, также мы не можем скопировать его, поскольку изменяемые ссылки не являются Copy.

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

fn main() {
    let x = String::new();
    let mut y: u32 = 10;
    let f = || {
        drop(x);
        &mut y
    };
    f();
}

Поскольку мы потребляем x внутри области замыкания, а x не равен Copy, компилятор обнаруживает, что замыкание может быть только FnOnce.Вызов FnOnce замыкания передает само замыкание по значению, поэтому нам разрешается перемещать изменяемую ссылку наружу.

Еще один более явный способ заставить закрытие быть FnOnce - передать его универсальномуфункция с привязкой черты.Этот код также хорошо работает:

fn make_fn_once<'a, T, F: FnOnce() -> T>(f: F) -> F {
    f
}

fn main() {
    let mut y: u32 = 10;
    let f = make_fn_once(|| {
        &mut y
    });
    f();
}
0 голосов
/ 11 октября 2018

Рассмотрим этот код:

fn main() {
    let mut y: u32 = 10;

    let ry = &mut y;
    let f = || ry;

    f();
}

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

Теперь эквивалентная версия вашего кода:

fn main() {
    let mut y: u32 = 10;

    let f = || {
        let ry = &mut y;
        ry
    };

    f();
}

Теперь компилятор назначает ry время жизни, связанное с областью действия тела замыкания, а не время жизни, связанное с основным телом.

Также обратите внимание, что работает неизменяемый эталонный случай:

fn main() {
    let mut y: u32 = 10;

    let f = || {
        let ry = &y;
        ry
    };

    f();
}

Это связано с тем, что &T имеет семантику копирования и &mut T имеет семантику перемещения, см. Документация по семантике копирования / перемещения для& T / & mut T для получения более подробной информации набирает .

Отсутствующий фрагмент

Компилятор выдает проблему, связанную с временем жизни:

cannot infer an appropriate lifetime for borrow expression due to conflicting requirements

, но, как указаноу Свена Марнача также есть проблема, связанная с запрещенным выходом из заемного контента.

Но почему компилятор не выдает эту ошибку?А именно:

cannot move out of borrowed content

Короткий ответ заключается в том, что компилятор сначала выполняет проверку типов, а затем заимствует проверку.

длинный ответ

Закрытие состоит из двух частей.:

  • состояние замыкания: структура, содержащая все объекты, захваченные замыканием

  • the логика замыкания: реализация признака FnOnce, FnMut или Fn

В этом случае состояние замыкания является изменяемой ссылкой y, логика - это тело замыкания {&mut y}, которое просто возвращает изменяемую ссылку.

При обнаружении ссылки ржавчина контролирует два аспекта:

  1. состояние : если ссылка указывает на действительный фрагмент памяти (т. Е. Срок действия чтения);

  2. логика : если фрагмент памятиявляется псевдонимом, другими словами, если на него указывают несколько ссылок одновременно;

Примечание tВыход из заимствованного содержимого запрещен во избежание алиасинга памяти.

rustc компилятор выполняет свою работу через этапов , здесь приведен упрощенный рабочий процесс:

.rs input -> AST -> HIR -> hir postprocessing -> MIR -> mir postprocessing -> llvm IR -> binary

Компилятор сообщает о проблеме времени жизни, потому что rustc сначала выполняет фазу проверки типа в hir postprocessing, которая включает анализ времени жизни, и после этого, в случае успеха, выполняет проверку заимствования в фазе mir postprocessing.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...