wandres.dev
TRAITS DEL SISTEMA · Clone, Copy, Debug, Default

From, Into, TryFrom: las conversiones idiomáticas

From define una conversión que no falla, e Into aparece gratis por una implementación general. TryFrom es su versión falible cuando la conversión puede no tener sentido. Y el operador interrogación usa From para convertir los errores que propaga: la conversión es la costura de todo el manejo de errores.

⏱ 19 min

Convertir un tipo en otro —un &str en String, un i64 en i32, un error de red en tu error de dominio— es una de las operaciones más frecuentes de cualquier programa. Rust la estandariza en dos parejas de traits: From/Into para las conversiones que siempre funcionan, y TryFrom/TryInto para las que pueden fallar. Implementas solo la mitad y recibes la otra gratis, por arte de una implementación general. Y hay un remate que cierra el círculo del nivel anterior: el operador ? usa From para convertir errores al propagarlos. La conversión no es una utilidad más; es la costura invisible del manejo de errores idiomático.

🎯 Al terminar esta lección sabrás
  • Definir una conversión infalible con From y usarla con .into().
  • Entender por qué Into aparece gratis y nunca se implementa a mano.
  • Definir una conversión falible con TryFrom que devuelve Result.
  • Ver cómo ? invoca From para convertir el error que propaga.

From: la conversión que no falla

From<T> define cómo construir tu tipo a partir de un T, con la promesa de que la conversión siempre tiene éxito: sin pérdida, sin fallo posible.

struct Celsius(f64);
struct Kelvin(f64);

impl From<Celsius> for Kelvin {
    fn from(c: Celsius) -> Self {
        Kelvin(c.0 + 273.15)   // conversion exacta, nunca falla
    }
}

fn main() {
    let punto_congelacion = Kelvin::from(Celsius(0.0));   // 273.15
}

Implementas From para tus propios tipos porque la regla del huérfano te lo permite: puedes escribir un impl de un trait si el trait o el tipo son tuyos. Existe además la conversión identidad From<T> for T para todo tipo, así que convertir algo a su propio tipo siempre es válido.

Into gratis: la implementación general

Aquí ocurre una de las piezas de magia más citadas de Rust. En cuanto implementas From<T> for U, recibes automáticamente Into<U> for T, sin escribir nada. Lo hace esta implementación general de la biblioteca estándar:

// En el nucleo de Rust, conceptualmente:
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

La consecuencia es una regla de oro: implementa siempre From, nunca Into. Al hacerlo obtienes ambas direcciones, y Into es más cómoda para encadenar (x.into()) y, sobre todo, insustituible en genéricos:

fn saludar(nombre: impl Into<String>) {
    let nombre: String = nombre.into();   // acepta &str, String, Cow, ...
    println!("Hola, {nombre}");
}

fn main() {
    saludar("Ada");                 // un &str se convierte a String
    saludar(String::from("Turing")); // un String pasa por la identidad
}

Aceptar impl Into<String> deja que quien llame pase un &str, un String o cualquier cosa convertible, y tú te quedas con el String ya construido. Ergonomía para el llamador, un solo tipo para ti.

TryFrom: cuando la conversión puede fallar

No toda conversión tiene siempre sentido. Meter un i64 en un i32 falla si el número no cabe; construir un color desde un texto falla si el texto es basura. Para eso está TryFrom, que devuelve un Result con un tipo de error asociado.

fn main() {
    let ok = i32::try_from(1000_i64);      // Ok(1000)
    let mal = i32::try_from(10_000_000_000_i64); // Err: no cabe en i32
    assert!(ok.is_ok());
    assert!(mal.is_err());

    // Tu propio tipo con validacion:
    struct Porcentaje(u8);
    impl TryFrom<u8> for Porcentaje {
        type Error = &'static str;
        fn try_from(n: u8) -> Result<Self, Self::Error> {
            if n <= 100 { Ok(Porcentaje(n)) } else { Err("fuera de rango 0..=100") }
        }
    }
    assert!(Porcentaje::try_from(50).is_ok());
    assert!(Porcentaje::try_from(200).is_err());
}

Igual que From regala Into, TryFrom regala TryInto por la misma clase de implementación general. La simetría con el nivel anterior es exacta: From es la conversión total —definida para toda entrada—, y TryFrom es su versión parcial —definida solo donde tiene sentido, y honesta con un Result allí donde no—.

? y la conversión de errores con From

El remate del nivel. Cuando una función devuelve Result<_, E> y usas ? sobre un Result<_, F> con un error de otro tipo F, el operador ? convierte F en E llamando a From::from. Desazucarado, el ? sobre un error es aproximadamente esto:

// v?  cuando v es Result y falla, equivale (grosso modo) a:
match v {
    Ok(x) => x,
    Err(e) => return Err(From::from(e)),   // From convierte F en E
}

Por eso, para que ? propague sin fricción varios errores distintos hacia un error de dominio común, basta con implementar un From por cada error de origen:

enum MiError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
}

impl From<std::io::Error> for MiError {
    fn from(e: std::io::Error) -> Self { MiError::Io(e) }
}
impl From<std::num::ParseIntError> for MiError {
    fn from(e: std::num::ParseIntError) -> Self { MiError::Parse(e) }
}

fn cargar(ruta: &str) -> Result<i32, MiError> {
    let texto = std::fs::read_to_string(ruta)?;  // io::Error -> MiError via From
    let n: i32 = texto.trim().parse()?;          // ParseIntError -> MiError via From
    Ok(n)
}

Los dos ? propagan errores de tipos distintos y ambos se convierten a MiError sin una línea de conversión visible: el operador la inserta por ti llamando a From. Aquí encaja lo que sembraste en el nivel 7: el ? no es más que azúcar sobre match y From.

flowchart TD
A[expresion con interrogacion sobre un Result] --> B[Es Ok]
A --> C[Es Err de tipo F]
B --> D[Se desenvuelve y sigue]
C --> E[return Err de From from sobre e]
E --> G[El error F se convierte al error E de la funcion]
G --> H[El llamador recibe el error de dominio unificado]
style B fill:#a6e3a1,color:#11111b
style C fill:#f38ba8,color:#11111b
style D fill:#89b4fa,color:#11111b
style H fill:#cba6f7,color:#11111b
Una implementación general propaga una capacidad por todo el universo de tipos

Detén la vista en la implementación general impl<T, U> Into<U> for T where U: From<T>. Es minúscula, pero es una de las construcciones más potentes del lenguaje, y conviene entender exactamente qué hace. No convierte un tipo: fabrica una capacidad para infinitos tipos a la vez. En el instante en que alguien —tú, la biblioteca estándar, un crate cualquiera— escribe un único impl From<A> for B, esta regla general hace aparecer, sin que nadie lo escriba, el Into<B> for A correspondiente. Una definición local se propaga como una propiedad universal: es la contrapartida en el sistema de traits de lo que el derive hacía a nivel de struct, capacidad que viaja sola. Y sobre esta base tan barata Rust construye algo mucho mayor. El operador ?, la pieza que hace legible todo el manejo de errores, se apoya en From para su conversión: cada vez que un error asciende por la pila de llamadas, From::from lo traduce al vocabulario de error de la función que lo recibe, de modo que un programa entero puede tener decenas de tipos de error de bibliotecas distintas y, aun así, cada capa habla su propio idioma de errores, con las traducciones cosidas automáticamente en cada ?. Es la razón de que thiserror y anyhow, que verás más adelante, no necesiten un mecanismo nuevo: solo generan los impl From adecuados y dejan que ? haga el resto. La lección general trasciende las conversiones: en Rust, la forma de dotar de una capacidad a todo un universo de tipos no es una jerarquía de herencia ni un caso especial en el compilador, sino un impl genérico con una cláusula where. From es el ejemplo canónico, y el manejo de errores idiomático entero es su corolario.

📝
Lo esencial de las conversiones

Implementa From para conversiones que nunca fallan y recibe Into gratis por la implementación general —nunca implementes Into a mano—. Usa impl Into<T> en firmas para aceptar cualquier cosa convertible. Para conversiones falibles, TryFrom devuelve Result con un type Error, y regala TryInto igual. Y recuerda el cierre del círculo: ? convierte el error que propaga llamando a From::from, así que unificar errores de dominio es cuestión de escribir un From por cada origen.

⚔️ Convierte, y deja que ? convierta por ti
  1. Define struct Metros(f64) y struct Pies(f64), e implementa From<Metros> for Pies. Convierte con Pies::from(...) y con .into(); comprueba que ambas existen habiendo escrito una sola.
  2. Escribe fn describir(x: impl Into<String>) y llámala con un &str y con un String. Explica por qué ambas funcionan.
  3. Implementa TryFrom<i32> for Porcentaje que valide el rango 0..=100 y devuelva un Result. Pruébalo con un valor válido y uno inválido.
  4. Define un enum MiError con dos variantes y sus impl From, y escribe una función que use ? dos veces sobre errores de tipos distintos. Identifica dónde el compilador inserta la conversión.
  5. Explica con tus palabras qué hace la implementación general de Into y por qué, gracias a ella, el operador ? puede unificar errores heterogéneos sin código de conversión visible.