Premessa : Questa risposta è stata scritta prima di opt-in incorporato tratti -specifically gli Copy
aspetti -Ve implementate. Ho usato le virgolette per indicare le sezioni che si applicavano solo al vecchio schema (quello che si applicava quando è stata posta la domanda).
Vecchio : per rispondere alla domanda di base, puoi aggiungere un campo indicatore che memorizza un NoCopy
valore . Per esempio
struct Triplet {
one: int,
two: int,
three: int,
_marker: NoCopy
}
Puoi anche farlo con un distruttore (tramite l'implementazione del Drop
tratto ), ma è preferibile usare i tipi di marker se il distruttore non sta facendo nulla.
I tipi ora si spostano per impostazione predefinita, ovvero quando definisci un nuovo tipo non viene implementato a Copy
meno che non lo implementi esplicitamente per il tuo tipo:
struct Triplet {
one: i32,
two: i32,
three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move
L'implementazione può esistere solo se ogni tipo contenuto nel nuovo struct
o enum
è esso stesso Copy
. In caso contrario, il compilatore stamperà un messaggio di errore. Può anche esistere solo se il tipo non ha Drop
un'implementazione.
Per rispondere alla domanda che non hai posto ... "che succede con le mosse e la copia?":
Per prima cosa definirò due diverse "copie":
- una copia in byte , che sta solo copiando superficialmente un oggetto byte per byte, non seguendo i puntatori, ad esempio se lo hai
(&usize, u64)
, è di 16 byte su un computer a 64 bit, e una copia superficiale prenderebbe quei 16 byte e replicherebbe i loro valore in qualche altro blocco di memoria da 16 byte, senza toccare usize
l'altra estremità del file &
. Cioè, è equivalente a chiamare memcpy
.
- una copia semantica , duplicando un valore per creare una nuova istanza (in qualche modo) indipendente che può essere tranquillamente utilizzata separatamente da quella vecchia. Ad esempio, una copia semantica di un
Rc<T>
comporta solo l'aumento del conteggio dei riferimenti, e una copia semantica di a Vec<T>
implica la creazione di una nuova allocazione, e quindi la copia semantica di ogni elemento memorizzato dal vecchio al nuovo. Questi possono essere copie profonde (ad esempio Vec<T>
) o superficiali (ad esempio Rc<T>
, non toccano ciò che è memorizzato T
), Clone
è definito liberamente come la più piccola quantità di lavoro richiesta per copiare semanticamente un valore di tipo T
da dentro a &T
a T
.
Rust è come C, ogni uso per valore di un valore è una copia in byte:
let x: T = ...;
let y: T = x; // byte copy
fn foo(z: T) -> T {
return z // byte copy
}
foo(y) // byte copy
Sono copie in byte indipendentemente dal fatto che si T
muovano o che siano "copiabili implicitamente". (Per essere chiari, non sono necessariamente copie letteralmente byte per byte in fase di esecuzione: il compilatore è libero di ottimizzare le copie se il comportamento del codice viene preservato.)
Tuttavia, c'è un problema fondamentale con le copie di byte: si finisce con valori duplicati in memoria, il che può essere pessimo se hanno distruttori, ad es.
{
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
} // destructors run here
Se w
fosse solo una semplice copia in byte di v
allora ci sarebbero due vettori che puntano alla stessa allocazione, entrambi con distruttori che lo liberano ... causando un doppio libero , il che è un problema. NB. Sarebbe perfettamente a posto, se facessimo una copia semantica di v
into w
, da allora w
sarebbe indipendente Vec<u8>
ei distruttori non si calpesterebbero a vicenda.
Ci sono alcune possibili soluzioni qui:
- Lascia che sia il programmatore a gestirlo, come C. (non ci sono distruttori in C, quindi non è così male ... invece ti rimangono solo perdite di memoria.: P)
- Esegui una copia semantica in modo implicito, in modo che
w
abbia la sua allocazione, come C ++ con i suoi costruttori di copia.
- Considera gli usi per valore come un trasferimento di proprietà, in modo che
v
non possa più essere utilizzato e non abbia il suo distruttore in esecuzione.
L'ultimo è ciò che fa Rust: una mossa è solo un uso per valore in cui il sorgente è staticamente invalidato, quindi il compilatore impedisce un ulteriore utilizzo della memoria ora non valida.
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value
I tipi che hanno distruttori devono spostarsi quando usati per valore (ovvero quando i byte copiati), poiché hanno la gestione / proprietà di alcune risorse (ad esempio un'allocazione di memoria o un handle di file) ed è molto improbabile che una copia di byte lo duplichi correttamente Proprietà.
"Beh ... cos'è una copia implicita?"
Pensa a un tipo primitivo come u8
: una copia di byte è semplice, basta copiare il singolo byte e una copia semantica è altrettanto semplice, copiare il singolo byte. In particolare, una copia di byte è una copia semantica ... Rust ha anche una caratteristica incorporataCopy
che cattura quali tipi hanno copie semantiche e byte identiche.
Quindi, per questi Copy
tipi gli usi in base al valore sono automaticamente anche copie semantiche, quindi è perfettamente sicuro continuare a utilizzare il sorgente.
let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine
Vecchio : il NoCopy
marcatore sovrascrive il comportamento automatico del compilatore di assumere che i tipi che possono essere Copy
(cioè contenenti solo aggregati di primitive e &
) lo siano Copy
. Tuttavia, questo cambierà quando verranno implementati i tratti incorporati di opt-in .
Come accennato in precedenza, vengono implementati i tratti incorporati opt-in, quindi il compilatore non ha più un comportamento automatico. Tuttavia, le regole utilizzate in passato per il comportamento automatico sono le stesse regole per verificare se è legale da implementare Copy
.