Для чого потрібні явні терміни життя в Русті?


199

Я читав розділ про життя книги «Іржа», і натрапив на цей приклад протягом названого / явного життя:

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;).

У яких випадках явні терміни життя фактично потрібні для запобігання помилок після використання (або якогось іншого класу?) Помилок?



2
Для майбутніх читачів цього питання, будь ласка, зверніть увагу, що воно посилається на перше видання книги, а тепер вже друге видання :)
carols10cents

Відповіді:


205

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

Це фактично те саме питання, що і "чому потрібні явні типи, коли компілятор може зробити їх висновком". Гіпотетичний приклад:

fn foo() -> _ {  
    ""
}

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

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

Функції - це природна межа для брандмауера наслідків зміни коду. Якби ми дозволили повністю перевірити життя за допомогою коду, то невинно виглядає зміна може вплинути на тривалість життя, що може призвести до помилок у функції далеко. Це не гіпотетичний приклад. Як я розумію, у Haskell є ця проблема, коли ви покладаєтесь на висновки типу для функцій верхнього рівня. Іржа стрибала цю проблему в зародку.

Існує також користь для компілятора - ефективність - для розпізнавання типів та строків життя потрібно розбирати лише підписи функцій. Що ще важливіше, це має перевагу ефективності для програміста. Якщо у нас не було явного життя, що робить ця функція:

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

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

шляхом виведення незаконного присвоєння посилання на ширший обсяг

Області застосування - це по суті життя. Трохи чіткіше, цілість життя 'a- це загальний параметр часу життя, який може спеціалізуватися на конкретному просторі під час компіляції на основі веб-сайту виклику.

чи потрібні явні терміни життя, щоб запобігти помилкам [...]?

Зовсім ні. Життя потрібні для запобігання помилок, але чіткі терміни життя потрібні для захисту того, що мало програмістів з розуму.


18
@jco Уявіть, що у вас є функція верхнього рівня f x = x + 1без підпису типу, який ви використовуєте в іншому модулі. Якщо згодом ви зміните визначення на f x = sqrt $ x + 1, його тип змінюється з Num a => a -> aна Floating a => a -> a, що призведе до помилок типу на всіх сайтах викликів, куди fвикликається, наприклад Intаргументом. Наявність підпису типу забезпечує, що помилки виникають локально.
fjh

11
"Сфера застосування - це, по суті, життя. Трохи чіткіше, цілість життя" a - це загальний параметр часу життя, який може бути спеціалізований з конкретною сферою на час виклику. " Нічого, це дійсно чудовий, освітлюючий момент. Мені б хотілося, якби це було чітко включено до книги.
corazza

2
@fjh Дякую Тільки для того, щоб побачити, чи я прихоплюю це - справа в тому, що якби тип був явно зазначений перед додаванням sqrt $, після зміни сталася б лише локальна помилка, а в інших місцях не було б багато помилок (що набагато краще, якби ми не не хочете змінити фактичний тип)?
corazza

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

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.


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] { ... }

Абсолютно немає способу компілятор міг би це зрозуміти.


IINM, висновок про термін експлуатації типу повернення двоаргументальної функції буде еквівалентним проблемі зупинки - IOW, не вирішується протягом певного часу.
дстромберг

4

Тут я знайшов ще одне чудове пояснення: http://doc.rust-lang.org/0.12.0/guide-lifasures.html#returning-references .

Взагалі посилання можна повернути лише в тому випадку, якщо вони виведені з параметра в процедуру. У цьому випадку результат вказівника завжди буде мати однаковий термін служби, як один з параметрів; названі терміни життя вказують, який саме параметр.


4

Якщо функція отримує два посилання в якості аргументів і повертає посилання, то реалізація функції може іноді повертати першу посилання, а іноді і другу. Неможливо передбачити, яка посилання буде повернута для даного дзвінка. У цьому випадку неможливо зробити термін експлуатації повернутої посилання, оскільки кожна посилання на аргумент може стосуватися різної прив'язки змінної з іншим терміном служби. Явне життя допомагає уникнути або прояснити таку ситуацію.

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

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


1

Причина, по якій ваш приклад не працює, полягає лише в тому, що у Русті є лише локальний термін служби та висновок про тип. Те, що ви пропонуєте, вимагає глобального висновку. Кожен раз, коли у вас є довідник, термін експлуатації якого не може бути закінчений, його потрібно помітити.


1

Як новачок у Расті, я розумію, що явні терміни життя служать двом цілям.

  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

Я вважаю, що довічна анотація як договір про дану відправку є дійсною в області отримання, поки вона залишається дійсною в межах джерела. Оголошення більшої кількості посилань за один і той же термін служби об'єднує сфери застосування, тобто всі джерела посилань повинні задовольняти цьому договору. Така анотація дозволяє компілятору перевірити на виконання договору.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.