rust - tipos - ¿Cuál es la diferencia entre pasar un valor a una función por referencia y pasarla por Box?
paso de parametros por valor y por referencia en c (3)
Cuando pasa un valor enmarcado, está moviendo el valor por completo. Ya no lo posees, lo que lo pasaste lo hace. Es así para cualquier tipo que no sea Copy
(datos antiguos y simples que pueden ser solo memcpy
''d, que una asignación de montón ciertamente no puede ser). Así es como funciona el modelo de propiedad de Rust: cada objeto es propiedad en exactamente un lugar.
Si desea cambiar el contenido del cuadro, debe pasar un &mut i32
lugar del Box<i32>
completo.
En realidad, Box<T>
solo es útil para estructuras de datos recursivas (para que puedan ser representadas en lugar de ser de tamaño infinito) y para la optimización del rendimiento muy ocasional en tipos grandes (que no debes intentar sin mediciones).
Para obtener &mut i32
de un Box<i32>
, tome una referencia mutable al cuadro desreferenciado, es decir, &mut *heap_a
.
¿Cuál es la diferencia entre pasar un valor a una función por referencia y pasarla "por cuadro"?
fn main() {
let mut stack_a = 3;
let mut heap_a = Box::new(3);
foo(&mut stack_a);
println!("{}", stack_a);
let r = foo2(&mut stack_a);
// compile error if the next line is uncommented
// println!("{}", stack_a);
bar(heap_a);
// compile error if the next line is uncommented
// println!("{}", heap_a);
}
fn foo(x: &mut i32) {
*x = 5;
}
fn foo2(x: &mut i32) -> &mut i32 {
*x = 5;
x
}
fn bar(mut x: Box<i32>) {
*x = 5;
}
¿Por qué heap_a
mueve a la función, pero stack_a
no está ( stack_a
todavía está disponible en la println!
Después de la llamada foo()
)?
El error al descomentar println!("{}", stack_a);
:
error[E0502]: cannot borrow `stack_a` as immutable because it is also borrowed as mutable
--> src/main.rs:10:20
|
8 | let r = foo2(&mut stack_a);
| ------- mutable borrow occurs here
9 | // compile error if the next line is uncommented
10 | println!("{}", stack_a);
| ^^^^^^^ immutable borrow occurs here
...
15 | }
| - mutable borrow ends here
Creo que este error se puede explicar haciendo referencia a vidas. En el caso de foo
, stack_a
(en la función main
) se mueve a la función foo
, pero el compilador encuentra que la duración del argumento de la función foo
, x: &mut i32
termina al final de foo
. Por lo tanto, nos permite usar la variable stack_a
en la función main
después de que devuelva foo
. En el caso de foo2
, stack_a
también se mueve a la función, pero también la devolvemos.
¿Por qué la vida útil de heap_a
termina al final de la bar
?
La diferencia entre pasar por referencia y "por caja" es que, en el caso de referencia ("prestar"), quien llama es responsable de desasignar el objeto, pero en el caso de caja ("mover"), el destinatario es responsable de la desasignación el objeto.
Por lo tanto, Box<T>
es útil para pasar objetos con la responsabilidad de desasignar, mientras que la referencia es útil para pasar objetos sin responsabilidad de desasignación.
Un ejemplo simple que demuestra estas ideas:
fn main() {
let mut heap_a = Box::new(3);
foo(&mut *heap_a);
println!("{}", heap_a);
let heap_b = Box::new(3);
bar(heap_b);
// can''t use `heap_b`. `heap_b` has been deallocated at the end of `bar`
// println!("{}", heap_b);
} // `heap_a` is destroyed here
fn foo(x: &mut i32) {
*x = 5;
}
fn bar(mut x: Box<i32>) {
*x = 5;
} // heap_b (now `x`) is deallocated here
Pass-by-value es siempre una copia (si el tipo involucrado es "trivial") o un movimiento (si no es así). Box<i32>
no se puede copiar porque (o al menos uno de sus miembros de datos) implementa Drop
. Esto se hace típicamente para algún tipo de código de "limpieza". Un Box<i32>
es un "puntero propietario". Es el único propietario de lo que apunta y es por eso que "se siente responsable" de liberar la memoria del i32
en su función de drop
. Imagine lo que sucedería si copiara un Box<i32>
: ahora, tendría dos instancias Box<i32>
apuntando a la misma ubicación de memoria. Esto sería malo porque esto llevaría a un error doblemente libre. Es por eso que bar(heap_a)
mueve la instancia de Box<i32>
a bar()
. De esta forma, siempre hay no más de un único propietario del i32
asignado en el montón. Y esto hace que administrar la memoria sea bastante simple: quienquiera que sea, lo libera eventualmente.
La diferencia con foo(&mut stack_a)
es que no pasas stack_a
por valor. Usted simplemente "presta" foo()
stack_a
de manera que foo()
puede mutarlo. Lo que obtiene foo()
es un puntero prestado . Cuando la ejecución vuelve de foo()
, stack_a
todavía está allí (y posiblemente modificado mediante foo()
). Puedes pensar que stack_a
volvió a su propio marco de pila porque foo()
simplemente lo tomó prestado por un tiempo.
La parte que parece confundirte es que al quitar el comentario de la última línea de
let r = foo2(&mut stack_a);
// compile error if uncomment next line
// println!("{}", stack_a);
en realidad no se prueba si stack_a
fue movido. stack_a
todavía está allí. El compilador simplemente no le permite acceder a él a través de su nombre porque todavía tiene una referencia prestada mutablemente: r
. Esta es una de las reglas que necesitamos para la seguridad de la memoria: solo puede haber una forma de acceder a una ubicación de memoria si también podemos modificarla. En este ejemplo, r
es una referencia tomada stack_a
para stack_a
. Entonces, stack_a
todavía se considera prestado mutablemente. La única forma de acceder a ella es a través de la referencia prestada r
.
Con algunas llaves adicionales, podemos limitar el tiempo de vida de esa referencia prestada r
:
let mut stack_a = 3;
{
let r = foo2(&mut stack_a);
// println!("{}", stack_a); WOULD BE AN ERROR
println!("{}", *r); // Fine!
} // <-- borrowing ends here, r ceases to exist
// No aliasing anymore => we''re allowed to use the name stack_a again
println!("{}", stack_a);
Después del paréntesis de cierre, nuevamente hay solo una forma de acceder a la ubicación de la memoria: el nombre stack_a
. ¡Es por eso que el compilador nos permite usarlo en println!
.
Ahora se puede preguntar, ¿cómo sabe el compilador que r
realidad se refiere a stack_a
? ¿Analiza la implementación de foo2
para eso? No. No hay necesidad. La firma de función de foo2
es suficiente para llegar a esta conclusión. Sus
fn foo2(x: &mut i32) -> &mut i32
que en realidad es la abreviatura de
fn foo2<''a>(x: &''a mut i32) -> &''a mut i32
de acuerdo con las llamadas "reglas de elisión de por vida". El significado de esta firma es: foo2()
es una función que toma un puntero prestado a algún i32
y devuelve un puntero prestado a un i32
que es el mismo i32
(o al menos una "parte" del i32
original) porque el el mismo parámetro de vida útil se utiliza para el tipo de devolución. Siempre que mantenga ese valor de retorno ( r
), el compilador considera stack_a prestado mutablemente.
Si estás interesado en por qué tenemos que rechazar el aliasing y la (potencial) mutación que ocurre al mismo tiempo que en algún lugar de la memoria, echa un vistazo a la gran charla de Niko .