← Índice

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 tq para este capítulo está en 1-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:

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.