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 parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Questo fallirà con l'errore:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
Per comprendere completamente questo errore, è necessario pensare a come i valori sono rappresentati in memoria e cosa succede quando si spostano
tali valori. Annotiamo Combined::new
con alcuni ipotetici indirizzi di memoria che mostrano dove si trovano i valori:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
Cosa dovrebbe succedere child
? Se il valore fosse spostato come parent
era, farebbe riferimento alla memoria che non garantisce più di avere un valore valido. A qualsiasi altro pezzo di codice è consentito memorizzare valori all'indirizzo di memoria 0x1000. L'accesso alla memoria presupponendo che fosse un numero intero potrebbe causare arresti anomali e / o bug di sicurezza ed è una delle principali categorie di errori che Rust impedisce.
Questo è esattamente il problema che impedisce la vita . Una durata è 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. La durata della ruggine non è il periodo di tempo che intercorre tra la creazione di un oggetto e la sua distruzione!
Come analogia, pensala in questo modo: durante la vita di una persona, risiederanno in molti luoghi diversi, ognuno con un indirizzo distinto. La vita di Rust riguarda l'indirizzo in cui risiedi attualmente , non quando morirai in futuro (anche se morire cambia anche il tuo indirizzo). Ogni volta che ti 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 proverbio dice che "le vite sono descrittive, non prescrittive".
Annotiamo Combined::new
con alcuni numeri di riga che utilizzeremo per evidenziare le vite:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
La durata concreta di parent
va da 1 a 4, compreso (che rappresenterò come [1,4]
). La durata concreta di child
è [2,4]
e la durata concreta del valore di ritorno è [4,5]
. È possibile avere vite concrete che iniziano da zero, il che rappresenterebbe la durata di un parametro per una funzione o qualcosa che esisteva al di fuori del blocco.
Si noti che la durata di child
se stessa è [2,4]
, ma che si riferisce a un valore con una durata di [1,4]
. Questo va bene fino a quando il valore di riferimento diventa non valido prima del valore di riferimento. Il problema si verifica quando proviamo a tornare child
dal blocco. Ciò "prolungherebbe eccessivamente" la vita oltre la sua lunghezza naturale.
Questa nuova conoscenza dovrebbe spiegare i primi due esempi. Il terzo richiede di esaminare l'implementazione di Parent::child
. Le probabilità sono, sarà simile a questo:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Questo utilizza l' elisione a vita per evitare di scrivere parametri di durata generici espliciti . È equivalente a:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
In entrambi i casi, il metodo dice che Child
verrà restituita una struttura che è stata parametrizzata con la durata concreta di
self
. Detto in altro modo, l' Child
istanza contiene un riferimento a Parent
ciò che l'ha creata e quindi non può vivere più a lungo di tale
Parent
istanza.
Questo ci consente anche di riconoscere che qualcosa non va nella nostra funzione di creazione:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Sebbene tu abbia maggiori probabilità di vederlo 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 la durata con cui Combined
verrà parametrizzata non è vincolata da nulla: può essere qualunque cosa il chiamante voglia che sia. Questo è senza senso, perché il chiamante potrebbe specificare la 'static
durata e non c'è modo di soddisfare tale condizione.
Come lo aggiusto?
La soluzione più semplice e consigliata è quella di non tentare di mettere insieme questi elementi nella stessa struttura. In questo modo, l'annidamento della struttura imiterà la durata del codice. Metti insieme i tipi che possiedono i dati in una struttura e quindi fornisci metodi che ti consentano di ottenere riferimenti o oggetti contenenti riferimenti secondo necessità.
C'è un caso speciale in cui il monitoraggio della durata è troppo zelante: quando hai qualcosa inserito nell'heap. Ciò si verifica quando si utilizza a
Box<T>
, 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, non importa, poiché segui sempre il puntatore.
La cassa a noleggio (NON PIÙ MANTENUTA O SUPPORTATA) o la cassa owning_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 in heap.
Esempi di problemi risolti con il noleggio:
In altri casi, potresti voler passare a qualche tipo di conteggio dei riferimenti, ad esempio utilizzando Rc
o Arc
.
Maggiori informazioni
Dopo essersi spostati parent
nella struttura, perché il compilatore non è in grado di ottenere un nuovo riferimento parent
e assegnarlo alla child
struttura?
Sebbene sia teoricamente possibile farlo, ciò comporterebbe una grande complessità e costi generali. Ogni volta che l'oggetto 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 anche significare che 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 questo accada per ogni mossa, il programmatore può scegliere quando ciò accadrà creando metodi che prenderanno i riferimenti appropriati solo quando li chiamerai.
Un tipo con un riferimento a se stesso
C'è un caso specifico in cui è possibile creare un tipo con un riferimento a se stesso. Devi usare qualcosa di simile Option
per farlo in due passaggi:
#[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);
}
Funziona, in un certo senso, ma il valore creato è fortemente limitato: non può mai essere spostato. In particolare, ciò significa che non può essere restituito da una funzione o trasmesso valore per nulla. Una funzione di costruzione mostra lo stesso problema con le vite precedenti:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Che dire Pin
?
Pin
, stabilizzato in Rust 1.33, ha questo nella documentazione del modulo :
Un primo esempio di tale scenario sarebbe la costruzione di strutture autoreferenziali, poiché lo spostamento di un oggetto con puntatori su se stesso li invaliderebbe, il che potrebbe causare un comportamento indefinito.
È importante notare che "autoreferenziale" non significa necessariamente usare un riferimento . In effetti, l' esempio di una struttura autoreferenziale dice specificamente (enfasi sulla mia):
Non possiamo informarne il compilatore con un riferimento normale, poiché questo modello non può essere descritto con le consuete regole di assunzione. Invece , utilizziamo un puntatore non elaborato , anche se è noto che non è nullo, poiché sappiamo che punta alla stringa.
La capacità di utilizzare un puntatore non elaborato per questo comportamento esiste da Rust 1.0. In effetti, possedere-ref e noleggio usano puntatori grezzi sotto il cofano.
L'unica cosa che si Pin
aggiunge alla tabella è un modo comune per affermare che è garantito che un determinato valore non si sposti.
Guarda anche:
Parent
eChild
potrebbe aiutare ...