Perché non posso memorizzare un valore e un riferimento a quel valore nella stessa struttura?

Ho un valore e voglio memorizzare quel valore e un riferimento a qualcosa all’interno di quel valore nel mio tipo:

struct Thing { count: u32, } struct Combined(Thing, &'a u32); fn make_combined() -> Combined { let thing = Thing { count: 42 }; Combined(thing, &thing.count) } 

A volte, ho un valore e voglio memorizzare quel valore e un riferimento a quel valore nella stessa struttura:

 struct Combined(Thing, &'a Thing); fn make_combined() -> Combined { let thing = Thing::new(); Combined(thing, &thing) } 

A volte, non sto nemmeno prendendo un riferimento al valore e ottengo lo stesso errore:

 struct Combined(Parent, Child); fn make_combined() -> Combined { let parent = Parent::new(); let child = parent.child(); Combined(parent, child) } 

In ognuno di questi casi, ottengo un errore sul fatto che uno dei valori “non vive abbastanza a lungo”. Cosa significa questo errore?

    Diamo un’occhiata a una semplice implementazione di questo :

     struct Parent { count: u32, } struct Child<'a> { parent: &'a Parent, } struct Combined<'a> { parent: Parent, child: Child<'a>, } impl<'a> Combined<'a> { fn new() -> Self { let p = Parent { count: 42 }; let c = Child { parent: &p }; Combined { parent: p, child: c } } } fn main() {} 

    Ciò non riuscirà con l’errore leggermente ripulito:

     error: `p` does not live long enough --> src/main.rs:17:34 | 17 | let c = Child { parent: &p }; | ^ | note: reference must be valid for the lifetime 'a as defined on the block at 15:21... --> src/main.rs:15:22 | 15 | fn new() -> Self { | ^ note: ...but borrowed value is only valid for the block suffix following statement 0 at 16:37 --> src/main.rs:16:38 | 16 | let p = Parent { count: 42 }; | ^ 

    Per comprendere completamente questo errore, devi pensare a come i valori sono rappresentati in memoria e cosa succede quando sposti quei valori. Annotiamo Combined::new con alcuni indirizzi di memoria ipotetici che mostrano dove si trovano i valori:

     let p = Parent { count: 42 }; // `p` lives at address 0x1000 and takes up 4 bytes // The value of `p` is 42 let c = Child { parent: &p }; // `c` lives at address 0x1010 and takes up 4 bytes // The value of `c` is 0x1000 Combined { parent: p, child: c } // The return value lives at address 0x2000 and takes up 8 bytes // `p` is moved to 0x2000 // `c` is ... ? 

    Cosa dovrebbe succedere a c ? Se il valore è stato appena spostato come p , allora si riferirebbe alla memoria che non è più garantito avere un valore valido in essa. Qualsiasi altra parte di codice è autorizzata a memorizzare valori all’indirizzo di memoria 0x1000. Accedere a quella memoria supponendo che fosse un intero potrebbe portare a crash e / o bug di sicurezza, ed è una delle principali categorie di errori che Rust impedisce.

    Questo è esattamente il problema che le vite impediscono. Una vita è un po ‘di metadati che consente a te e al compilatore di sapere per quanto tempo un valore sarà valido nella sua posizione di memoria corrente . Questa è una distinzione importante, in quanto è un errore comune che fanno i nuovi arrivati ​​di Rust. Le durate della rust non sono il periodo di tempo tra quando un object viene creato e quando viene distrutto!

    Come un’analogia, pensala in questo modo: durante la vita di una persona, risiederanno in molti luoghi diversi, ciascuno con un indirizzo distinto. Una vita di rust riguarda l’indirizzo in cui attualmente risiedi , non circa ogni volta che morirai in futuro (anche se muore cambia anche il tuo indirizzo). Ogni volta che lo sposti è rilevante perché il tuo indirizzo non è più valido.

    È anche importante notare che le vite non cambiano il tuo codice; il tuo codice controlla le vite, le tue vite non controllano il codice. Il detto conciso è “le vite sono descrittive, non prescrittive”.

    Annotiamo Combined::new con alcuni numeri di linea che utilizzeremo per evidenziare le vite:

     { // 0 let p = Parent { count: 42 }; // 1 let c = Child { parent: &p }; // 2 // 3 Combined { parent: p, child: c } // 4 } // 5 

    La durata concreta di p è p tra 1 e 4 inclusi (che rappresenterò come [1,4] ). La durata concreta di c è [2,4] e la durata di vita del calcestruzzo del valore di ritorno è [4,5] . È ansible avere vite concrete che iniziano da zero – ciò rappresenterebbe la durata di un parametro per una funzione o qualcosa che esisteva al di fuori del blocco.

    Si noti che il tempo di vita di c stesso è [2,4] , ma che si riferisce a un valore con una durata di [1,4] . Questo va bene fintanto che il valore di riferimento diventa non valido prima del valore riferito. Il problema si verifica quando proviamo a restituire c dal blocco. Ciò “estenderebbe” eccessivamente la durata oltre la sua lunghezza naturale.

    Questa nuova conoscenza dovrebbe spiegare i primi due esempi. Il terzo richiede l’implementazione di Parent::child . Le probabilità sono, assomiglierà a questo:

     impl Parent { fn child(&self) -> Child { ... } } 

    Questo usa elision per tutta la vita per evitare di scrivere parametri generici di durata generica . È equivalente a:

     impl Parent { fn child<'a>(&'a self) -> Child<'a> { ... } } 

    In entrambi i casi, il metodo dice che verrà restituita una struttura Child che è stata parametrizzata con la durata concreta del self . Detto in un altro modo, l’istanza Child contiene un riferimento al Parent che lo ha creato, e quindi non può vivere più a lungo di Parent .

    Questo ci consente anche di riconoscere che qualcosa è veramente sbagliato nella nostra funzione di creazione:

     fn make_combined<'a>() -> Combined<'a> { ... } 

    Anche se è più probabile che tu veda questo scritto in una forma diversa:

     impl<'a> Combined<'a> { fn new() -> Combined<'a> { ... } } 

    In entrambi i casi, non viene fornito alcun parametro di durata tramite un argomento. Ciò significa che il tempo di vita in cui Combined verrà parametrizzato non è vincolato da nulla: può essere qualunque cosa voglia chiamare il chiamante. Questo è privo di senso, perché il chiamante potrebbe specificare la 'static durata 'static e non c’è modo di soddisfare tale condizione.

    Come lo aggiusto?

    La soluzione più semplice e consigliata è non tentare di mettere insieme questi elementi nella stessa struttura. In questo modo, la nidificazione della struttura simulerà la durata del codice. Inserire i tipi che possiedono i dati in una struttura e quindi fornire metodi che consentono di ottenere riferimenti o oggetti contenenti riferimenti secondo necessità.

    Esiste un caso particolare in cui il tracciamento della durata è troppo zelante: quando si dispone di qualcosa posto nell’heap. Ciò si verifica quando si utilizza una Box , ad esempio. In questo caso, la struttura che viene spostata contiene un puntatore nell’heap. Il valore puntato rimarrà stabile, ma l’indirizzo del puntatore stesso si sposterà. In pratica, questo non ha importanza, come si segue sempre il puntatore.

    La cassa di noleggio o la gabbia own_ref sono modi di rappresentare questo caso, ma richiedono che l’indirizzo di base non si sposti mai . Questo esclude i vettori mutanti, che possono causare una riallocazione e uno spostamento dei valori allocati all’heap.

    Maggiori informazioni

    Dopo aver spostato p nella struct, perché il compilatore non è in grado di ottenere un nuovo riferimento a p e assegnarlo a c nella struct?

    Mentre è teoricamente ansible farlo, farebbe introdurre una grande quantità di complessità e sovraccarico. Ogni volta che l’object viene spostato, il compilatore dovrebbe inserire il codice per “correggere” il riferimento. Ciò significherebbe che copiare una struttura non è più un’operazione molto economica che sposta solo alcuni bit. Potrebbe persino significare che il codice come questo è costoso, a seconda di quanto sarebbe buono un ipotetico ottimizzatore:

     let a = Object::new(); let b = a; let c = b; 

    Invece di forzare ciò per ogni mossa, il programmatore può scegliere quando questo accadrà creando metodi che prenderanno i riferimenti appropriati solo quando li chiamerai.


    C’è un caso specifico in cui è ansible creare un tipo con un riferimento a se stesso. Devi usare qualcosa come Option per farlo in due passi però:

     #[derive(Debug)] struct WhatAboutThis<'a> { name: String, nickname: Option<&'a str>, } fn main() { let mut tricky = WhatAboutThis { name: "Annabelle".to_string(), nickname: None, }; tricky.nickname = Some(&tricky.name[..4]); println!("{:?}", tricky); } 

    Questo funziona, in un certo senso, ma il valore creato è fortemente limitato – non può mai essere spostato. In particolare, questo significa che non può essere restituito da una funzione o passato per valore a qualcosa. Una funzione di costruzione mostra lo stesso problema con le durate come sopra:

     fn creator<'a>() -> WhatAboutThis<'a> { // ... } 

    Un problema leggermente diverso che causa messaggi del compilatore molto simili è la dipendenza dalla durata dell’object, piuttosto che memorizzare un riferimento esplicito. Un esempio di ciò è la libreria ssh2 . Quando si sviluppa qualcosa di più grande di un progetto di test, si è tentati di provare a mettere la Session e il Channel ottenuti da quella sessione uno accanto all’altro in una struttura, nascondendo i dettagli di implementazione all’utente. Tuttavia, si noti che la definizione del Channel ha la durata 'sess nel suo tipo di annotazione, mentre la Session non lo fa.

    Ciò causa errori del compilatore simili correlati alla durata.

    Un modo per risolverlo in un modo molto semplice è dichiarare la Session all’esterno nel chiamante, e quindi annotare il riferimento all’interno della struct con una vita, simile alla risposta in questo post di Rust User’s Forum che parla dello stesso problema mentre incapsula SFTP. Questo non sembrerà elegante e potrebbe non essere sempre applicabile – perché ora hai due entity framework da affrontare, piuttosto che una che tu volevi!

    Risulta che la cassa di noleggio o la cassa own_ref dall’altra risposta sono le soluzioni anche per questo problema. Consideriamo il own_ref, che ha l’object speciale per questo esatto scopo: OwningHandle . Per evitare lo spostamento dell’object sottostante, lo allociamo sull’heap utilizzando una Box , che ci offre la seguente soluzione ansible:

     use ssh2::{Channel, Error, Session}; use std::net::TcpStream; use owning_ref::OwningHandle; struct DeviceSSHConnection { tcp: TcpStream, channel: OwningHandle, Box>>, } impl DeviceSSHConnection { fn new(targ: &str, c_user: &str, c_pass: &str) -> Self { use std::net::TcpStream; let mut session = Session::new().unwrap(); let mut tcp = TcpStream::connect(targ).unwrap(); session.handshake(&tcp).unwrap(); session.set_timeout(5000); session.userauth_password(c_user, c_pass).unwrap(); let mut sess = Box::new(session); let mut oref = OwningHandle::new_with_fn( sess, unsafe { |x| Box::new((*x).channel_session().unwrap()) }, ); oref.shell().unwrap(); let ret = DeviceSSHConnection { tcp: tcp, channel: oref, }; ret } } 

    Il risultato di questo codice è che non possiamo più usare la Session , ma è memorizzata insieme al Channel che useremo. Poiché l’object OwningHandle dereferenzia a Box , quali dereferenze al Channel , quando lo si memorizza in una struttura, lo chiamiamo come tale. NOTA: questa è solo la mia comprensione. Ho il sospetto che questo potrebbe non essere corretto, dal momento che sembra essere molto vicino alla discussione sulla OwningHandle di OwningHandle OwningHandle .

    Un dettaglio curioso qui è che la Session ha logicamente una relazione simile con TcpStream come Channel ha a Session , tuttavia la sua proprietà non è presa e non ci sono annotazioni di tipo intorno a farlo. Invece, spetta all’utente curare questo, come dice la documentazione del metodo handshake :

    Questa sessione non assume la proprietà del socket fornito, si consiglia di assicurarsi che il socket permanga la durata di questa sessione per assicurare che la comunicazione sia eseguita correttamente.

    Si raccomanda inoltre vivamente che lo stream fornito non venga utilizzato contemporaneamente altrove per la durata di questa sessione in quanto potrebbe interferire con il protocollo.

    Quindi, con l’utilizzo di TcpStream , è completamente al programmatore garantire la correttezza del codice. Con OwningHandle , l’attenzione su dove avviene la “magia pericolosa” viene disegnata usando il blocco unsafe {} .

    Un’ulteriore discussione ad alto livello su questo problema si trova in questo thread di Rust User’s Forum , che include un esempio diverso e la sua soluzione che utilizza la cassa di noleggio, che non contiene blocchi non sicuri.