Conversiones en Rust: From, Into, TryFrom, FromStr y AsRef
Los programas convierten cosas todo el tiempo. El texto de un formulario se convierte en un número. Una tupla de tres números se convierte en un Color. Un tipo de error se convierte en otro. Rust tiene una pequeña familia de traits (rasgos) estándar para esto, y en cuanto los conozcas, los verás por todas partes en código real.
Piensa en estos traits como los adaptadores de enchufe que metes en la maleta para un viaje al extranjero. Cada uno sirve para una tarea. Algunos siempre encajan (From). Otros puede que no encajen, y te lo dicen (TryFrom). Otros leen una etiqueta y construyen la cosa (FromStr). Y otros simplemente te dejan conectar lo que tengas (AsRef). Esta página los recorre uno por uno, con calma. Ya conociste as en la página de Conversión de tipos y echaste un primer vistazo a From en la de Traits comunes, así que partiremos de ahí.
¿Por qué no usar simplemente as?
Sección titulada «¿Por qué no usar simplemente as?»as es rápido, pero tiene dos límites. Solo funciona entre tipos simples incorporados, como números, char y bool. Y nunca falla: si un valor no cabe, en silencio te da otro número distinto.
fn main() { let items_in_cart: i32 = 300;
// `as` mete 300 a la fuerza en un u8 y se equivoca sin avisar let squeezed = items_in_cart as u8; println!("con as: {squeezed}");
// `try_from` detecta el problema y te lo dice let checked = u8::try_from(items_in_cart); println!("con try_from: {:?}", checked);
// `from` solo existe cuando la conversión nunca puede perder datos let widened = i64::from(items_in_cart); println!("con from: {widened}");}Salida:
con as: 44con try_from: Err(TryFromIntError(PosOverflow))con from: 300Ese 44 es el tipo de bug que se esconde durante meses. Los traits de conversión lo solucionan de tres maneras:
- Son honestos.
Fromsolo se implementa cuando nada puede salir mal. Cuando algo puede salir mal, usasTryFrom, que devuelve unResult. - Funcionan con tus propios tipos. No puedes escribir
(255, 0, 0) as Color, pero sí puedes implementarFrom<(u8, u8, u8)> for Color. - El resto del código los entiende. El operador
?,.parse()y muchas funciones de bibliotecas están construidos sobre estos traits.
Así que una buena regla práctica: deja as para trucos rápidos y seguros con números, y usa los traits en todo lo demás.
From e Into: conversiones que siempre funcionan
Sección titulada «From e Into: conversiones que siempre funcionan»From dice “sé cómo construirme a partir de ese otro tipo”. Ya lo has usado. Muchos tipos estándar lo implementan:
let greeting = String::from("hola"); // &str -> Stringlet big = i64::from(42_i32); // i32 -> i64, siempre cabelet precise = f64::from(7_u8); // u8 -> f64, siempre exactolet scores = Vec::from([90, 85, 77]); // array -> Vec<i32>Fíjate en lo que falta: no existe i32::from(some_i64). Un i64 podría no caber en un i32, así que Rust no finge que siempre funciona. Volveremos a esto en la sección de TryFrom.
Implementar From para tu propio tipo
Sección titulada «Implementar From para tu propio tipo»Aquí un Color se puede construir a partir de una tupla con los valores de rojo, verde y azul:
#[derive(Debug)]struct Color { red: u8, green: u8, blue: u8,}
impl From<(u8, u8, u8)> for Color { fn from((red, green, blue): (u8, u8, u8)) -> Self { Color { red, green, blue } }}
fn main() { let orange = Color::from((255, 136, 0)); println!("{:?}", orange);
// Nunca escribimos `Into`, pero lo obtenemos gratis: let sky: Color = (135, 206, 235).into(); println!("el cielo es r={} g={} b={}", sky.red, sky.green, sky.blue);}Salida:
Color { red: 255, green: 136, blue: 0 }el cielo es r=135 g=206 b=235Esta es la parte bonita. La biblioteca estándar dice: “si B implementa From<A>, entonces A implementa automáticamente Into<B>”. Así que un solo impl From te da dos formas de convertir:
Color::from(tuple)se lee como “construye unColora partir de esto”.tuple.into()se lee como “convierte esto en lo que haga falta aquí”.
¿Por qué escribimos let sky: Color? Porque .into() no dice en qué convertir. En teoría, una tupla podría convertirse en muchos tipos. La anotación le dice a Rust a cuál te refieres. Si Rust ya puede ver el tipo de destino, por ejemplo por el parámetro de una función, no la necesitas.
También verás a menudo .into() en los parámetros de funciones. Una función como fn new(title: impl Into<String>) acepta tanto un &str como un String, y llama a title.into() por dentro. Ahí Rust conoce el destino, así que no hace falta anotación. Probarás este patrón tú mismo en el ejercicio 2 de la práctica.
From en el manejo de errores: cómo convierte ? los errores
Sección titulada «From en el manejo de errores: cómo convierte ? los errores»Aquí hay un sitio donde From hace mucho trabajo por ti sin que te des cuenta. Cuando ? se encuentra un Err, no se limita a devolverlo. Primero llama a From::from sobre el error, para convertirlo en el tipo de error que devuelve tu función.
Así que si tu función devuelve tu propio AppError, y AppError implementa From<ParseIntError>, entonces ? puede convertir por sí solo un error de lectura de número en un AppError:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { MissingEquals, BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
// Lee una línea de configuración como "max_users = 50"fn read_setting(line: &str) -> Result<(String, u32), AppError> { let (key, value) = line.split_once('=').ok_or(AppError::MissingEquals)?; let number = value.trim().parse::<u32>()?; // ParseIntError -> AppError Ok((key.trim().to_string(), number))}
fn main() { println!("{:?}", read_setting("max_users = 50")); println!("{:?}", read_setting("max_users = muchos")); println!("{:?}", read_setting("max_users 50"));}Salida:
Ok(("max_users", 50))Err(BadNumber(ParseIntError { kind: InvalidDigit }))Err(MissingEquals)Sin el impl From, ese segundo ? no compilaría, porque Rust no sabría cómo convertir un ParseIntError en un AppError. Con él, el cuerpo de tu función queda limpio. La página de Errores personalizados profundiza en cómo diseñar tipos de error, y la de anyhow y thiserror muestra crates que escriben estos impl From por ti.
TryFrom y TryInto: conversiones que pueden fallar
Sección titulada «TryFrom y TryInto: conversiones que pueden fallar»Algunas conversiones solo funcionan a veces. 300 no cabe en un u8. -5 no es un porcentaje válido. Para estos casos, Rust tiene TryFrom. Funciona como From, pero devuelve un Result, así que tienes que ocuparte del caso del “no”. Eso es bueno: el fallo no se te puede colar.
Ya viste la versión incorporada al principio de esta página: u8::try_from(300_i32) devuelve un Err en lugar de un número equivocado. Y igual que From viene con Into, TryFrom viene con TryInto, así que let small: Result<u8, _> = big.try_into(); también funciona. TryInto está en el preludio desde la edición 2021, así que no necesitas importar nada.
Implementar TryFrom para tu propio tipo
Sección titulada «Implementar TryFrom para tu propio tipo»Vamos a crear un tipo Percentage que solo pueda contener valores de 0 a 100. Si cada Percentage se crea mediante try_from, entonces todos los Percentage de tu programa son válidos. Compruebas una vez, en la puerta, y nunca más.
#[derive(Debug)]struct Percentage(u8);
#[derive(Debug)]struct OutOfRange(i32);
impl TryFrom<i32> for Percentage { type Error = OutOfRange;
fn try_from(value: i32) -> Result<Self, Self::Error> { if (0..=100).contains(&value) { Ok(Percentage(value as u8)) // seguro: acabamos de comprobar el rango } else { Err(OutOfRange(value)) } }}
fn main() { println!("{:?}", Percentage::try_from(85)); println!("{:?}", Percentage::try_from(150));
let battery: Result<Percentage, _> = (-5).try_into(); println!("{:?}", battery);}Salida:
Ok(Percentage(85))Err(OutOfRange(150))Err(OutOfRange(-5))La línea type Error = OutOfRange; le dice a Rust qué tipo de error devuelve esta conversión. Lo eliges tú. Aquí lleva dentro el valor incorrecto, para que quien reciba el error pueda mostrar un mensaje útil.
FromStr y .parse(): construir valores a partir de texto
Sección titulada «FromStr y .parse(): construir valores a partir de texto»El texto es lo que más vas a convertir. Los archivos, los argumentos de la línea de comandos y los formularios web te dan cadenas de texto. El trait FromStr describe “cómo construir este tipo a partir de un &str”, y .parse() es la forma cómoda de llamarlo.
Cuando escribes "42".parse::<i32>(), Rust busca la implementación de FromStr para i32 y la ejecuta. La parte ::<i32> (la gente la llama “turbofish”) le dice a Rust qué tipo quieres. Una anotación de tipo en la variable también funciona.
let age = "42".parse::<i32>(); // Ok(42)let price: Result<f64, _> = "4.99".parse(); // Ok(4.99)let broken = "cuarenta".parse::<i32>(); // Err(ParseIntError { kind: InvalidDigit })Cada llamada devuelve un Result, porque el texto siempre puede venir mal.
Implementar FromStr para tu propio tipo
Sección titulada «Implementar FromStr para tu propio tipo»Imagina una tienda de marcos para cuadros donde los clientes escriben medidas como "30x40". Vamos a enseñarle a Rust a leer eso:
use std::str::FromStr;
#[derive(Debug)]struct Size { width: u32, height: u32,}
#[derive(Debug)]enum SizeError { MissingX, BadNumber,}
impl FromStr for Size { type Err = SizeError;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (w, h) = text.split_once('x').ok_or(SizeError::MissingX)?; let width = w.trim().parse().map_err(|_| SizeError::BadNumber)?; let height = h.trim().parse().map_err(|_| SizeError::BadNumber)?; Ok(Size { width, height }) }}
fn main() { println!("{:?}", "30x40".parse::<Size>());
println!("{:?}", "30 por 40".parse::<Size>()); println!("{:?}", "30xgrande".parse::<Size>());}Salida:
Ok(Size { width: 30, height: 40 })Err(MissingX)Err(BadNumber)Dos detalles pequeños. Primero, FromStr llama a su tipo de error Err, no Error como hace TryFrom. Es una rareza histórica de nombres, así que no te preocupes si al principio los confundes; el compilador te lo recordará. Segundo, necesitas use std::str::FromStr; para implementar el trait, pero no para llamar a .parse().
.map_err(|_| SizeError::BadNumber) cambia el error de lectura del número por nuestro propio error. Es una alternativa práctica a escribir un impl From cuando solo lo necesitas en un sitio.
AsRef: acepta cualquier cosa que te pueda prestar una vista
Sección titulada «AsRef: acepta cualquier cosa que te pueda prestar una vista»Los traits que hemos visto hasta ahora crean un valor nuevo. AsRef es distinto. No construye nada. Solo dice “puedo darte una vista prestada y barata de mí mismo como ese tipo”.
¿Para qué querrías eso? Supón que escribes una función que cuenta palabras. Si recibe &str, quien tenga un String tiene que escribir &name. Si recibe String, quien tenga un &str tiene que crear un String nuevo. Con impl AsRef<str>, todos funcionan sin más:
fn word_count(text: impl AsRef<str>) -> usize { text.as_ref().split_whitespace().count()}
fn main() { let note = String::from("comprar leche y huevos");
println!("{}", word_count("hola mundo rust")); // &str println!("{}", word_count(¬e)); // &String println!("{}", word_count(note)); // String}Salida:
344Dentro de la función, text.as_ref() te da un &str normal, sea lo que sea lo que haya pasado quien llama.
AsRef en la biblioteca estándar: rutas
Sección titulada «AsRef en la biblioteca estándar: rutas»Seguramente ya has usado AsRef sin darte cuenta. Las funciones de archivos de std::fs tienen este aspecto:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>Por eso funcionan fs::read_to_string("notas.txt"), fs::read_to_string(my_string) y fs::read_to_string(my_path_buf). Puedes usar el mismo truco en tu propio código. Este ejemplo solo mira los nombres de las rutas, así que no toca ningún archivo real:
use std::path::{Path, PathBuf};
fn extension_of(path: impl AsRef<Path>) -> String { match path.as_ref().extension() { Some(ext) => ext.to_string_lossy().to_string(), None => String::from("(ninguna)"), }}
fn main() { println!("{}", extension_of("notas.txt")); println!("{}", extension_of(String::from("vacaciones.jpeg"))); println!("{}", extension_of(PathBuf::from("respaldos/2024/sitio.tar.gz"))); println!("{}", extension_of("README"));}Salida:
txtjpeggz(ninguna)AsMut es la misma idea, pero presta una vista mutable. Por ejemplo, tanto un Vec como un array pueden prestarse como un slice (porción) mutable:
fn reset_all(scores: &mut impl AsMut<[u32]>) { for score in scores.as_mut() { *score = 0; }}
// Las dos funcionan:// reset_all(&mut vec![12, 7, 30]);// reset_all(&mut [3, 1, 4]);Usarás AsMut mucho menos que AsRef. Basta con que lo reconozcas cuando lo veas.
¿Cuál uso?
Sección titulada «¿Cuál uso?»Cuando no lo tengas claro, esta tabla es un buen punto de partida:
| Quieres… | Usa | Ejemplo |
|---|---|---|
| Convertir rápido entre tipos numéricos, aceptando que se recorte | as |
x as f64 |
| Convertir cuando nunca puede fallar | From |
i64::from(n), String::from("hola") |
| Lo mismo, cuando el tipo de destino ya se conoce | Into |
let s: String = "hola".into(); |
| Convertir cuando puede fallar | TryFrom / TryInto |
u8::try_from(300) |
| Construir un valor a partir de texto | FromStr / .parse() |
"42".parse::<i32>() |
| Aceptar sin coste muchas entradas tipo cadena o tipo ruta | AsRef |
fn f(p: impl AsRef<Path>) |
| Convertir un valor en texto para que lo lean personas | Display → .to_string() |
42.to_string() |
La última fila sale gratis: implementa Display (mira Traits comunes) y cada valor de tu tipo tendrá un método .to_string().
Errores comunes
Sección titulada «Errores comunes»Llamar a .into() cuando Rust no puede saber el tipo de destino.
fn main() { let name = "Ferris".into(); println!("{name}");}error[E0283]: type annotations needed --> src/main.rs:2:9 |2 | let name = "Ferris".into(); | ^^^^ ---- type must be known at this point | = note: the type must implement `From<&str>` = note: required for `&str` to implement `Into<_>`help: consider giving `name` an explicit type |2 | let name: /* Type */ = "Ferris".into(); | ++++++++++++El compilador dice que necesita una anotación de tipo: tiene que saber el tipo en ese punto. Un &str puede convertirse en un String, un Box<str>, un Vec<u8> y más. Rust no va a adivinar. Añade el tipo: let name: String = "Ferris".into();, o escribe directamente String::from("Ferris").
Implementar Into en lugar de From. Parece que debería ser lo mismo, así que es una trampa fácil:
use std::num::ParseIntError;
#[derive(Debug)]struct AppError(String);
impl Into<AppError> for ParseIntError { fn into(self) -> AppError { AppError(self.to_string()) }}
fn read_port(text: &str) -> Result<u16, AppError> { let port = text.parse::<u16>()?; Ok(port)}error[E0277]: `?` couldn't convert the error to `AppError` --> src/main.rs:13:35 |12 | fn read_port(text: &str) -> Result<u16, AppError> { | --------------------- expected `AppError` because of this13 | let port = text.parse::<u16>()?; | --------------^ the trait `From<ParseIntError>` is not implemented for `AppError` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` traitEl mensaje dice que ? no pudo convertir el error en AppError porque AppError no implementa From<ParseIntError>, y la nota recuerda que ? convierte los errores usando el trait From. El regalo solo va en un sentido: implementar From te da Into, pero implementar Into no te da From. Y ?, igual que mucho código de bibliotecas, pide From. Así que implementa siempre From y deja que Into venga solo. (Clippy, el linter de Rust, incluso avisa de esto).
Práctica
Sección titulada «Práctica»1. Leer una hora del reloj
Sección titulada «1. Leer una hora del reloj»Crea un struct Time con hour: u8 y minute: u8. Implementa FromStr para que funcione "09:45".parse::<Time>(). Devuelve un error si no hay :, si una de las partes no es un número, o si la hora es mayor que 23 o los minutos mayores que 59. Pruébalo con "09:45", "25:00" y "almuerzo". Pista: split_once(':') te da las dos mitades. Como tipo de error basta con un String.
Ver solución
use std::str::FromStr;
#[derive(Debug)]struct Time { hour: u8, minute: u8,}
impl FromStr for Time { type Err = String;
fn from_str(text: &str) -> Result<Self, Self::Err> { let (h, m) = text.split_once(':').ok_or("falta ':'")?; let hour: u8 = h.parse().map_err(|_| "la hora no es un número")?; let minute: u8 = m.parse().map_err(|_| "los minutos no son un número")?;
if hour > 23 || minute > 59 { return Err(format!("{text} no es una hora real")); } Ok(Time { hour, minute }) }}
fn main() { for input in ["09:45", "25:00", "almuerzo"] { match input.parse::<Time>() { Ok(t) => println!("{input} -> {}h {}m", t.hour, t.minute), Err(e) => println!("{input} -> error: {e}"), } }}Salida:
09:45 -> 9h 45m25:00 -> error: 25:00 no es una hora realalmuerzo -> error: falta ':'El ? después de .ok_or("falta ':'") funciona porque String implementa From<&str>. Es el mismo truco de From de la sección de manejo de errores, esta vez con un tipo estándar.
2. Un temporizador de cocina
Sección titulada «2. Un temporizador de cocina»Crea un struct Timer con minutes: u32 y seconds: u32. Implementa From<u32> para que un número total de segundos se convierta en un Timer (150 pasa a ser 2 minutos y 30 segundos). Después escribe fn start(timer: impl Into<Timer>), que imprima el tiempo, y llámala con 150 y con Timer { minutes: 1, seconds: 5 }. Pista: los minutos son total / 60 y el resto es total % 60. Todo tipo ya se convierte en sí mismo, así que pasar un Timer también funciona.
Ver solución
struct Timer { minutes: u32, seconds: u32,}
impl From<u32> for Timer { fn from(total: u32) -> Self { Timer { minutes: total / 60, seconds: total % 60 } }}
fn start(timer: impl Into<Timer>) { let t = timer.into(); println!("Temporizador puesto en {}:{:02}", t.minutes, t.seconds);}
fn main() { start(150_u32); start(Timer { minutes: 1, seconds: 5 });}Salida:
Temporizador puesto en 2:30Temporizador puesto en 1:05Solo escribimos From<u32>, y aun así start acepta tanto un número como un Timer. El número funciona gracias a nuestro From, y el Timer funciona porque la biblioteca estándar dice que todo tipo se convierte en sí mismo.
Resumen
Sección titulada «Resumen»assirve para conversiones rápidas entre números y puede perder datos sin avisar. Los traits de conversión son explícitos, seguros y funcionan con tus propios tipos.- Implementa
Frompara las conversiones que siempre funcionan, y obtendrásIntogratis..into()necesita saber su tipo de destino. - El operador
?llama aFrompara convertir tipos de error, así queimpl From<OtherError> for MyErrormantiene corto el manejo de errores. - Usa
TryFrom/TryIntocuando una conversión puede fallar. Devuelven unResult. - Implementa
FromStrpara que.parse::<TuTipo>()funcione con texto. - Usa
impl AsRef<str>oimpl AsRef<Path>para aceptar muchos tipos de entrada sin coste, sin copiar nada.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. Implementas From<Celsius> for Fahrenheit. ¿Qué más obtienes automáticamente?
2. ¿Cuál es la forma segura de convertir en u8 un i32 que podría valer 300?
3. ¿Qué implementas para que funcione "30x40".parse::<Size>()?
4. ¿Por qué no compila let name = "Ferris".into();?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»std::convert— documentación de la biblioteca estándar (en inglés): el hogar deFrom,Into,TryFrom,TryInto,AsRefyAsMut, con una breve guía de cuándo implementar cada uno.FromStr— documentación de la biblioteca estándar (en inglés): el trait que hay detrás de.parse(), con un ejemplo completo de cómo implementarlo.AsRef— documentación de la biblioteca estándar (en inglés): en qué se diferenciaAsRefdeBorrow, y por qué se usa para conversiones baratas entre referencias.- Conversion — Rust by Example (en inglés): capítulos cortos y ejecutables sobre
From/Into,TryFrom/TryIntoy la conversión desde y hacia cadenas de texto. - Conversions — Rust API Guidelines (en inglés): los consejos de la comunidad sobre qué traits de conversión deberían implementar tus propios tipos.