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
tqpara este capítulo está en3-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>.