← Índice

3.1 La lengua común

Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.

Hay acuerdos que no requieren firma. Dos artesanos del mismo gremio los conocen de memoria: qué puede pedirse, qué debe darse, qué puede negarse. Un artesano que no conoce estos acuerdos explica - cada vez, desde cero, a mano. Esto suele llamarse toque personal. Quienes lo practican suficiente tiempo conocen otra palabra.

Lo que necesitas

Task puede informar de errores honestamente y nombrar lo ausente por su nombre. Pero todavía habla su propio idioma. Crear una tarea requiere Task::new(id, title)? - explícitamente, con una comprobación, de la misma forma cada vez.

El ecosistema de Rust tiene un acuerdo para una situación común: “B puede obtenerse de A.” Cuando un tipo declara esto, el resto del código lo da por sentado: las funciones que esperan A aceptan B automáticamente - sin explicación, sin intermediario.

El acuerdo se expresa a través de la palabra clave trait. Task debería declarar uno: que sabe cómo crear una nueva tarea a partir de un &str.

La construcción

En la biblioteca estándar, el acuerdo para la conversión de tipos tiene este aspecto:

pub trait From<T> {
    fn from(value: T) -> Self;
}

trait define un contrato: cualquier tipo que implemente este trait debe proporcionar el método fn from(value: T) -> Self. El compilador verificará que la promesa se cumple. Self significa el tipo para el que se escribe el impl - aquí será Task.

Impleméntalo para Task:

impl From<&str> for Task {
    fn from(title: &str) -> Self {
        Task {
            id: 0,
            title: title.to_string(),
            status: Status::Todo,
        }
    }
}

from no tiene acceso a la lista de tareas - no hay de dónde asignar el siguiente identificador. Por ahora, usa id: 0; en el capítulo 3.3 el almacén se encargará de asignar identificadores, y Task::from se convertirá en el camino natural de creación al añadir.

Ahora en main:

let from_task = Task::from("Quick note");
println!("{:?}", from_task);

Pero From tiene una consecuencia.

La biblioteca estándar contiene una regla general: al implementar From<A> for B, obtienes automáticamente la capacidad de llamar .into() en un valor de tipo A para obtener un B. No hace falta implementarlo por separado:

let task: Task = "Quick note".into();

La anotación de tipo : Task es necesaria aquí - el compilador debe saber en qué convertir la cadena.

El resultado

Task::from("Buy coffee") y "Buy coffee".into() ambos funcionan. Task ahora habla el estándar del gremio - y cualquier código que acepte “algo de lo que pueda hacerse un Task” aceptará una cadena sin más explicación. El siguiente capítulo pondrá esto en uso en add.

Dos tests cubren ambos caminos:

#[test]
fn task_from_str_sets_title() {
    let task = Task::from("Buy coffee");
    assert_eq!(task.title, "Buy coffee");
}

#[test]
fn str_converts_into_task() {
    let task: Task = "Buy milk".into();
    assert_eq!(task.title, "Buy milk");
}

make ci pasa. Task ya no necesita explicaciones.

El código completo de tq para este capítulo está en 3-a-voice/01-the-common-tongue/.


Lore: Requeridos y opcionales

Un trait puede declarar cualquier número de métodos - pero no todos necesitan ser proporcionados. Un método sin implementación por defecto debe suministrarse: el compilador lo exigirá en cada impl. Un método con implementación por defecto puede sobreescribirse o dejarse como está.

pub trait Greet {
    fn name(&self) -> &str;                           // requerido
    fn hello(&self) { println!("Hi, {}!", self.name()); } // opcional
}

Un tipo que implemente Greet debe escribir name - y obtiene hello gratis.

From<T> es completamente requerido: un método, sin valor por defecto.


Lore: From e Into - dos caras de un mismo acuerdo

Al implementar From<&str> for Task, obtienes Into<Task> for &str gratis. La biblioteca estándar contiene una implementación general (los genéricos se cubren completamente en el capítulo 3.3):

impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

La regla práctica de esto: implementa siempre From. Nunca implementes Into a mano - aparece solo.

From es un contrato sin excepciones: por convención debe tener siempre éxito. Si una conversión puede fallar, Rust ofrece TryFrom<T> - la misma idea, pero el método devuelve Result<Self, E>. En efecto, Task::new ya se comporta como TryFrom<&str>: toma una cadena y devuelve Result<Task, TqError>.