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

199

Я читал главу о жизни в книге 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;).

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

Corazza
источник
1
Это было опубликовано в Reddit
Shepmaster
2
Для будущих читателей этого вопроса, пожалуйста, обратите внимание, что это ссылки на первое издание книги, и теперь есть второе издание :)
carols10cents

Ответы:

205

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

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

fn foo() -> _ {  
    ""
}

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

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

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

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

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

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

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

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

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

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

Shepmaster
источник
19
@jco Представьте, что у вас есть функция верхнего уровня f x = x + 1без сигнатуры типа, которую вы используете в другом модуле. Если позднее вы измените определение на f x = sqrt $ x + 1, его тип изменится с Num a => a -> aна Floating a => a -> a, что приведет к ошибкам типов на всех сайтах вызовов, где fвызывается, например, с Intаргументом. Наличие сигнатуры типа гарантирует, что ошибки происходят локально.
FJH
11
«Области - это время жизни, по сути. Немного более ясно, время жизни» - это общий параметр времени жизни, который может быть специализирован с определенной областью действия во время вызова. « Вау, это действительно великолепная, освещающая точка. Я хотел бы, чтобы это было включено в книгу это явно.
Корацца
2
@fjh Спасибо. Просто для того, чтобы понять, не ошибаюсь ли я, дело в том, что если бы тип был явно указан перед добавлением sqrt $, только локальная ошибка произошла бы после изменения, а не много ошибок в других местах (что намного лучше, если бы мы не не хотите изменить фактический тип)?
Корацца
5
@jco Точно. Не указание типа означает, что вы можете случайно изменить интерфейс функции. Это одна из причин, по которой настоятельно рекомендуется аннотировать все элементы верхнего уровня в Haskell.
FJH
5
Также, если функция получает две ссылки и возвращает ссылку, она может иногда возвращать первую ссылку, а иногда и вторую. В этом случае невозможно определить время жизни возвращаемой ссылки. Явные времена жизни помогают избежать / прояснить такую ​​ситуацию.
MichaelMoser
93

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

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
FJH
источник
16

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

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

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

Пример вы наткнулись в книге Ржавчина не иллюстрирует это потому , что 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.

user3151599
источник
9

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

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

struct RefPair(&u32, &u32);

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

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

Владимир Матвеев
источник
2
Можете ли вы объяснить, почему они очень разные?
AB
@AB Второй требует, чтобы обе ссылки имели одинаковое время жизни. Это означает, что refpair.1 не может жить дольше, чем refpair.2, и наоборот - поэтому оба рефери должны указывать на что-то с одним владельцем. Первое, однако, требует, чтобы RefPair пережил обе свои части.
Llogiq
2
@AB, он компилируется, потому что оба времени жизни унифицированы - поскольку локальные времена жизни меньше 'static, 'staticих можно использовать везде, где можно использовать локальные времена жизни, поэтому в вашем примере pего параметр времени жизни будет выведен как локальное время жизни y.
Владимир Матвеев
5
@AB RefPair<'a>(&'a u32, &'a u32)означает, что 'aэто будет пересечение обоих входных времен жизни, т.е. в этом случае время жизни y.
FJH
1
@llogiq "требует, чтобы RefPair пережил обе свои части"? Я думал, что все было наоборот ... a & u32 все еще может иметь смысл без RefPair, в то время как RefPair с мертвыми реферами будет странным
Qed
6

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

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

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

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

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

llogiq
источник
IINM, вывод времени жизни возвращаемого типа функции с двумя аргументами, будет эквивалентен проблеме остановки - IOW, не разрешим за конечное время.
Дстромберг
4

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

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

Corazza
источник
4

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

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

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

MichaelMoser
источник
1

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

Клас. S
источник
1

Как новичок в 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), компилятор жалуется на несоответствие времени жизни. В нужном месте. В нужное время. Отладка на одном дыхании.

Йонас Дальбек
источник
0

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

Хорхе Гонсалес
источник