Perché sono necessarie vite esplicite in Rust?


199

Stavo leggendo il capitolo sul ciclo di vita del libro Rust, e mi sono imbattuto in questo esempio per una vita denominata / esplicita:

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

È abbastanza chiaro per me che l'errore che il compilatore ha evitato è l' uso libero dopo il riferimento assegnato a x: dopo che l'ambito interno è stato fatto, fquindi &f.xdiventa non valido e non avrebbe dovuto essere assegnato x.

Il mio problema è che il problema avrebbe potuto essere facilmente analizzato senza utilizzare la durata esplicita 'a , ad esempio inferendo un'assegnazione illegale di un riferimento a un ambito più ampio ( x = &f.x;).

In quali casi sono effettivamente necessarie vite esplicite per prevenire errori use-after-free (o qualche altra classe?)?


1
Questo messaggio è stato inviato a Reddit
Shepmaster il

2
Per i futuri lettori di questa domanda, ti preghiamo di notare che si collega alla prima edizione del libro e ora c'è una seconda edizione :)
carols10cents

Risposte:


205

Le altre risposte hanno tutti punti salienti ( l'esempio concreto di fjh in cui è necessaria una vita esplicita ), ma manca una cosa chiave: perché sono necessarie vite esplicite quando il compilatore ti dirà che hai sbagliato ?

Questa è in realtà la stessa domanda di "perché sono necessari tipi espliciti quando il compilatore può inferirli". Un esempio ipotetico:

fn foo() -> _ {  
    ""
}

Certo, il compilatore può vedere che sto restituendo un &'static str, quindi perché il programmatore deve digitarlo?

Il motivo principale è che mentre il compilatore può vedere cosa fa il tuo codice, non sa quale sia il tuo intento.

Le funzioni sono un limite naturale al firewall degli effetti della modifica del codice. Se dovessimo consentire che le vite siano completamente ispezionate dal codice, un cambiamento apparentemente innocuo potrebbe influire sulle vite, che potrebbero quindi causare errori in una funzione molto lontana. Questo non è un esempio ipotetico. A quanto ho capito, Haskell ha questo problema quando si fa affidamento sull'inferenza del tipo per le funzioni di livello superiore. La ruggine ha stroncato quel particolare problema sul nascere.

Il compilatore offre anche un vantaggio in termini di efficienza: per verificare i tipi e le durate è necessario analizzare solo le firme delle funzioni. Ancora più importante, ha un vantaggio in termini di efficienza per il programmatore. Se non abbiamo avuto vite esplicite, cosa fa questa funzione:

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

È impossibile dirlo senza ispezionare la fonte, il che andrebbe contro un numero enorme di migliori pratiche di codifica.

deducendo un'assegnazione illegale di un riferimento a un ambito più ampio

Scopes sono essenzialmente delle vite. Un po 'più chiaramente, una durata 'aè un parametro generico di durata che può essere specializzato con un ambito specifico in fase di compilazione, in base al sito della chiamata.

sono effettivamente necessarie vite esplicite per prevenire [...] errori?

Affatto. La vita è necessaria per prevenire errori, ma sono necessarie vite esplicite per proteggere ciò che hanno i programmatori di sanità mentale.


18
@jco Immagina di avere una funzione di alto livello f x = x + 1senza una firma del tipo che stai utilizzando in un altro modulo. Se successivamente si modifica la definizione in f x = sqrt $ x + 1, il suo tipo cambia da Num a => a -> aa Floating a => a -> a, il che causerà errori di tipo in tutti i siti di chiamata in cui fviene chiamato, ad esempio con un Intargomento. Avere una firma di tipo garantisce che si verifichino errori localmente.
fjh,

11
"Gli ambiti sono vite, in sostanza. Un po 'più chiaramente, una vita' a è un parametro di vita generico che può essere specializzato con un ambito specifico al momento della chiamata." Caspita, questo è davvero un ottimo punto illuminante. Mi piacerebbe se fosse stato incluso nel libro in modo esplicito.
corazza,

2
@fjh Grazie. Solo per vedere se lo sollevo - il punto è che se il tipo fosse stato esplicitamente dichiarato prima di aggiungere sqrt $, dopo la modifica si sarebbe verificato solo un errore locale e non molti errori in altri posti (il che è molto meglio se non lo facessimo vuoi cambiare il tipo attuale)?
corazza,

5
@jco Esatto. Non specificare un tipo significa che è possibile modificare accidentalmente l'interfaccia di una funzione. Questo è uno dei motivi per cui è fortemente consigliato annotare tutti gli elementi di livello superiore in Haskell.
fjh

5
Inoltre, se una funzione riceve due riferimenti e restituisce un riferimento, a volte potrebbe restituire il primo riferimento e talvolta il secondo. In questo caso è impossibile dedurre una durata per il riferimento restituito. Le vite esplicite aiutano a evitare / chiarire una situazione del genere.
MichaelMoser,

93

Diamo un'occhiata al seguente esempio.

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

Qui, le vite esplicite sono importanti. Questo viene compilato perché il risultato di fooha la stessa durata del suo primo argomento ( 'a), quindi potrebbe sopravvivere al suo secondo argomento. Questo è espresso dai nomi a vita nella firma di foo. Se hai passato gli argomenti nella chiamata al foocompilatore si lamenterebbe che ynon vive abbastanza a lungo:

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

L'annotazione a vita nella seguente struttura:

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

specifica che Fooun'istanza non deve sopravvivere al riferimento che contiene (x campo).

L'esempio che hai trovato nel libro di Rust non illustra questo perché fe le yvariabili vanno al di fuori dell'ambito allo stesso tempo.

Un esempio migliore sarebbe questo:

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

Ora fsopravvive davvero alla variabile indicata da f.x.


9

Nota che non ci sono vite esplicite in quel pezzo di codice, tranne la definizione della struttura. Il compilatore è perfettamente in grado di dedurre le vite main().

Nelle definizioni dei tipi, tuttavia, sono inevitabili vite esplicite. Ad esempio, qui c'è un'ambiguità:

struct RefPair(&u32, &u32);

Queste dovrebbero essere vite diverse o dovrebbero essere le stesse? Importa dal punto di vista dell'uso, struct RefPair<'a, 'b>(&'a u32, &'b u32)è molto diverso da struct RefPair<'a>(&'a u32, &'a u32).

Ora, per casi semplici, come quello che hai fornito, il compilatore potrebbe teoricamente eludere le vite come avviene in altri luoghi, ma tali casi sono molto limitati e non meritano una complessità aggiuntiva nel compilatore, e questo guadagno di chiarezza sarebbe al per lo meno discutibile.


2
Puoi spiegare perché sono molto diversi?
AB,

@AB Il secondo richiede che entrambi i riferimenti condividano la stessa durata. Ciò significa che refpair.1 non può vivere più a lungo di refpair.2 e viceversa, quindi entrambi i ref devono indicare qualcosa con lo stesso proprietario. Il primo richiede solo che RefPair sopravviva a entrambe le sue parti.
llogiq,

2
@AB, si compila perché entrambe le vite sono unificate - perché le vite locali sono più piccole 'static, 'staticpossono essere usate ovunque dove possano essere usate vite locali, quindi nel tuo esempio il psuo parametro di vita sarà inferito come vita locale di y.
Vladimir Matveev,

5
@AB RefPair<'a>(&'a u32, &'a u32)significa che 'asarà l'intersezione di entrambe le vite di ingresso, ovvero in questo caso la durata di y.
fjh

1
@llogiq "richiede che RefPair sopravviva a entrambe le sue parti"? Pensavo fosse il contrario ... a & u32 può ancora avere senso senza RefPair, mentre un RefPair con i suoi ref morti sarebbe strano.
qed

6

Il caso del libro è molto semplice dal punto di vista del design. L'argomento delle vite è considerato complesso.

Il compilatore non può facilmente dedurre la durata in una funzione con più argomenti.

Inoltre, la mia cassa opzionale ha un OptionBooltipo con un as_slicemetodo la cui firma è in realtà:

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

Non c'è assolutamente modo in cui il compilatore avrebbe potuto capirlo.


IINM, inferendo la durata del tipo restituito di una funzione a due argomenti sarà equivalente al problema di arresto - IOW, non decidibile in un tempo finito.
dstromberg,


4

Se una funzione riceve due riferimenti come argomenti e restituisce un riferimento, l'implementazione della funzione potrebbe talvolta restituire il primo riferimento e talvolta il secondo. È impossibile prevedere quale riferimento verrà restituito per una determinata chiamata. In questo caso, è impossibile dedurre una durata per il riferimento restituito, poiché ogni riferimento a argomento può fare riferimento a un'associazione variabile diversa con una durata diversa. Le vite esplicite aiutano a evitare o chiarire una situazione del genere.

Allo stesso modo, se una struttura contiene due riferimenti (come due campi membro), a volte una funzione membro della struttura può restituire il primo riferimento e talvolta il secondo. Ancora una volta le vite esplicite impediscono tali ambiguità.

In alcune semplici situazioni, esiste un'elisione a vita in cui il compilatore può dedurre vite.


1

Il motivo per cui il tuo esempio non funziona è semplicemente perché Rust ha solo una durata locale e un'inferenza di tipo. Quello che stai suggerendo richiede un'inferenza globale. Ogni volta che hai un riferimento la cui durata non può essere elusa, deve essere annotato.


1

Come nuovo arrivato in Rust, la mia comprensione è che le vite esplicite hanno due scopi.

  1. Inserendo un'annotazione di durata esplicita su una funzione si limita il tipo di codice che può apparire all'interno di quella funzione. Le vite esplicite consentono al compilatore di assicurarsi che il programma stia eseguendo ciò che intendevi.

  2. Se tu (il compilatore) vuoi verificare se un pezzo di codice è valido, tu (il compilatore) non dovrai guardare iterativamente all'interno di ogni funzione chiamata. È sufficiente dare un'occhiata alle annotazioni delle funzioni chiamate direttamente da quel pezzo di codice. Questo rende il tuo programma molto più facile da ragionare per te (il compilatore) e rende gestibili i tempi di compilazione.

Al punto 1., considera il seguente programma scritto in 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()

che stamperà

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

Questo tipo di comportamento mi sorprende sempre. Quello che sta succedendo è che dfsta condividendo la memoria ar, quindi quando parte del contenuto dei dfcambiamenti work, anche quel cambiamento infetta ar. Tuttavia, in alcuni casi questo può essere esattamente quello che vuoi, per motivi di efficienza della memoria (nessuna copia). Il vero problema in questo codice è che la funzionesecond_row sta restituendo la prima riga invece della seconda; buona fortuna debug.

Considera invece un programma simile scritto in 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);
}

Compilando questo, ottieni

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> {
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

In effetti si ottengono due errori, ce n'è anche uno con i ruoli di 'ae 'bscambiati. Osservando l'annotazione di second_row, troviamo che l'output dovrebbe essere &mut &'b mut [i32], ovvero, l'output dovrebbe essere un riferimento a un riferimento con la durata 'b(la durata della seconda riga di Array). Tuttavia, poiché stiamo restituendo la prima riga (che ha una durata 'a), il compilatore si lamenta della mancata corrispondenza della durata. Nel posto giusto Al momento giusto. Il debugging è un gioco da ragazzi.


0

Penso che un'annotazione a vita come un contratto su un dato riferimento sia valido nell'ambito di ricezione solo mentre rimane valido nell'ambito di origine. La dichiarazione di più riferimenti nello stesso tipo di vita unisce gli ambiti, il che significa che tutti i riferimenti sorgente devono soddisfare questo contratto. Tale annotazione consente al compilatore di verificare l'adempimento del contratto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.