Что такое нелексические времена жизни?

У Rust есть RFC, связанный с нелегическими сроками жизни, который был одобрен для долгого использования на этом языке. В последнее время поддержка Rust этой функции значительно улучшилась и считается полной.

Мой вопрос: что такое нелексическое время жизни?

Ответ 1

Легче всего понять, что такое нелексические времена жизни, понимая, что такое лексические времена жизни. В версиях Rust до появления нелексических времен жизни этот код завершится ошибкой:

fn main() {
    let mut scores = vec![1, 2, 3];
    let score = &scores[0];
    scores.push(4);
}

Ржавчина компилятор видит, что scores заимствованы по score переменной, поэтому он запрещает дальнейшую мутацию scores:

error[E0502]: cannot borrow 'scores' as mutable because it is also borrowed as immutable
 --> src/main.rs:4:5
  |
3 |     let score = &scores[0];
  |                  ------ immutable borrow occurs here
4 |     scores.push(4);
  |     ^^^^^^ mutable borrow occurs here
5 | }
  | - immutable borrow ends here

Тем не менее, человек может легко увидеть, что этот пример слишком консервативен: score никогда не используется ! Проблема в том, что заимствование scores по score является лексическим - оно длится до конца блока, в котором оно содержится:

fn main() {
    let mut scores = vec![1, 2, 3]; //
    let score = &scores[0];         //
    scores.push(4);                 //
                                    // <-- score stops borrowing here
}

Нелексические времена жизни исправляют это, улучшая компилятор для понимания этого уровня детализации. Компилятор теперь может более точно определить, когда требуется заимствование, и этот код скомпилируется.

Замечательная вещь о нелексических жизнях состоит в том, что, когда они включены, никто о них не подумает. Это просто станет "тем, что делает Rust", и все будет (надеюсь) просто работать.

Почему лексические времена жизни были разрешены?

Rust предназначен для компиляции только известных безопасных программ. Однако невозможно точно разрешить только безопасные программы и отклонить небезопасные. С этой целью Rust ошибается на стороне консервативности: некоторые безопасные программы отклоняются. Лексические времена жизни являются одним из примеров этого.

Лексические времена жизни было намного проще реализовать в компиляторе, потому что знание блоков "тривиально", а знание потока данных - меньше. Компилятор нужно было переписать, чтобы ввести и использовать "промежуточное представление среднего уровня" (MIR). Затем необходимо было переписать средство проверки заимствований ("заем"), чтобы использовать MIR вместо абстрактного синтаксического дерева (AST). Затем правила проверки заимствований должны были быть уточнены, чтобы быть более детальными.

Лексические времена жизни не всегда мешают программисту, и есть много способов обойти лексические времена жизни, когда они это делают, даже если они раздражают. Во многих случаях это включало добавление дополнительных фигурных скобок или логическое значение. Это позволило Rust 1.0 поставляться и быть полезным в течение многих лет, прежде чем были внедрены нелексические времена жизни.

Интересно, что определенные хорошие образцы были разработаны из-за лексических времен жизни. Главный пример для меня - шаблон entry. Этот код завершается с ошибкой до нелексического времени жизни и компилируется с ним:

fn example(mut map: HashMap<i32, i32>, key: i32) {
    match map.get_mut(&key) {
        Some(value) => *value += 1,
        None => {
            map.insert(key, 1);
        }
    }
}

Однако этот код неэффективен, потому что он вычисляет хэш ключа дважды. Решение, созданное из-за лексических времен жизни, короче и эффективнее:

fn example(mut map: HashMap<i32, i32>, key: i32) {
    *map.entry(key).or_insert(0) += 1;
}

Название "нелексические времена жизни" мне не подходит

Время жизни значения - это промежуток времени, в течение которого значение остается по определенному адресу памяти (см. Почему я не могу сохранить значение и ссылку на это значение в той же структуре? Для более подробного объяснения). Функция, известная как нелексические времена жизни, не меняет времени жизни каких-либо значений, поэтому она не может сделать времена жизни нелексическими. Это только делает отслеживание и проверку заимствований этих значений более точными.

Более точное название функции может быть "нелексические заимствования". Некоторые разработчики компиляторов ссылаются на базовый "заем на основе MIR".

Нелексические времена жизни, как таковые, никогда не задумывались как функция, ориентированная на пользователя. Они в основном выросли в наших умах из-за маленьких бумажек, которые мы получаем от их отсутствия. Их название было в основном предназначено для внутренних разработок, и изменение его в маркетинговых целях никогда не было приоритетом.

Да, но как мне это использовать?

В Rust 1.31 (выпущен 2018-12-06) вам необходимо подписаться на редакцию Rust 2018 в вашем Cargo.toml:

[package]
name = "foo"
version = "0.0.1"
authors = ["An Devloper <[email protected]>"]
edition = "2018"

Начиная с Rust 1.36, выпуск Rust 2015 также обеспечивает нелексические времена жизни.

Текущая реализация нелексических времен жизни находится в "режиме миграции". Если проверка заимствования NLL прошла, компиляция продолжается. Если это не так, вызывается предыдущий заемщик. Если старая программа проверки заимствования разрешает ввод кода, выводится предупреждение, информирующее вас о том, что ваш код может сломаться в будущей версии Rust и должен быть обновлен.

В ночных версиях Rust вы можете подписаться на принудительное отключение с помощью флага функции:

#![feature(nll)]

Вы даже можете -Z polonius на экспериментальную версию NLL, используя флаг компилятора -Z polonius.

Пример реальных проблем, решаемых нелексическими временами жизни