¿Las referencias mutables tienen semántica de movimiento?
fn main() {
let mut name = String::from("Charlie");
let x = &mut name;
let y = x; // x has been moved
say_hello(y);
say_hello(y); // but y has not been moved, it is still usable
change_string(y);
change_string(y);
}
fn say_hello(s: &str) {
println!("Hello {}", s);
}
fn change_string(s: &mut String) {
s.push_str(" Brown");
}
Cuando lo asigno x
a y
x
se ha movido. Sin embargo, esperaría que algo con semántica de movimiento se moviera cuando lo uso en una función. Sin embargo, todavía puedo usar la referencia después de llamadas posteriores. Tal vez esto tenga que ver con say_hello() tomando una referencia inmutable pero change_string() toma una referencia mutable pero la referencia aún no se mueve.
Tienes toda la razón tanto en tu razonamiento como en tus observaciones. Definitivamente parece que las cosas deberían suceder como usted las describe. Sin embargo, el compilador aplica aquí algo de magia de conveniencia.
La semántica de movimiento generalmente se aplica en Rust para todos los tipos que no implementan el Copy
rasgo. Las referencias compartidas son Copy
, por lo que simplemente se copian cuando se asignan o se pasan a una función. Las referencias mutables no lo son Copy
, por lo que deben moverse.
Ahí es donde comienza la magia. Siempre que se asigna una referencia mutable a un nombre con un tipo que el compilador ya sabe que es una referencia mutable, la referencia original se vuelve a tomar prestada implícitamente en lugar de moverse. Entonces la llamada a la función
change_string(y);
es transformado por el compilador para significar
change_string(&mut *y);
Se elimina la referencia original y se crea un nuevo préstamo mutable. Este nuevo préstamo se traslada a la función y el préstamo original se libera una vez que regresa la función.
Tenga en cuenta que esta no es una diferencia entre llamadas a funciones y asignaciones. Los nuevos préstamos implícitos ocurren siempre que el compilador ya sabe que el tipo de destino es una referencia mutable, por ejemplo, porque el patrón tiene una anotación de tipo explícita. Entonces, esta línea también crea un nuevo préstamo implícito, ya que la anotamos explícitamente como un tipo de referencia mutable:
let y: &mut _ = x;
Esta llamada a función, por otro lado, mueve (y por lo tanto consume) la referencia mutable y
:
fn foo<T>(_: T) {}
[...]
foo(y);
El tipo genérico T
aquí no es explícitamente un tipo de referencia mutable, por lo que no se produce ningún nuevo préstamo implícito, aunque el compilador infiera que el tipo es una referencia mutable, tal como en el caso de su tarea let y = x;
.
En algunos casos, el compilador puede inferir que un tipo genérico es una referencia mutable incluso en ausencia de una anotación de tipo explícita:
fn bar<T>(_a: T, _b: T) {}
fn main() {
let mut i = 42;
let mut j = 43;
let x = &mut i;
let y = &mut j;
bar(x, y); // Moves x, but reborrows y.
let _z = x; // error[E0382]: use of moved value: `x`
let _t = y; // Works fine.
}
Al inferir el tipo del primer parámetro, el compilador aún no sabe que es una referencia mutable, por lo que no se produce ningún nuevo préstamo implícito y x
se mueve a la función. Sin embargo, al llegar al segundo parámetro, el compilador ya ha inferido que T
es una referencia mutable, por lo que y
implícitamente lo vuelve a tomar prestado. (Este ejemplo es una buena ilustración de por qué agregar magia del compilador para que las cosas "simplemente funcionen" generalmente es una mala idea. Explícito es mejor que implícito).
Lamentablemente, este comportamiento no está documentado actualmente en la referencia de Rust .
Ver también:
- Cosas que hace la función de identidad (en Rust)
- Discusión del tema en el foro de usuarios de Rust.
- ¿Por qué no se mueve aquí la referencia mutable?