¿Cómo proporciona Rust la semántica de movimiento?
move-semantics (2)
El sitio web de Rust Language afirma que la semántica se mueve como una de las características del lenguaje. Pero no puedo ver cómo se implementa la semántica de movimiento en Rust.
Las cajas de óxido son el único lugar donde se utilizan semánticas de movimiento.
let x = Box::new(5);
let y: Box<i32> = x; // x is ''moved''
El código Rust anterior se puede escribir en C ++ como
auto x = std::make_unique<int>();
auto y = std::move(x); // Note the explicit move
Por lo que sé (corrígeme si me equivoco),
- Rust no tiene constructores en absoluto, y mucho menos mover constructores.
- No hay soporte para referencias de valor.
- No hay manera de crear sobrecargas de funciones con parámetros rvalue.
¿Cómo proporciona Rust la semántica de movimiento?
Creo que es un problema muy común cuando viene de C ++. En C ++ estás haciendo todo explícitamente cuando se trata de copiar y mover. El lenguaje fue diseñado alrededor de copia y referencias. Con C ++ 11, la capacidad de "mover" cosas estaba pegada a ese sistema. Rust por otro lado tomó un nuevo comienzo.
Rust no tiene constructores en absoluto, y mucho menos mover constructores.
No necesitas mover constructores. Rust mueve todo lo que "no tiene un constructor de copia", alias "no implementa el rasgo de Copy
".
struct A;
fn test() {
let a = A;
let b = a;
let c = a; // error, a is moved
}
El constructor predeterminado de Rust es (por convención) simplemente una función asociada llamada new
:
struct A(i32);
impl A {
fn new() -> A {
A(5)
}
}
Los constructores más complejos deberían tener nombres más expresivos. Este es el lenguaje constructor nombrado en C ++
No hay soporte para referencias de valor.
Siempre ha sido una característica solicitada, consulte el problema de RFC 998 , pero lo más probable es que solicite una característica diferente: mover cosas a funciones:
struct A;
fn move_to(a: A) {
// a is moved into here, you own it now.
}
fn test() {
let a = A;
move_to(a);
let c = a; // error, a is moved
}
No hay manera de crear sobrecargas de funciones con parámetros rvalue.
Puedes hacer eso con rasgos.
trait Ref {
fn test(&self);
}
trait Move {
fn test(self);
}
struct A;
impl Ref for A {
fn test(&self) {
println!("by ref");
}
}
impl Move for A {
fn test(self) {
println!("by value");
}
}
fn main() {
let a = A;
(&a).test(); // prints "by ref"
a.test(); // prints "by value"
}
La semántica de movimiento y copia de Rust es muy diferente de C ++. Voy a tomar un enfoque diferente para explicarlos que la respuesta existente.
En C ++, la copia es una operación que puede ser arbitrariamente compleja, debido a los constructores de copia personalizados. Rust no quiere semánticas personalizadas de asignación simple o transmisión de argumentos, por lo que adopta un enfoque diferente.
Primero, una asignación o argumento que pasa en Rust es siempre una simple copia de memoria.
let foo = bar; // copies the bytes of bar to the location of foo (might be elided)
function(foo); // copies the bytes of foo to the parameter location (might be elided)
Pero, ¿y si el objeto controla algunos recursos? Digamos que estamos tratando con un simple puntero inteligente, Box
.
let b1 = Box::new(42);
let b2 = b1;
En este punto, si solo se copian los bytes, ¿no se llamaría al destructor ( drop
Rust) para cada objeto, liberando así el mismo puntero dos veces y causando un comportamiento indefinido?
La respuesta es que Rust se mueve por defecto. Esto significa que copia los bytes a la nueva ubicación, y el objeto antiguo desaparece. Es un error de compilación acceder a b1
después de la segunda línea de arriba. Y el destructor no es llamado para ello. El valor se movió a b2
, y b1
podría no existir más.
Así es como funciona la semántica de movimiento en Rust. Los bytes se copian y el objeto antiguo desaparece.
En algunas discusiones sobre la semántica del movimiento de C ++, el camino de Rust se denominó "movimiento destructivo". Ha habido propuestas para agregar el "destructor de movimiento" o algo similar a C ++ para que pueda tener la misma semántica. Pero mover la semántica a medida que se implementan en C ++ no hace esto. El viejo objeto se queda atrás, y su destructor todavía se llama. Por lo tanto, necesita un constructor de movimiento para tratar con la lógica personalizada requerida por la operación de movimiento. Mover es solo un constructor especializado / operador de asignación que se espera que se comporte de cierta manera.
Entonces, de forma predeterminada, la asignación de Rust mueve el objeto, haciendo que la ubicación anterior no sea válida. Pero muchos tipos (enteros, puntos flotantes, referencias compartidas) tienen una semántica donde copiar los bytes es una forma perfectamente válida de crear una copia real, sin necesidad de ignorar el objeto anterior. Tales tipos deberían implementar el rasgo de Copy
, que puede ser derivado por el compilador automáticamente.
#[derive(Copy)]
struct JustTwoInts {
one: i32,
two: i32,
}
Esto indica al compilador que la asignación y el paso de argumentos no invalidan el objeto anterior:
let j1 = JustTwoInts { one: 1, two: 2 };
let j2 = j1;
println!("Still allowed: {}", j1.one);
Tenga en cuenta que la copia trivial y la necesidad de destrucción se excluyen mutuamente; un tipo que es Copy
no puede también ser Drop
.
Ahora, ¿qué ocurre cuando no desea hacer una copia de algo donde simplemente copiar los bytes no es suficiente, por ejemplo, un vector? No hay ninguna característica de idioma para esto; técnicamente, el tipo solo necesita una función que devuelva un nuevo objeto que se creó de la manera correcta. Pero, por convención, esto se logra implementando el rasgo de Clone
y su función de clone
. De hecho, el compilador también soporta la derivación automática de Clone
, donde simplemente clona todos los campos.
#[Derive(Clone)]
struct JustTwoVecs {
one: Vec<i32>,
two: Vec<i32>,
}
let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();
Y siempre que derive Copy
, también debería derivar Clone
, porque los contenedores como Vec
usan internamente cuando se clonan ellos mismos.
#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }
Ahora, ¿hay desventajas a esto? Sí, de hecho, hay un inconveniente bastante grande: debido a que mover un objeto a otra ubicación de memoria solo se realiza mediante la copia de bytes, y sin lógica personalizada, un tipo no puede tener referencias en sí mismo . De hecho, el sistema de por vida de Rust hace imposible construir tales tipos de manera segura.
Pero en mi opinión, la compensación vale la pena.