Почему в Rust нужны явные времена жизни?

Я читал главу о жизни в книге Rust и наткнулся на этот пример для именованного / явного времени жизни:

struct Foo<'a> {
    x: &'a i32,
}

fn main() {
    let x;                    // -+ x goes into scope
                              //  |
    {                         //  |
        let y = &5;           // ---+ y goes into scope
        let f = Foo { x: y }; // ---+ f goes into scope
        x = &f.x;             //  | | error here
    }                         // ---+ f and y go out of scope
                              //  |
    println!("{}", x);        //  |
}                             // -+ x goes out of scope

Мне совершенно ясно, что ошибка, предотвращаемая компилятором, это использование после освобождения ссылки, назначенной x: после того, как внутренний объем сделан, f и поэтому &f.x стать недействительным и не должен был быть назначен x,

Моя проблема в том, что проблема могла быть легко проанализирована без использования явного 'a время жизни, например, путем неверного присвоения ссылки на более широкий контекст (x = &f.x;).

В каких случаях требуется явное время жизни для предотвращения ошибок использования после освобождения (или какого-либо другого класса?)?

11 ответов

Решение

Все остальные ответы имеют существенные моменты ( конкретный пример fjh, где требуется явное время жизни), но не хватает одного ключевого момента: зачем нужны явные времена жизни, когда компилятор скажет вам, что вы ошиблись?

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

fn foo() -> _ {  
    ""
}

Конечно, компилятор может видеть, что я возвращаю &'static strтак почему же программист должен его набирать?

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

Функции - это естественная граница брандмауэра для эффектов изменения кода. Если бы мы позволили полностью проверить время жизни из кода, то невинно выглядящие изменения могут повлиять на время жизни, что может привести к ошибкам в функции на большом расстоянии. Это не гипотетический пример. Насколько я понимаю, у Haskell есть такая проблема, когда вы полагаетесь на вывод типов для функций верхнего уровня. Ржавчина пресекла эту конкретную проблему в зародыше.

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

fn foo(a: &u8, b: &u8) -> &u8

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

сделав вывод о незаконном присвоении ссылки на более широкий охват

Области - это время жизни, по сути. Чуть яснее, на всю жизнь 'a является общим параметром времени жизни, который может быть специализирован с определенной областью во время компиляции, основанной на сайте вызова.

действительно ли необходимы явные времена жизни для предотвращения [...] ошибок?

Не за что. Время жизни необходимо для предотвращения ошибок, но явное время жизни необходимо для защиты того, что имеют маленькие здравомыслящие программисты.

Давайте посмотрим на следующий пример.

fn foo<'a, 'b>(x: &'a u32, y: &'b u32) -> &'a u32 {
    x
}

fn main() {
    let x = 12;
    let z: &u32 = {
        let y = 42;
        foo(&x, &y)
    };
}

Здесь важны явные времена жизни. Это компилируется, потому что результат foo имеет то же время жизни, что и первый аргумент ('a), поэтому он может пережить свой второй аргумент. Это выражается именами времени жизни в подписи foo, Если вы переключили аргументы в вызове foo компилятор будет жаловаться, что y не живет достаточно долго

error[E0597]: `y` does not live long enough
  --> src/main.rs:10:5
   |
9  |         foo(&y, &x)
   |              - borrow occurs here
10 |     };
   |     ^ `y` dropped here while still borrowed
11 | }
   | - borrowed value needs to live until here

Аннотация времени жизни в следующей структуре:

struct Foo<'a> {
    x: &'a i32,
}

указывает, что Foo экземпляр не должен переживать содержащуюся в нем ссылку (x поле).

Пример, который вы встретили в книге Rust, не иллюстрирует это, потому что f а также y переменные выходят из области видимости одновременно.

Лучший пример будет следующим:

fn main() {
    let f : Foo;
    {
        let n = 5;  // variable that is invalid outside this block
        let y = &n;
        f = Foo { x: y };
    };
    println!("{}", f.x);
}

Сейчас, f действительно переживает переменную, на которую указывает f.x,

Обратите внимание, что в этом фрагменте кода нет явных времен жизни, кроме определения структуры. Компилятор прекрасно умеет определять время жизни в main(),

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

struct RefPair(&u32, &u32);

Должны ли они быть разными жизнями или они должны быть одинаковыми? Это имеет значение с точки зрения использования, struct RefPair<'a, 'b>(&'a u32, &'b u32) сильно отличается от struct RefPair<'a>(&'a u32, &'a u32),

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

Если функция получает две ссылки в качестве аргументов и возвращает ссылку, то реализация функции может иногда возвращать первую ссылку, а иногда и вторую. Невозможно предсказать, какая ссылка будет возвращена для данного вызова. В этом случае невозможно определить время жизни возвращаемой ссылки, поскольку каждая ссылка на аргумент может ссылаться на другую привязку переменной с другим временем жизни. Явные времена жизни помогают избежать или прояснить такую ​​ситуацию.

Аналогично, если структура содержит две ссылки (как два поля-члена), то функция-член структуры может иногда возвращать первую ссылку, а иногда и вторую. Снова явные времена жизни предотвращают такие неясности.

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

Я нашел другое отличное объяснение здесь: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html.

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

Корпус из книги очень прост по дизайну. Тема жизней считается сложной.

Компилятор не может легко определить время жизни функции с несколькими аргументами.

Кроме того, мой собственный дополнительный ящик имеет OptionBool введите с as_slice метод, чья подпись на самом деле:

fn as_slice(&self) -> &'static [bool] { ... }

Компилятор никак не мог понять это.

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

  1. Размещение явной аннотации времени жизни для функции ограничивает тип кода, который может появляться внутри этой функции. Явные времена жизни позволяют компилятору гарантировать, что ваша программа делает то, что вы хотели.

  2. Если вы (компилятор) хотите (и) проверить, является ли фрагмент кода допустимым, вам (компилятору) не придется итеративно заглядывать внутрь каждой вызываемой функции. Достаточно взглянуть на аннотации функций, которые напрямую вызываются этим фрагментом кода. Это делает вашу программу намного проще для вас (компилятор) и делает время компиляции управляемым.

В пункте 1. Рассмотрим следующую программу, написанную на Python:

import pandas as pd
import numpy as np

def second_row(ar):
    return ar[0]

def work(second):
    df = pd.DataFrame(data=second)
    df.loc[0, 0] = 1

def main():
    # .. load data ..
    ar = np.array([[0, 0], [0, 0]])

    # .. do some work on second row ..
    second = second_row(ar)
    work(second)

    # .. much later ..
    print(repr(ar))

if __name__=="__main__":
    main()

который напечатает

array([[1, 0],
       [0, 0]])

Такое поведение всегда меня удивляет. Что происходит, так это df делится памятью с ar, поэтому, когда некоторые из содержания df изменения в workта смена заражает ar также. Однако в некоторых случаях это может быть именно тем, что вам нужно, из соображений эффективности использования памяти (без копирования). Настоящая проблема в этом коде заключается в том, что функция second_row возвращает первый ряд вместо второго; удачи в отладке этого.

Рассмотрим вместо этого похожую программу, написанную на Rust:

#[derive(Debug)]
struct Array<'a, 'b>(&'a mut [i32], &'b mut [i32]);

impl<'a, 'b> Array<'a, 'b> {
    fn second_row(&mut self) -> &mut &'b mut [i32] {
        &mut self.0
    }
}

fn work(second: &mut [i32]) {
    second[0] = 1;
}

fn main() {
    // .. load data ..
    let ar1 = &mut [0, 0][..];
    let ar2 = &mut [0, 0][..];
    let mut ar = Array(ar1, ar2);

    // .. do some work on second row ..
    {
        let second = ar.second_row();
        work(second);
    }

    // .. much later ..
    println!("{:?}", ar);
}

Составив это, вы получите

error[E0308]: mismatched types
 --> src/main.rs:6:13
  |
6 |             &mut self.0
  |             ^^^^^^^^^^^ lifetime mismatch
  |
  = note: expected type `&mut &'b mut [i32]`
             found type `&mut &'a mut [i32]`
note: the lifetime 'b as defined on the impl at 4:5...
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...does not necessarily outlive the lifetime 'a as defined on the impl at 4:5
 --> src/main.rs:4:5
  |
4 |     impl<'a, 'b> Array<'a, 'b> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

На самом деле вы получаете две ошибки, есть еще одна с ролями 'a а также 'b взаимозаменяемыми. Глядя на аннотацию second_rowмы находим, что вывод должен быть &mut &'b mut [i32]выход должен быть ссылкой на ссылку с временем жизни 'b (время жизни второго ряда Array). Тем не менее, потому что мы возвращаем первый ряд (который имеет время жизни 'a), компилятор жалуется на несоответствие времени жизни. В правильном месте. В нужное время. Отладка на одном дыхании.

Я думаю, что аннотация времени жизни - это контракт о данном ref, который действителен в принимающей области, только пока он остается действительным в исходной области. Объявление большего количества ссылок в одном и том же виде времени жизни объединяет области видимости, что означает, что все исходные ссылки должны соответствовать этому контракту. Такая аннотация позволяет компилятору проверить выполнение контракта.

Причина, по которой ваш пример не работает, заключается в том, что Rust имеет только локальное время жизни и вывод типа. То, что вы предлагаете, требует глобального вывода. Всякий раз, когда у вас есть ссылка, срок жизни которой нельзя исключить, она должна быть аннотирована.

Это сводится к производительности компилятора.

Компилятор Rust смотрит только на сигнатуру функции, а не на ее тело . Вот почему мы явно указываем связь между временем жизни на входе и временем жизни на выходе.

      fn longest_string<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

fn main() {
    let string1 = "abcdef";
    let string2 = "xyz";
    let result;

    result = longest_string(&string1, &string2);

    println!("The longest string is {}", result);
    println!("The longest string is {}", result);
}

Подробно: в функции longest_string мы возвращаем ссылку из функции, и эта ссылка ссылается на некоторые данные (данные внутри x). Несмотря на то, что в реализации longest_string мы всегда возвращаем x, компилятор Rust смотрит только на сигнатуру функции, а не на ее тело, чтобы определить, какие гарантии предоставляются в отношении времени жизни ссылок.

Другие вопросы по тегам