Rust

Rust

Tutorial Rust: Traits

Aprende a definir, implementar y usar traits en Rust, incluyendo traits como parámetros y la implementación externa para tipos propios y externos.

Aprende Rust y certifícate

Definición e implementación

Los traits son una de las características más potentes de Rust, permitiendo definir comportamientos compartidos entre diferentes tipos. Si estás familiarizado con otros lenguajes, puedes pensar en ellos como algo similar a las interfaces, aunque con capacidades adicionales.

Un trait define una colección de métodos que un tipo debe implementar para "cumplir" con ese trait. Esto permite escribir código que funciona con cualquier tipo que implemente un comportamiento específico, sin necesidad de conocer el tipo concreto de antemano.

Definiendo un trait

Para definir un trait en Rust, utilizamos la palabra clave trait seguida del nombre (por convención en PascalCase) y un bloque que contiene las firmas de los métodos:

trait Describible {
    fn describir(&self) -> String;
}

En este ejemplo, hemos definido un trait llamado Describible que requiere que cualquier tipo que lo implemente proporcione una implementación para el método describir, que devuelve una cadena de texto.

Implementando un trait

Para implementar un trait para un tipo específico, utilizamos la palabra clave impl seguida del nombre del trait, la palabra clave for y el nombre del tipo:

struct Producto {
    nombre: String,
    precio: f64,
}

impl Describible for Producto {
    fn describir(&self) -> String {
        format!("{}: ${:.2}", self.nombre, self.precio)
    }
}

Ahora cualquier instancia de Producto puede utilizar el método describir:

fn main() {
    let producto = Producto {
        nombre: String::from("Teclado mecánico"),
        precio: 89.99,
    };
    
    println!("Descripción: {}", producto.describir());
    // Imprime: "Descripción: Teclado mecánico: $89.99"
}

Implementaciones por defecto

Una característica poderosa de los traits en Rust es la capacidad de proporcionar implementaciones por defecto para algunos o todos los métodos. Esto permite que los tipos que implementan el trait puedan usar la implementación predeterminada o proporcionar su propia versión:

trait Saludable {
    fn nombre(&self) -> String;
    
    fn saludar(&self) -> String {
        format!("¡Hola, {}!", self.nombre())
    }
}

struct Usuario {
    nombre: String,
}

impl Saludable for Usuario {
    fn nombre(&self) -> String {
        self.nombre.clone()
    }
    // No implementamos saludar(), usaremos la implementación por defecto
}

En este ejemplo, el trait Saludable requiere que se implemente el método nombre, pero proporciona una implementación por defecto para saludar. Al implementar Saludable para Usuario, solo necesitamos proporcionar una implementación para nombre:

fn main() {
    let usuario = Usuario {
        nombre: String::from("Ana"),
    };
    
    println!("{}", usuario.saludar()); // Imprime: "¡Hola, Ana!"
}

Múltiples métodos en un trait

Los traits pueden definir múltiples métodos, algunos requeridos y otros con implementaciones por defecto:

trait Figura {
    fn area(&self) -> f64;
    fn perimetro(&self) -> f64;
    
    fn describir(&self) -> String {
        format!("Figura con área: {:.2} y perímetro: {:.2}", self.area(), self.perimetro())
    }
}

struct Rectangulo {
    ancho: f64,
    alto: f64,
}

impl Figura for Rectangulo {
    fn area(&self) -> f64 {
        self.ancho * self.alto
    }
    
    fn perimetro(&self) -> f64 {
        2.0 * (self.ancho + self.alto)
    }
    
    // Usamos la implementación por defecto para describir()
}

Traits con tipos asociados

Los tipos asociados son una característica avanzada de los traits que permite definir un tipo dentro del trait que será especificado por cada implementación:

trait Contenedor {
    type Elemento;
    
    fn agregar(&mut self, elemento: Self::Elemento);
    fn obtener(&self, indice: usize) -> Option<&Self::Elemento>;
    fn longitud(&self) -> usize;
}

struct Vector<T> {
    elementos: Vec<T>,
}

impl<T> Contenedor for Vector<T> {
    type Elemento = T;
    
    fn agregar(&mut self, elemento: T) {
        self.elementos.push(elemento);
    }
    
    fn obtener(&self, indice: usize) -> Option<&T> {
        self.elementos.get(indice)
    }
    
    fn longitud(&self) -> usize {
        self.elementos.len()
    }
}

En este ejemplo, el trait Contenedor define un tipo asociado Elemento. Cuando implementamos este trait para Vector<T>, especificamos que Elemento es T.

Implementando múltiples traits

Un tipo puede implementar múltiples traits, lo que permite una gran flexibilidad:

trait Imprimible {
    fn imprimir(&self);
}

trait Comparable {
    fn es_igual(&self, otro: &Self) -> bool;
}

struct Libro {
    titulo: String,
    autor: String,
    paginas: u32,
}

impl Imprimible for Libro {
    fn imprimir(&self) {
        println!("{} por {} ({} páginas)", self.titulo, self.autor, self.paginas);
    }
}

impl Comparable for Libro {
    fn es_igual(&self, otro: &Self) -> bool {
        self.titulo == otro.titulo && self.autor == otro.autor
    }
}

Traits derivables

Rust proporciona varios traits que pueden ser derivados automáticamente para tus tipos utilizando el atributo #[derive]:

#[derive(Debug, Clone, PartialEq)]
struct Punto {
    x: f64,
    y: f64,
}

En este ejemplo, derivamos:

  • Debug: Permite imprimir la estructura con {:?} en macros como println!
  • Clone: Proporciona el método clone() para crear una copia profunda
  • PartialEq: Permite comparar instancias con == y !=

Algunos de los traits derivables más comunes son:

  • Debug - Para depuración
  • Clone y Copy - Para duplicación de valores
  • PartialEq y Eq - Para comparaciones de igualdad
  • PartialOrd y Ord - Para ordenación
  • Hash - Para hashing
  • Default - Para valores por defecto

Traits y métodos sin self

Los traits también pueden definir métodos asociados (similares a métodos estáticos) que no toman self como parámetro:

trait Creador {
    fn crear(nombre: &str) -> Self;
    fn crear_predeterminado() -> Self;
}

struct Tarea {
    nombre: String,
    completada: bool,
}

impl Creador for Tarea {
    fn crear(nombre: &str) -> Self {
        Tarea {
            nombre: nombre.to_string(),
            completada: false,
        }
    }
    
    fn crear_predeterminado() -> Self {
        Tarea {
            nombre: "Nueva tarea".to_string(),
            completada: false,
        }
    }
}

Estos métodos se pueden llamar utilizando la sintaxis Tarea::crear("Hacer compras") o <Tarea as Creador>::crear("Hacer compras") cuando es necesario especificar el trait.

Los traits son fundamentales en Rust para lograr abstracción y polimorfismo de manera segura y eficiente. Permiten escribir código genérico que funciona con cualquier tipo que implemente un comportamiento específico, facilitando la creación de componentes reutilizables y extensibles.

Traits como parámetros

Una vez que hemos definido e implementado traits, el siguiente paso es utilizarlos para crear funciones genéricas que puedan trabajar con cualquier tipo que implemente un determinado comportamiento. En Rust, existen dos formas principales de especificar que un parámetro debe implementar un trait específico.

Trait bounds con sintaxis genérica

La primera forma de utilizar traits como parámetros es mediante trait bounds en funciones genéricas. Esta sintaxis utiliza el operador : después del parámetro de tipo:

fn imprimir_descripcion<T: Describible>(item: T) {
    println!("Descripción: {}", item.describir());
}

En este ejemplo, la función imprimir_descripcion acepta cualquier tipo T que implemente el trait Describible. Esto nos permite llamar al método describir() dentro de la función, independientemente del tipo concreto que se pase como argumento.

Podemos usar esta función con cualquier tipo que implemente el trait Describible:

fn main() {
    let producto = Producto {
        nombre: String::from("Monitor"),
        precio: 299.99,
    };
    
    imprimir_descripcion(producto);
    // Imprime: "Descripción: Monitor: $299.99"
}

Sintaxis impl Trait

La segunda forma, introducida en versiones más recientes de Rust, es la sintaxis impl Trait. Esta forma es más concisa y se utiliza directamente en la lista de parámetros:

fn imprimir_descripcion(item: impl Describible) {
    println!("Descripción: {}", item.describir());
}

Ambas formas son funcionalmente equivalentes para casos simples, pero tienen algunas diferencias sutiles en casos más complejos. La sintaxis impl Trait es generalmente más legible cuando solo necesitas especificar que un parámetro implementa un trait, sin necesidad de referirte al tipo genérico en otras partes de la función.

Múltiples trait bounds

A veces necesitamos que un tipo implemente varios traits. Podemos especificar múltiples trait bounds utilizando el operador +:

fn procesar<T: Describible + Clone>(item: T) {
    let copia = item.clone();
    println!("Original: {}", item.describir());
    println!("Copia: {}", copia.describir());
}

Con la sintaxis impl Trait:

fn procesar(item: impl Describible + Clone) {
    let copia = item.clone();
    println!("Original: {}", item.describir());
    println!("Copia: {}", copia.describir());
}

Cláusula where

Cuando tenemos múltiples parámetros genéricos con varios trait bounds, la sintaxis puede volverse difícil de leer. Para estos casos, Rust proporciona la cláusula where, que permite especificar los trait bounds de manera más clara:

fn comparar<T, U>(item1: T, item2: U) -> bool
where
    T: Describible + PartialEq,
    U: Describible + PartialEq + Clone,
{
    println!("Comparando {} con {}", item1.describir(), item2.describir());
    // Implementación de la comparación...
    true
}

La cláusula where hace que el código sea más legible, especialmente cuando hay múltiples parámetros genéricos con varios trait bounds.

Retornando tipos que implementan traits

También podemos utilizar traits para especificar el tipo de retorno de una función. Esto es particularmente útil cuando queremos devolver diferentes tipos que comparten un comportamiento común:

fn crear_figura(tipo: &str) -> impl Figura {
    match tipo {
        "rectangulo" => Rectangulo { ancho: 3.0, alto: 4.0 },
        "circulo" => Circulo { radio: 5.0 },
        _ => Cuadrado { lado: 2.0 },
    }
}

En este ejemplo, la función crear_figura puede devolver diferentes tipos (Rectangulo, Circulo, Cuadrado), pero todos implementan el trait Figura. Esto permite al código que llama a esta función trabajar con el resultado sin conocer el tipo concreto.

Sin embargo, hay una limitación importante: cuando usas impl Trait como tipo de retorno, solo puedes devolver un tipo concreto dentro de la función. El siguiente código no compilaría:

// Este código NO compila
fn crear_figura(tipo: &str) -> impl Figura {
    if tipo == "rectangulo" {
        Rectangulo { ancho: 3.0, alto: 4.0 }
    } else {
        Circulo { radio: 5.0 } // Error: tipos de retorno incompatibles
    }
}

Traits como parámetros en métodos

Los traits también pueden utilizarse como parámetros en métodos de estructuras o enumeraciones:

struct Coleccion<T> {
    items: Vec<T>,
}

impl<T> Coleccion<T> {
    fn nuevo() -> Self {
        Coleccion { items: Vec::new() }
    }
    
    fn agregar(&mut self, item: T) {
        self.items.push(item);
    }
    
    fn procesar_con<F>(&self, procesador: F)
    where
        F: Fn(&T),
    {
        for item in &self.items {
            procesador(item);
        }
    }
}

En este ejemplo, el método procesar_con acepta cualquier función o closure que implemente el trait Fn(&T), lo que significa que puede tomar una referencia a un elemento de tipo T y hacer algo con él.

Ejemplo práctico: filtrado genérico

Veamos un ejemplo más completo que muestra cómo los traits como parámetros pueden hacer nuestro código más flexible:

trait Filtrable {
    fn cumple_criterio(&self) -> bool;
}

struct Producto {
    nombre: String,
    precio: f64,
    en_stock: bool,
}

impl Filtrable for Producto {
    fn cumple_criterio(&self) -> bool {
        self.en_stock && self.precio < 100.0
    }
}

struct Usuario {
    nombre: String,
    edad: u32,
    activo: bool,
}

impl Filtrable for Usuario {
    fn cumple_criterio(&self) -> bool {
        self.activo && self.edad >= 18
    }
}

fn filtrar_elementos<T: Filtrable>(elementos: &[T]) -> Vec<&T> {
    elementos
        .iter()
        .filter(|elemento| elemento.cumple_criterio())
        .collect()
}

fn main() {
    let productos = vec![
        Producto { nombre: "Teclado".to_string(), precio: 89.99, en_stock: true },
        Producto { nombre: "Monitor".to_string(), precio: 299.99, en_stock: true },
        Producto { nombre: "Ratón".to_string(), precio: 49.99, en_stock: false },
    ];
    
    let usuarios = vec![
        Usuario { nombre: "Ana".to_string(), edad: 25, activo: true },
        Usuario { nombre: "Carlos".to_string(), edad: 17, activo: true },
        Usuario { nombre: "Elena".to_string(), edad: 30, activo: false },
    ];
    
    let productos_filtrados = filtrar_elementos(&productos);
    let usuarios_filtrados = filtrar_elementos(&usuarios);
    
    println!("Productos que cumplen el criterio: {}", productos_filtrados.len());
    println!("Usuarios que cumplen el criterio: {}", usuarios_filtrados.len());
}

En este ejemplo, hemos definido un trait Filtrable que requiere un método cumple_criterio(). Luego implementamos este trait para dos tipos diferentes (Producto y Usuario) con lógicas de filtrado específicas para cada tipo. La función filtrar_elementos puede trabajar con cualquier colección de elementos que implementen el trait Filtrable.

Traits como parámetros en constructores

También podemos utilizar traits como parámetros en los constructores de nuestros tipos:

struct Registro<W: std::io::Write> {
    escritor: W,
    nivel: String,
}

impl<W: std::io::Write> Registro<W> {
    fn nuevo(escritor: W, nivel: &str) -> Self {
        Registro {
            escritor,
            nivel: nivel.to_string(),
        }
    }
    
    fn log(&mut self, mensaje: &str) -> std::io::Result<()> {
        writeln!(self.escritor, "[{}] {}", self.nivel, mensaje)
    }
}

fn main() -> std::io::Result<()> {
    let archivo = std::fs::File::create("log.txt")?;
    let mut registro_archivo = Registro::nuevo(archivo, "INFO");
    registro_archivo.log("Mensaje guardado en archivo")?;
    
    let mut registro_consola = Registro::nuevo(std::io::stdout(), "DEBUG");
    registro_consola.log("Mensaje mostrado en consola")?;
    
    Ok(())
}

En este ejemplo, la estructura Registro acepta cualquier tipo que implemente el trait std::io::Write, lo que nos permite crear registros que escriban en diferentes destinos (archivos, consola, red, etc.).

Los traits como parámetros son una herramienta fundamental en Rust para escribir código genérico y reutilizable. Permiten definir comportamientos que pueden ser implementados por diferentes tipos, facilitando la creación de abstracciones poderosas sin sacrificar la seguridad ni el rendimiento.

Implementación externa

En Rust, una de las características más flexibles del sistema de traits es la capacidad de implementar traits para tipos que no te pertenecen. Esta característica permite extender la funcionalidad de tipos existentes sin modificar su código original, siguiendo un patrón similar al de las "extensiones" en otros lenguajes.

Sin embargo, Rust impone una restricción importante conocida como la regla de coherencia (coherence rule), que establece límites claros sobre qué implementaciones están permitidas.

La regla de coherencia

La regla de coherencia en Rust establece que:

  1. Puedes implementar un trait externo para un tipo propio
  2. Puedes implementar un trait propio para un tipo externo
  3. No puedes implementar un trait externo para un tipo externo

Esta regla evita conflictos y ambigüedades que podrían surgir si dos crates diferentes implementaran el mismo trait para el mismo tipo.

// ✅ Implementando trait estándar (externo) para tipo propio
impl std::fmt::Display for MiTipo {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "MiTipo: {}", self.valor)
    }
}

// ✅ Implementando trait propio para tipo estándar (externo)
trait Invertible {
    fn invertir(&self) -> Self;
}

impl Invertible for String {
    fn invertir(&self) -> Self {
        self.chars().rev().collect()
    }
}

// ❌ Esto NO compilaría: implementando trait externo para tipo externo
// impl std::fmt::Display for Vec<i32> { ... }

Implementando traits estándar para tipos propios

Una de las aplicaciones más comunes de la implementación externa es añadir comportamientos estándar a nuestros propios tipos:

struct Coordenada {
    x: f64,
    y: f64,
}

// Implementamos Display para permitir formateo personalizado
impl std::fmt::Display for Coordenada {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "({:.2}, {:.2})", self.x, self.y)
    }
}

// Implementamos Add para permitir la suma de coordenadas con el operador +
impl std::ops::Add for Coordenada {
    type Output = Self;

    fn add(self, otra: Self) -> Self::Output {
        Coordenada {
            x: self.x + otra.x,
            y: self.y + otra.y,
        }
    }
}

fn main() {
    let p1 = Coordenada { x: 1.0, y: 2.0 };
    let p2 = Coordenada { x: 3.0, y: 4.0 };
    let suma = p1 + p2;
    
    println!("La suma de coordenadas es: {}", suma);
    // Imprime: "La suma de coordenadas es: (4.00, 6.00)"
}

En este ejemplo, hemos implementado dos traits estándar para nuestro tipo Coordenada:

  • std::fmt::Display para permitir imprimir la coordenada con formato personalizado
  • std::ops::Add para permitir sumar coordenadas usando el operador +

Implementando traits propios para tipos externos

También podemos extender tipos que no nos pertenecen con comportamientos específicos para nuestra aplicación:

trait Estadisticas {
    fn suma(&self) -> f64;
    fn promedio(&self) -> f64;
    fn desviacion_estandar(&self) -> f64;
}

impl Estadisticas for Vec<f64> {
    fn suma(&self) -> f64 {
        self.iter().sum()
    }
    
    fn promedio(&self) -> f64 {
        if self.is_empty() {
            0.0
        } else {
            self.suma() / self.len() as f64
        }
    }
    
    fn desviacion_estandar(&self) -> f64 {
        if self.is_empty() || self.len() == 1 {
            return 0.0;
        }
        
        let prom = self.promedio();
        let varianza: f64 = self.iter()
            .map(|&x| (x - prom).powi(2))
            .sum::<f64>() / (self.len() - 1) as f64;
            
        varianza.sqrt()
    }
}

fn main() {
    let datos = vec![2.0, 4.0, 6.0, 8.0, 10.0];
    
    println!("Suma: {}", datos.suma());
    println!("Promedio: {}", datos.promedio());
    println!("Desviación estándar: {}", datos.desviacion_estandar());
}

En este ejemplo, hemos definido un trait Estadisticas y lo hemos implementado para el tipo Vec<f64> de la biblioteca estándar, añadiendo métodos para calcular estadísticas básicas.

Implementación condicional con genéricos

Podemos implementar traits de forma condicional para tipos genéricos, lo que nos permite añadir comportamientos solo cuando se cumplen ciertas condiciones:

struct Contenedor<T> {
    valor: T,
}

// Implementamos ToString solo si T implementa Display
impl<T: std::fmt::Display> ToString for Contenedor<T> {
    fn to_string(&self) -> String {
        format!("Contenedor que almacena: {}", self.valor)
    }
}

// Implementamos Default solo si T implementa Default
impl<T: Default> Default for Contenedor<T> {
    fn default() -> Self {
        Contenedor {
            valor: T::default(),
        }
    }
}

Implementación de traits para enums

También podemos implementar traits para enumeraciones, lo que es especialmente útil para tipos que representan variantes:

enum Resultado {
    Exito(String),
    Error(u32, String),
    EnProceso,
}

impl std::fmt::Display for Resultado {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Resultado::Exito(msg) => write!(f, "Operación exitosa: {}", msg),
            Resultado::Error(codigo, msg) => write!(f, "Error {}: {}", codigo, msg),
            Resultado::EnProceso => write!(f, "Operación en proceso..."),
        }
    }
}

fn main() {
    let r1 = Resultado::Exito(String::from("Datos guardados"));
    let r2 = Resultado::Error(404, String::from("Recurso no encontrado"));
    
    println!("{}", r1); // Imprime: "Operación exitosa: Datos guardados"
    println!("{}", r2); // Imprime: "Error 404: Recurso no encontrado"
}

Implementación de traits con tipos asociados

Los traits con tipos asociados son particularmente útiles cuando implementamos comportamientos para tipos externos:

trait Coleccionable {
    type Item;
    
    fn agregar(&mut self, item: Self::Item);
    fn contiene(&self, item: &Self::Item) -> bool;
}

impl<T: PartialEq> Coleccionable for Vec<T> {
    type Item = T;
    
    fn agregar(&mut self, item: T) {
        self.push(item);
    }
    
    fn contiene(&self, item: &T) -> bool {
        self.contains(item)
    }
}

impl<K, V> Coleccionable for std::collections::HashMap<K, V> 
where 
    K: PartialEq + std::hash::Hash,
    V: PartialEq,
{
    type Item = (K, V);
    
    fn agregar(&mut self, item: Self::Item) {
        self.insert(item.0, item.1);
    }
    
    fn contiene(&self, item: &Self::Item) -> bool {
        self.get(&item.0).map_or(false, |v| v == &item.1)
    }
}

En este ejemplo, implementamos el trait Coleccionable tanto para Vec<T> como para HashMap<K, V>, adaptando el comportamiento a las características específicas de cada estructura de datos.

Extensión de tipos primitivos

Incluso podemos extender tipos primitivos con nuestros propios comportamientos:

trait NumeroExtendido {
    fn es_par(&self) -> bool;
    fn es_primo(&self) -> bool;
    fn factores(&self) -> Vec<Self> where Self: Sized;
}

impl NumeroExtendido for u32 {
    fn es_par(&self) -> bool {
        self % 2 == 0
    }
    
    fn es_primo(&self) -> bool {
        if *self <= 1 {
            return false;
        }
        if *self <= 3 {
            return true;
        }
        if self.es_par() || *self % 3 == 0 {
            return false;
        }
        
        let limite = (*self as f64).sqrt() as u32 + 1;
        let mut i = 5;
        
        while i < limite {
            if *self % i == 0 || *self % (i + 2) == 0 {
                return false;
            }
            i += 6;
        }
        
        true
    }
    
    fn factores(&self) -> Vec<Self> {
        let mut resultado = Vec::new();
        let mut n = *self;
        let mut divisor = 2;
        
        while n > 1 {
            while n % divisor == 0 {
                resultado.push(divisor);
                n /= divisor;
            }
            divisor += 1;
        }
        
        resultado
    }
}

fn main() {
    let numero = 42u32;
    
    println!("¿{} es par? {}", numero, numero.es_par());
    println!("¿{} es primo? {}", numero, numero.es_primo());
    println!("Factores de {}: {:?}", numero, numero.factores());
}

Consideraciones prácticas

Al implementar traits externos para tipos propios o traits propios para tipos externos, hay algunas consideraciones importantes:

  • Organización del código: Mantén las implementaciones de traits cerca del código que las utiliza para mejorar la legibilidad.

  • Documentación: Documenta claramente qué comportamientos estás añadiendo a tipos externos para facilitar el mantenimiento.

  • Evita colisiones de nombres: Asegúrate de que los nombres de métodos que añades no entren en conflicto con métodos existentes o futuros.

  • Pruebas: Las implementaciones externas deben probarse exhaustivamente, especialmente cuando extiendes tipos de la biblioteca estándar.

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_estadisticas() {
        let datos = vec![2.0, 4.0, 6.0, 8.0, 10.0];
        assert_eq!(datos.suma(), 30.0);
        assert_eq!(datos.promedio(), 6.0);
        assert!((datos.desviacion_estandar() - 3.16).abs() < 0.01);
    }
}

La implementación externa de traits es una herramienta poderosa que permite extender la funcionalidad de tipos existentes de forma segura y controlada. La regla de coherencia de Rust garantiza que estas extensiones no causen conflictos, mientras que el sistema de tipos estático asegura que todas las implementaciones son correctas en tiempo de compilación.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Traits con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender qué son los traits y cómo definirlos en Rust.
  • Implementar traits para tipos propios y externos, incluyendo métodos con implementaciones por defecto.
  • Utilizar traits como parámetros en funciones genéricas y métodos, incluyendo sintaxis con trait bounds, impl Trait y cláusula where.
  • Aplicar traits con tipos asociados y múltiples traits para lograr abstracción y reutilización de código.
  • Entender la regla de coherencia para implementaciones externas y cómo extender tipos estándar o primitivos con traits personalizados.