2015-08-11 24 views
34

Sono un po 'confuso su come funzionano i puntatori in Rust. C'è ref, Box, &, * e non sono sicuro di come lavorano insieme.Comprensione e relazione tra Box, ref, & e *

Ecco come mi risulta attualmente:

  1. Box non è in realtà un puntatore - è un modo per allocare i dati sul mucchio, e passare intorno tipi non calibrati (tratti soprattutto) in argomenti della funzione.
  2. ref viene utilizzato nella corrispondenza del modello per prendere in prestito qualcosa su cui si combina, invece di prenderlo. Ad esempio,

    let thing: Option<i32> = Some(4); 
    match thing { 
        None => println!("none!"), 
        Some(ref x) => println!("{}", x), // x is a borrowed thing 
    } 
    println!("{}", x + 1); // wouldn't work without the ref since the block would have taken ownership of the data 
    
  3. & è usato per fare un prestito (puntatore prestito). Se ho una funzione fn foo(&self) allora mi sto riferendo a me stesso che scadrà dopo che la funzione si è conclusa, lasciando solo i dati del chiamante. Posso anche passare i dati di cui voglio mantenere la proprietà eseguendo bar(&mydata).

  4. * viene utilizzato per creare un puntatore non elaborato: ad esempio, let y: i32 = 4; let x = &y as *const i32. Comprendo i puntatori in C/C++ ma non sono sicuro di come funzioni con il sistema di tipi di Rust e di come possano essere utilizzati in modo sicuro. Inoltre non sono sicuro di quali siano i casi d'uso per questo tipo di puntatore. Inoltre, il simbolo * può essere utilizzato per il dereferenziamento delle cose (quali cose e perché?).

Qualcuno potrebbe spiegare il 4 ° tipo di puntatore a me e verificare che la mia comprensione degli altri tipi sia corretta? Gradirei anche che qualcuno sottolinei qualsiasi caso di utilizzo comune che non ho menzionato.

risposta

69

Prima di tutto, tutti gli elementi elencati sono cose molto diverse, anche se sono correlati ai puntatori. Box è un tipo di puntatore intelligente definito dalla libreria; ref è una sintassi per la corrispondenza del modello; & è un operatore di riferimento, che raddoppia come un sigillo nei tipi di riferimento; * è un operatore di dereferenziazione, che raddoppia come un sigillo nei tipi di puntatore non elaborati. Vedi sotto per ulteriori spiegazioni.

Ci sono quattro tipi di puntatore di base in Rust che possono essere suddivisi in due gruppi - i riferimenti e puntatori prime:

&T  - immutable (shared) reference 
&mut T - mutable (exclusive) reference 

*const T - immutable raw pointer 
*mut T - mutable raw pointer 

Differenza tra gli ultimi due è molto sottile perché o possono essere fusa ad un altro senza alcuna restrizione , quindi la distinzione const/mut serve principalmente come sfilacciamento. Puntatori grezzi possono essere creati liberamente per qualsiasi cosa, e possono anche essere creati dal nulla dagli interi, ad esempio.

Naturalmente, non è così per i riferimenti: i tipi di riferimento e la loro interazione definiscono una delle caratteristiche chiave di Rust, prendendo in prestito. I riferimenti hanno molte restrizioni su come e quando potrebbero essere creati, come potrebbero essere utilizzati e come interagiscono tra loro. In cambio possono essere utilizzati senza blocchi unsafe. Tuttavia, quale prestito è esattamente e come funziona è fuori dallo scopo di questa risposta.

Entrambi i riferimenti e puntatori prime possono essere creati utilizzando & operatore:

let x: u32 = 12; 

let ref1: &u32 = &x; 
let raw1: *const u32 = &x; 

let ref2: &mut u32 = &mut x; 
let raw2: *mut u32 = &mut x; 

Entrambi i riferimenti e puntatori prime possono essere dereferenziato utilizzando * operatore, anche se per i puntatori prime richiede un unsafe blocco:

*ref1; *ref2; 

unsafe { *raw1; *raw2; } 

L'operatore di riferimento viene spesso omesso perché un altro operatore, denominato punto ., fa automaticamente riferimento o dereferenzia l'argomento di sinistra.Così, per esempio, se abbiamo queste definizioni:

struct X { n: u32 }; 

impl X { 
    fn method(&self) -> u32 { self.n } 
} 

allora, nonostante che method() prende self per riferimento, self.n dereferenziazioni automaticamente, in modo da non dover digitare (*self).n. cosa simile accade quando method() si chiama:

let x = X { n: 12 }; 
let n = x.method(); 

Qui il compilatore fa riferimento automaticamente x in x.method(), in modo da non dovrà scrivere (&x).method().

Il penultimo codice mostra anche la sintassi speciale &self. Significa solo self: &Self o, più precisamente, self: &X in questo esempio. &mut self, *const self, *mut self funzionano anche.

Quindi, i riferimenti sono il tipo di puntatore principale in Rust e dovrebbero essere utilizzati quasi sempre. I puntatori grezzi, che non hanno restrizioni di riferimento, dovrebbero essere usati nel codice di basso livello che implementa le astrazioni di alto livello (collezioni, puntatori intelligenti, ecc.) E in FFI (interagendo con le librerie C).

Rust ha anche dynamically-sized (or unsized) types. Questi tipi non hanno una dimensione definita staticamente definita e quindi possono essere utilizzati solo tramite un puntatore/riferimento, tuttavia, solo un puntatore non è sufficiente - sono necessarie informazioni aggiuntive, ad esempio, la lunghezza per le sezioni o un puntatore a metodi virtuali tavolo per oggetti tratti. Questa informazione è "incorporata" nei puntatori ai tipi non registrati, rendendo questi puntatori "grassi".

Un puntatore di grasso è fondamentalmente una struttura che contiene il puntatore effettivo al pezzo di dati e alcune informazioni aggiuntive (lunghezza per sezioni, puntatore a vtable per oggetti tratto). Ciò che è importante qui è che Rust gestisce questi dettagli sul contenuto del puntatore in modo assolutamente trasparente per l'utente: se si superano i valori o *mut SomeTrait, le informazioni interne corrispondenti verranno automaticamente trasferite.

Box<T> è uno dei puntatori intelligenti nella libreria standard di Rust. Fornisce un modo per allocare memoria sufficiente sull'heap per memorizzare un valore del tipo corrispondente e quindi funge da handle, un puntatore a quella memoria. Box<T> possiede i dati a cui punta; quando viene rilasciato, il corrispondente pezzo di memoria sull'heap viene deallocato.

Un modo molto utile di pensare alle scatole è considerarle valori normali, ma con dimensioni fisse. Cioè, Box<T> equivale a solo T, tranne che prende sempre un numero di byte che corrispondono alla dimensione del puntatore del tuo computer. Diciamo che le scatole (di proprietà) forniscono la semantica valore. Internamente sono implementati usando puntatori grezzi, come quasi ogni altra astrazione di alto livello.

Box es (in realtà, questo è vero per quasi tutti gli altri puntatori intelligenti, come Rc) possono anche essere presi in prestito: è possibile ottenere un &T di Box<T>. Questo può succedere automaticamente con . operatore o si può farlo in modo esplicito dereferenziando e riferimento di nuovo:

let x: Box<u32> = Box::new(12); 
let y: &u32 = &*x; 

A questo proposito Box es sono simili a built-in puntatori - è possibile utilizzare dereference all'operatore di raggiungere i loro contenuti.Ciò è possibile perché l'operatore di dereferenziazione in Rust è sovraccarico ed è sovraccarico per la maggior parte (se non tutti) dei tipi di puntatore intelligente. Ciò consente un facile prestito di questi contenuti di puntatori.

Infine, ref è solo una sintassi nei modelli per ottenere una variabile di tipo di riferimento anziché valore. Ad esempio:

let x: u32 = 12; 

let y = x;   // y: u32, a copy of x 
let ref z = x;  // z: &u32, points to x 
let ref mut zz = x; // zz: &mut u32, points to x 

Mentre l'esempio di cui sopra può essere riscritta con operatori di riferimento:

let z = &x; 
let zz = &mut x; 

(che potrebbe anche rendere più idiomatica), ci sono casi in cui ref s indispensabili, per esempio, quando prendendo riferimenti in varianti enum:

let x: Option<Vec<u32>> = ...; 

match x { 
    Some(ref v) => ... 
    None => ... 
} 

nell'esempio sopra x è preso in prestito solo all'interno dell'intero match istruzione, che consente di utilizzare x dopo questo match. Se scriviamo come tale:

match x { 
    Some(v) => ... 
    None => ... 
} 

poi x saranno consumati da questa match e diventerà inutilizzabile dopo di esso.

+1

Risposta straordinaria, grazie. Sono stato in grado di fare un po 'più di ricerca per conto mio, e ho scoperto che il modo per sovraccaricare l'operazione di dereferenziazione è implementare la caratteristica 'Deref'. (Lasciando questo qui per chiunque altro legga la risposta ed è curioso). – zrneely

3

Riferimenti e puntatori grezzi sono la stessa cosa a livello di implementazione. La differenza dal punto di vista del programmatore è che i riferimenti sono sicuri (in termini di Rust), ma i puntatori grezzi non lo sono.

Le prendere in prestito garanzie checker che i riferimenti sono sempre validi (gestione vita), che si può avere un solo riferimento mutabile al momento, ecc

Questi tipo di vincolo può essere troppo stretto per molti casi d'uso, così crudo i puntatori (che non hanno vincoli, come in C/C++) sono utili per implementare strutture di dati di basso livello, e in generale roba di basso livello. Tuttavia, è possibile annullare la ricerca di puntatori grezzi o eseguire operazioni su di essi all'interno di un blocco unsafe.

I contenitori nella libreria standard vengono implementati utilizzando i puntatori raw, Box e Rc.

Box e Rc sono ciò che i puntatori intelligenti sono in C++, ovvero i wrapper attorno ai puntatori grezzi.

6

Box è logicamente un newtype attorno a un puntatore raw (*const T). Tuttavia, assegna e rilascia i suoi dati durante la costruzione e la distruzione, quindi non deve prendere in prestito dati da un'altra fonte.

La stessa cosa è vera per altri tipi di puntatori, come Rc - un puntatore conteggiato di riferimento. Si tratta di strutture contenenti puntatori raw privati ​​da cui vengono allocati e deallocati.

Un puntatore raw ha esattamente lo stesso layout di un puntatore normale, quindi non è compatibile con i puntatori C in diversi casi. È importante sottolineare che *const str e *const [T] sono punti grassi, il che significa che contengono informazioni aggiuntive sulla lunghezza del valore.

Tuttavia, i puntatori grezzi non offrono assolutamente alcuna garanzia sulla loro validità. Ad esempio, posso tranquillamente fare

123 as *const String 

Questo puntatore non è valido, dal momento che la posizione di memoria 123 non punta a una valida String. Pertanto, durante la dereferenziazione di uno, è richiesto un blocco unsafe.

Inoltre, mentre i mutuatari sono tenuti a rispettare determinate leggi, vale a dire che non è possibile avere più mutuatari se uno è mutabile, i puntatori grezzi non devono rispettarlo. There are other, weaker, laws that must be obeyed, ma è meno probabile che tu possa scappare di questi.

Non c'è alcuna differenza logica tra *mut e *const, anche se potrebbe essere necessario eseguire il cast sull'altro per eseguire determinate operazioni: la differenza è documentativa.