Prefacio : esta respuesta se escribió antes de que se implementaran los rasgos incorporados opcionales, específicamente los Copy
aspectos . He usado comillas en bloque para indicar las secciones que solo se aplicaban al esquema anterior (el que se aplicaba cuando se hizo la pregunta).
Antiguo : para responder a la pregunta básica, puede agregar un campo de marcador que almacene un NoCopy
valor . P.ej
struct Triplet {
one: int,
two: int,
three: int,
_marker: NoCopy
}
También puede hacerlo teniendo un destructor (mediante la implementación del Drop
rasgo ), pero se prefiere usar los tipos de marcador si el destructor no está haciendo nada.
Los tipos ahora se mueven de forma predeterminada, es decir, cuando define un nuevo tipo, no se implementa a Copy
menos que lo implemente explícitamente para su tipo:
struct Triplet {
one: i32,
two: i32,
three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move
La implementación solo puede existir si todos los tipos contenidos en el nuevo struct
o enum
son ellos mismos Copy
. De lo contrario, el compilador imprimirá un mensaje de error. También puede existir solo si el tipo no tiene una Drop
implementación.
Para responder a la pregunta que no hiciste ... "¿qué pasa con los movimientos y la copia?":
En primer lugar, definiré dos "copias" diferentes:
- una copia de bytes , que es simplemente copiar superficialmente un objeto byte por byte, sin seguir punteros, por ejemplo, si tiene
(&usize, u64)
, son 16 bytes en una computadora de 64 bits, y una copia superficial sería tomar esos 16 bytes y replicar sus valor en algún otro bloque de memoria de 16 bytes, sin tocar el usize
en el otro extremo del &
. Es decir, equivale a llamar memcpy
.
- una copia semántica , duplicando un valor para crear una nueva instancia (algo) independiente que se puede usar de forma segura por separado de la anterior. Por ejemplo, una copia semántica de a
Rc<T>
implica simplemente aumentar el recuento de referencias, y una copia semántica de a Vec<T>
implica crear una nueva asignación y luego copiar semánticamente cada elemento almacenado del antiguo al nuevo. Pueden ser copias profundas (p Vec<T>
. Ej. ) O superficiales (p. Ej. Rc<T>
, No toca lo almacenado T
), Clone
se define vagamente como la menor cantidad de trabajo necesaria para copiar semánticamente un valor de tipo T
desde dentro de &T
a T
.
Rust es como C, cada uso por valor de un valor es una copia de byte:
let x: T = ...;
let y: T = x; // byte copy
fn foo(z: T) -> T {
return z // byte copy
}
foo(y) // byte copy
Son copias de bytes ya sea que T
mueven como son "copiables implícitamente". (Para ser claros, no son necesariamente copias literalmente byte por byte en tiempo de ejecución: el compilador es libre de optimizar las copias si se conserva el comportamiento del código).
Sin embargo, hay un problema fundamental con las copias de bytes: terminas con valores duplicados en la memoria, lo que puede ser muy malo si tienen destructores, p. Ej.
{
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
} // destructors run here
Si w
fuera solo una copia de byte simple, v
habría dos vectores apuntando a la misma asignación, ambos con destructores que lo liberan ... causando un doble libre , lo cual es un problema. NÓTESE BIEN. Esto estaría perfectamente bien, si hiciéramos una copia semántica de v
into w
, ya que entonces w
serían independientes Vec<u8>
y los destructores no se pisotearían entre sí.
Aquí hay algunas posibles correcciones:
- Deje que el programador lo maneje, como C. (no hay destructores en C, por lo que no es tan malo ... simplemente se queda con pérdidas de memoria.: P)
- Realiza una copia semántica implícitamente, de modo que
w
tenga su propia asignación, como C ++ con sus constructores de copia.
- Considere los usos por valor como una transferencia de propiedad, por lo que
v
ya no se puede usar y no se ejecuta su destructor.
Lo último es lo que hace Rust: un movimiento es solo un uso por valor donde la fuente está invalidada estáticamente, por lo que el compilador evita el uso posterior de la memoria ahora no válida.
let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value
Los tipos que tienen destructores deben moverse cuando se usan por valor (también conocido como cuando se copian bytes), ya que tienen administración / propiedad de algún recurso (por ejemplo, una asignación de memoria o un identificador de archivo) y es muy poco probable que una copia de bytes duplique correctamente esto propiedad.
"Bueno ... ¿qué es una copia implícita?"
Piense en un tipo primitivo como u8
: una copia de byte es simple, simplemente copie el byte único, y una copia semántica es igual de simple, copie el byte único. En particular, una copia de bytes es una copia semántica ... Rust incluso tiene un rasgo incorporadoCopy
que captura qué tipos tienen copias semánticas y de bytes idénticas.
Por lo tanto, para estos Copy
tipos, los usos por valor también son copias semánticas automáticamente, por lo que es perfectamente seguro continuar usando la fuente.
let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine
Antiguo : el NoCopy
marcador anula el comportamiento automático del compilador de asumir que los tipos que pueden ser Copy
(es decir, que solo contienen agregados de primitivas y &
) lo son Copy
. Sin embargo, esto cambiará cuando se implementen los rasgos incorporados opcionales .
Como se mencionó anteriormente, se implementan rasgos incorporados opt-in, por lo que el compilador ya no tiene comportamiento automático. Sin embargo, la regla utilizada para el comportamiento automático en el pasado son las mismas reglas para verificar si su implementación es legal Copy
.