Ir al contenido

Traits comunes en Rust

En la página de Traits escribiste tus propios traits (rasgos). Pero la biblioteca estándar ya trae un conjunto de traits que usa casi cualquier programa en Rust. Responden a preguntas sencillas sobre un tipo: ¿Puedo imprimirlo? ¿Puedo copiarlo? ¿Puedo compararlo? ¿Puedo crear uno vacío?

Piensa en ellos como insignias en un uniforme. Cada insignia dice “este tipo sabe hacer X”. Muchas de estas insignias las consigues gratis con #[derive(...)]; otras las escribes a mano. Esta página repasa las que te encontrarás más a menudo y termina con una tabla a la que puedes volver cuando quieras.

Debug te permite imprimir un valor con {:?}. Está pensado para ti, el desarrollador: para revisar valores mientras construyes y corriges tu código. Casi nunca lo escribes a mano; basta con derivarlo. Usa {:#?} para una versión “bonita”, con un campo por línea, que se lee mejor en structs grandes:

#[derive(Debug)]
struct Book {
title: String,
pages: u32,
}
fn main() {
let book = Book { title: String::from("Dune"), pages: 412 };
println!("{:?}", book);
println!("{:#?}", book);
}

Salida:

Book { title: "Dune", pages: 412 }
Book {
title: "Dune",
pages: 412,
}
▶ Pruébalo tú mismo

La macro dbg!, que quizá veas en código de otras personas, también usa Debug.

Display es lo que usa {}. Sirve para el texto que ve el usuario: el mensaje que una persona lee en la pantalla. Rust no puede adivinar cómo debería verse, así que no hay derive. Lo escribes tú implementando fmt::Display:

use std::fmt;
struct Book {
title: String,
author: String,
}
impl fmt::Display for Book {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} by {}", self.title, self.author)
}
}
fn main() {
let book = Book {
title: String::from("Dune"),
author: String::from("Frank Herbert"),
};
println!("{}", book);
let text = book.to_string();
println!("Ese texto tiene {} caracteres", text.len());
}

Salida:

Dune by Frank Herbert
Ese texto tiene 21 caracteres
▶ Pruébalo tú mismo

write! funciona como println!, pero escribe en el formateador f en lugar de en la pantalla. Un buen extra: en cuanto un tipo tiene Display, obtienes .to_string() gratis. Dejamos "{} by {}" en inglés a propósito: .len() cuenta bytes, no letras, y si cambiaras el texto (sobre todo con letras acentuadas, que ocupan más de un byte) el número de la salida cambiaría. Volverás a ver Display en la página de Errores personalizados, donde sirve para darles a los errores un mensaje claro.

Los dos hacen un duplicado de un valor, pero de formas distintas.

  • Clone te da el método .clone(). Lo llamas a propósito, y puede hacer trabajo de verdad, como copiar todo el texto de un String.
  • Copy hace un duplicado automáticamente cada vez que asignas o pasas el valor. Solo se permite en tipos pequeños y sencillos que se pueden copiar bit a bit: números, bool, char y structs formadas solo por ellos.
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
#[derive(Debug, Clone)]
struct User {
name: String,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy: p1 se puede seguir usando
println!("{:?} y {:?}", p1, p2);
let u1 = User { name: String::from("Ana") };
let u2 = u1.clone(); // Clone: pedimos una copia
println!("{:?} y {:?}", u1, u2);
}

Salida:

Point { x: 1, y: 2 } y Point { x: 1, y: 2 }
User { name: "Ana" } y User { name: "Ana" }
▶ Pruébalo tú mismo

Sin Copy, let p2 = p1; movería p1, y no podrías usarlo después (consulta Ownership). Fíjate en que Copy siempre necesita también Clone: por eso se derivan juntos.

PartialEq te permite usar == y !=. Cuando lo derivas, dos valores son iguales si todos sus campos son iguales:

#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b);
println!("a != c: {}", a != c);
}

Salida:

a == b: true
a != c: true
▶ Pruébalo tú mismo

¿Y qué es Eq? No añade ningún método nuevo. Es una promesa: “todo valor es siempre igual a sí mismo”. Parece obvio, pero los números de punto flotante la rompen: en Rust, f64::NAN == f64::NAN es false. Por eso f64 tiene PartialEq pero no Eq. Necesitas Eq (junto con Hash) para usar tu tipo como clave de un HashMap o dentro de un HashSet. Si tu struct no tiene campos de punto flotante, deriva los dos.

PartialOrd te permite usar <, >, <= y >=. Ord añade un orden total, que es lo que necesitan .sort(), .max() y .min(). Cuando los derivas, Rust compara los campos de arriba abajo: primero el primer campo, y solo si son iguales pasa a mirar el siguiente.

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
fn main() {
let old = Version { major: 1, minor: 2, patch: 0 };
let new = Version { major: 1, minor: 10, patch: 3 };
println!("old < new: {}", old < new);
let mut versions = vec![
Version { major: 2, minor: 0, patch: 0 },
new,
old,
];
versions.sort();
for v in &versions {
println!("{}.{}.{}", v.major, v.minor, v.patch);
}
}

Salida:

old < new: true
1.2.0
1.10.3
2.0.0
▶ Pruébalo tú mismo

Como los campos se comparan en orden, importa el orden en que escribes los campos. Aquí major es el más importante, que es justo lo correcto para los números de versión. En los enums, las variantes que aparecen primero cuentan como “más pequeñas”. Igual que con Eq, los números de punto flotante solo tienen PartialOrd, no Ord; la página de Ordenar te muestra cómo ordenarlos de todos modos.

Default le da a tu tipo una función ::default() que crea un valor “de partida”. Si lo derivas, cada campo recibe su propio valor por defecto: 0 para los números, false para bool, un String vacío, un Vec vacío. La sintaxis ..Default::default() te permite fijar unos pocos campos y rellenar el resto:

#[derive(Debug, Default)]
struct Settings {
volume: u32,
muted: bool,
username: String,
}
fn main() {
let basic = Settings::default();
println!("{:?}", basic);
let custom = Settings {
volume: 80,
..Default::default()
};
println!("{:?}", custom);
}

Salida:

Settings { volume: 0, muted: false, username: "" }
Settings { volume: 80, muted: false, username: "" }
▶ Pruébalo tú mismo

Si quieres otros valores iniciales (por ejemplo, volumen 50), no uses derive: escribe tú mismo impl Default for Settings, con una función fn default() -> Self que devuelva los valores que quieras.

From describe cómo construir un tipo a partir de otro. Ya lo has usado: String::from("hola"). Cuando implementas From, Rust te da Into gratis, que va en la dirección contraria:

struct Celsius(f64);
struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
fn main() {
let boiling = Fahrenheit::from(Celsius(100.0));
println!("El agua hierve a {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into();
println!("La temperatura del cuerpo es {:.1}°F", body.0);
}

Salida:

El agua hierve a 212°F
La temperatura del cuerpo es 98.6°F
▶ Pruébalo tú mismo

Con .into(), Rust necesita saber a qué tipo quieres convertir; por eso escribimos let body: Fahrenheit.

Cómo usa ? el trait From. Aquí es donde From brilla de verdad. Cuando el operador ? se encuentra con un error, llama a From::from para convertir ese error en el tipo de error que devuelve tu función. Así que, si implementas From para tu propio tipo de error, ? convierte los errores por ti:

use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
BadNumber(ParseIntError),
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::BadNumber(e)
}
}
fn parse_age(text: &str) -> Result<u32, AppError> {
let age: u32 = text.parse()?; // aquí el ParseIntError se convierte en AppError
Ok(age)
}
fn main() {
println!("{:?}", parse_age("42"));
println!("{:?}", parse_age("abc"));
}

Salida:

Ok(42)
Err(BadNumber(ParseIntError { kind: InvalidDigit }))
▶ Pruébalo tú mismo

La página de Errores personalizados desarrolla esta idea con más detalle.

En Rust, a + b es en realidad una llamada al método add del trait std::ops::Add. Impleméntalo para tu tipo y + funcionará sin más. type Output indica de qué tipo es el resultado de la suma:

use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point { x: self.x + other.x, y: self.y + other.y }
}
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = Point { x: 3, y: 4 };
println!("{:?}", a + b);
}

Salida:

Point { x: 4, y: 6 }
▶ Pruébalo tú mismo

La misma idea sirve para Sub (-), Mul (*), Neg (el - delante de un valor) y AddAssign (+=). Úsalo cuando el significado sea obvio: sumar dos puntos o dos cantidades de dinero. Si quien lee tu código tuviera que adivinar qué significa + para tu tipo, es mejor un método normal con un nombre claro.

Aquí tienes una guía rápida para una struct típica:

Trait ¿Derivarlo? Qué obtienes
Debug Casi siempre Imprimir con {:?} y {:#?}
Clone Normalmente .clone()
Copy Solo en tipos pequeños sin campos String, Vec ni Box Copias automáticas en lugar de moves
PartialEq Normalmente == y !=
Eq Cuando no hay campos de punto flotante Necesario para claves de HashMap (junto con Hash)
Hash Cuando se usa como clave de HashMap o en un HashSet Hashing
PartialOrd, Ord Cuando los valores tienen un orden natural <, >, .sort(), .max()
Default Cuando tiene sentido un valor “vacío” ::default() y ..Default::default()
Display No se puede derivar: escríbelo a mano Imprimir con {} y .to_string()

Una primera línea muy habitual es #[derive(Debug, Clone, PartialEq)]. Añade más cuando los necesites: el compilador te avisará cuando falte alguno.

Derivar Copy en una struct que tiene un String.

#[derive(Clone, Copy)]
struct User {
name: String,
age: u32,
}
error[E0204]: the trait `Copy` cannot be implemented for this type
--> src/main.rs:2:8
|
1 | #[derive(Clone, Copy)]
| ---- in this derive macro expansion
2 | struct User {
| ^^^^
3 | name: String,
| ------------ this field does not implement `Copy`

El compilador dice que no se puede implementar Copy para este tipo, porque el campo name no implementa Copy. Un String es dueño de memoria en el heap, así que no se puede copiar bit a bit. Quita Copy y usa .clone() cuando necesites un duplicado.

Derivar Eq en una struct que tiene un número de punto flotante.

#[derive(Debug, PartialEq, Eq)]
struct Product { name: String, price: f64 }
error[E0277]: the trait bound `f64: Eq` is not satisfied
--> src/main.rs:2:32
|
1 | #[derive(Debug, PartialEq, Eq)]
| -- in this derive macro expansion
2 | struct Product { name: String, price: f64 }
| ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`

Aquí el mensaje dice que f64 no implementa Eq. Mantén PartialEq y quita Eq. O guarda el precio en un u64, como un número entero de centavos, que además es más seguro para trabajar con dinero.

Crea una struct Task con un priority: u8 y un title: String. Deriva los traits que necesites para poder ordenar un Vec<Task>, imprimir cada tarea con {:?} y comparar dos tareas con ==. Los números más bajos son más urgentes, así que deben ir primero. Pista: .sort() necesita Ord, y Ord necesita también Eq, PartialEq y PartialOrd. Pon priority como primer campo para que se compare primero.

Ver solución
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Task {
priority: u8,
title: String,
}
fn main() {
let mut tasks = vec![
Task { priority: 3, title: String::from("Regar las plantas") },
Task { priority: 1, title: String::from("Pagar el alquiler") },
Task { priority: 2, title: String::from("Responder a Sam") },
];
let copy = tasks[1].clone();
println!("¿Es la misma tarea? {}", copy == tasks[1]);
tasks.sort();
for task in &tasks {
println!("{:?}", task);
}
}

Salida:

¿Es la misma tarea? true
Task { priority: 1, title: "Pagar el alquiler" }
Task { priority: 2, title: "Responder a Sam" }
Task { priority: 3, title: "Regar las plantas" }
▶ Pruébalo tú mismo

Crea una struct Money que guarde centavos enteros en cents: u64. Implementa From<u64> para que funcione 350.into(), Display para que se imprima como $3.50, y Add para poder sumar dos cantidades con +. Pista: los dólares son cents / 100 y el resto es cents % 100. Imprime el resto con {:02} para que 5 centavos se muestren como 05. Deriva Copy para poder seguir imprimiendo las cantidades después de sumarlas.

Ver solución
use std::fmt;
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Money {
cents: u64,
}
impl From<u64> for Money {
fn from(cents: u64) -> Self {
Money { cents }
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.cents / 100, self.cents % 100)
}
}
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money { cents: self.cents + other.cents }
}
}
fn main() {
let coffee: Money = 350.into();
let cake = Money::from(405);
let total = coffee + cake;
println!("Café: {coffee}");
println!("Pastel: {cake}");
println!("Total: {total}");
}

Salida:

Café: $3.50
Pastel: $4.05
Total: $7.55
▶ Pruébalo tú mismo
  • Debug ({:?}) es para desarrolladores y casi siempre se deriva. Display ({}) es para usuarios y se escribe a mano.
  • Clone copia cuando se lo pides con .clone(). Copy copia automáticamente, pero solo en tipos sencillos.
  • PartialEq/Eq te dan ==. PartialOrd/Ord te dan < y la posibilidad de ordenar, comparando los campos de arriba abajo.
  • Default crea un valor de partida; ..Default::default() rellena los campos que no fijaste.
  • Implementa From y obtienes Into gratis. El operador ? usa From para convertir errores.
  • Implementa std::ops::Add (y sus parientes) para que + funcione con tus propios tipos.

¿Quieres ir más allá? En Traits avanzados se explican los tipos asociados como type Output, los parámetros de tipo por defecto y cómo implementar un trait para un tipo que no escribiste tú.

Comprueba lo aprendido

1. ¿Qué trait usa {} en println!?

2. ¿Por qué una struct con un campo String no puede derivar Copy?

3. Cuando derivas PartialOrd, ¿cómo se comparan dos structs?

4. Implementaste From<ParseIntError> for AppError. ¿Qué le permite eso hacer a ??

5. ¿Qué trait implementas para que a + b funcione con tu propio tipo?

Puntuación: 0 / 5