1.5 Prestar el trabajo
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
Pasar el trabajo de mano en mano es como funciona un taller. Un aprendiz pule, otro examina - cada uno hace su parte, y nadie explica la regla porque se sobreentiende: quien recibe la pieza es quien la controla. La tarea lleva un nombre, una forma y un estado. Ha llegado el momento de pasarla - y de descubrir en qué se diferencia prestar de ceder. Lo que se da por sentado rara vez se explica de antemano.
El intento
Hasta ahora, toda la lógica vivía en main: crear una tarea a través de sus campos y cambiar
task.status directamente. Esto es difícil de leer - requiere comentarios explícitos - y más
difícil aún de mantener: con muchas tareas, habría que rastrear y actualizar cada pieza de
lógica individualmente. La solución es directa: extraer funciones separadas - una para crear
una tarea, otra para completarla.
La primera - create_task. Recibe un id y un título, y devuelve una nueva tarea:
fn create_task(id: u64, title: &str) -> Task {
Task {
id,
title: String::from(title),
status: Status::Todo,
}
}
title: &str es una cadena pasada por referencia: la función la lee pero no la toma.
String::from(title) crea una copia propia - igual que hacía main antes. Esto compila.
En la mayoría de lenguajes una función devuelve un valor con return. En Rust, return
también existe, pero rara vez hace falta: la última expresión del cuerpo de una función,
escrita sin punto y coma, es el valor de retorno. create_task funciona exactamente así -
el constructor Task { ... } es el último y no tiene punto y coma, así que la función lo
devuelve al llamador.
La segunda - task_complete. Como mostró el capítulo anterior, cambiar un valor requiere
mut - y eso se aplica también a los argumentos de función:
fn task_complete(mut task: Task) {
task.status = Status::Done;
}
El parámetro mut task: Task - la tarea se recibe por valor, y mut permite modificarla
dentro de la función. El espejo de create_task: la última línea termina con ;, así que
la función no devuelve nada. create_task dejó Task { ... } sin ; y devolvió la tarea;
task_complete añade ; y no devuelve nada. El compilador lo acepta. Prueba a usar ambas:
fn main() {
let mut task = create_task(1, "Buy coffee");
println!("{:#?}", task);
task_complete(task);
println!("{:#?}", task);
}
main es ahora más simple y mucho más agradable de leer.
Pero el compilador tiene una pregunta:
error[E0382]: borrow of moved value: `task`
--> src/main.rs:XX:XX
|
XX | let mut task = create_task(1, "Buy coffee");
| -------- move occurs because `task` has type `Task`, which does not implement the `Copy` trait
XX | println!("{:#?}", task);
XX | task_complete(task);
| ---- value moved here
XX | println!("{:#?}", task);
| ^^^^ value borrowed here after move
Lo que sabe el compilador
En Rust, cada valor tiene un único propietario. Pasar un valor a una función significa transferirle los derechos completos sobre él - la función se convierte en propietaria, y el propietario anterior pierde el acceso. Cuando la función termina, todas sus variables locales se destruyen con ella.
Excepto lo que devuelve - que es exactamente por qué create_task crea una tarea y la
entrega de vuelta: la propiedad se transfiere al llamador. La tarea vive donde fue recibida.
task_complete tal como está escrita toma la tarea y la retiene. La función termina - la
tarea se destruye. El segundo println! busca algo que ya no existe.
Modificar una pieza sin llevársela es otra cosa.
La corrección
En lugar de pasar toda la tarea, dale a la función acceso temporal de escritura - en Rust
esto se llama préstamo. En lugar de Task, usa &mut Task:
fn task_complete(task: &mut Task) {
task.status = Status::Done;
}
La función recibe una referencia, no el valor. Puede modificar la tarea a través de la
referencia, pero no la posee. Cuando la función termina, la referencia desaparece; la tarea
en main sigue viva.
La llamada ahora requiere un &mut explícito:
fn main() {
let mut task = create_task(1, "Buy coffee");
println!("Task #{}: {}", task.id, task.title);
println!("{:#?}", task);
task_complete(&mut task);
println!("{:#?}", task);
}
&mut task es una referencia mutable a la tarea. let mut task en main sigue siendo
necesario: sin él, no se puede crear una referencia mutable.
Task #1: Buy coffee
Task {
id: 1,
title: "Buy coffee",
status: Todo,
}
Task {
id: 1,
title: "Buy coffee",
status: Done,
}
make ci pasa. La tarea se crea a través de una función, se completa a través de una
función - y sigue siendo accesible para el llamador.
El código completo de
tqpara este capítulo está en1-a-task/05-lending-the-work/.
Scroll: La forma más sencilla de entender la propiedad en Rust
Lore: Tres formas de pasar un valor
Una función recibe un valor de una de tres formas - y cada una tiene consecuencias:
Task- move (cesión). La función se convierte en propietaria. Tras la llamada, el valor es inaccesible para el llamador.&Task- préstamo compartido. La función puede leer el valor; el llamador conserva la propiedad.&mut Task- préstamo mutable. La función puede modificar el valor; el llamador conserva la propiedad pero debe declarar la variable conlet mut.
create_task devuelve un Task - no recibe propiedad, la crea y la entrega al llamador.
Ese es un cuarto caso: la función como fuente, no como receptora.
Las mismas reglas se aplican a cualquier tipo - no solo a Task.
Lore: mut en un binding vs. mut en un tipo
El capítulo introdujo dos mut diferentes, y a primera vista parecen inconsistentes:
fn task_complete(mut task: Task) // mut antes del nombre
fn task_complete(task: &mut Task) // mut dentro del tipo
En el primer caso, mut es un modificador sobre el binding. Dice: “la variable local
task es mutable.” Sin él, la línea task.status = Status::Done; no compilaría - el
compilador detecta el intento de modificar algo inmutable.
En el segundo caso, mut es parte del tipo. &mut Task es su propio tipo: una
referencia mutable a una tarea. La variable task en sí no es mutable - no necesita
reasignarse, solo leerse y escribirse a través de ella. La mutabilidad aquí describe no la
variable sino lo que apunta.
La regla es sencilla: mut antes de un nombre es sobre la variable; mut dentro de un tipo
después de & es sobre el valor al otro lado de la referencia.