Result en Rust
Si Option representa un valor que puede faltar, Result representa una operación que puede fallar. Y, a diferencia de un valor que simplemente no está, un fallo suele venir con un motivo.
Cómo es Result
Sección titulada «Cómo es Result»Result es otro enum de la biblioteca estándar, esta vez con dos variantes que llevan datos:
enum Result<T, E> { Ok(T), // éxito, con un valor dentro Err(E), // fallo, con un error dentro}Una función que puede fallar
Sección titulada «Una función que puede fallar».parse(), que conociste en la página de conversión de tipos, es un buen ejemplo real: convertir texto en un número puede fallar si el texto no es un número de verdad:
fn main() { let good: Result<i32, std::num::ParseIntError> = "42".parse(); let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good); println!("{:?}", bad);}Salida:
Ok(42)Err(ParseIntError { kind: InvalidDigit })ParseIntError { kind: InvalidDigit } es el error que define la propia biblioteca estándar de Rust, por eso aparece en inglés: significa que el texto contiene un “dígito no válido”.
Manejar un Result con match
Sección titulada «Manejar un Result con match»fn main() { let input = "42";
match input.parse::<i32>() { Ok(number) => println!("Convertido: {number}"), Err(e) => println!("No se pudo convertir: {e}"), }}Tiene la misma forma que manejar un Option con match, solo que la rama Err te entrega un valor de error de verdad, que puedes examinar o mostrar, y no un simple “aquí no había nada”.
Escribir tu propia función que devuelve Result
Sección titulada «Escribir tu propia función que devuelve Result»fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("no se puede dividir entre cero")) } else { Ok(a / b) }}
fn main() { match divide(10.0, 2.0) { Ok(result) => println!("Resultado: {result}"), Err(e) => println!("Error: {e}"), }
match divide(10.0, 0.0) { Ok(result) => println!("Resultado: {result}"), Err(e) => println!("Error: {e}"), }}Salida:
Resultado: 5Error: no se puede dividir entre ceroDevolver Result<f64, String> obliga a todo el que llame a la función a tener en cuenta que puede fallar. No hay forma de tratar por accidente una división entre cero como si fuera un número normal.
El operador ?
Sección titulada «El operador ?»Si encadenas varias operaciones que pueden fallar, acabas con bloques match metidos unos dentro de otros. El operador ? lo aplana. Significa: “si esto es Err, devuelve ese error ahora mismo desde la función actual; si no, dame el valor que hay dentro de Ok”.
fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> { let number = input.parse::<i32>()?; // sale de la función si la conversión falla Ok(number * 2)}
fn main() { println!("{:?}", parse_and_double("21")); println!("{:?}", parse_and_double("oops"));}Salida:
Ok(42)Err(ParseIntError { kind: InvalidDigit })Sin ?, esa línea necesitaría su propio match solo para sacar number o salir antes con el error. ? hace exactamente eso con un solo carácter. Eso sí, solo funciona dentro de una función que también devuelve un Result (o un Option), porque así tiene a dónde devolver el error.
Result o panic!
Sección titulada «Result o panic!»Rust también tiene panic!, que detiene el programa al instante; lo verás con más detalle en la siguiente página. Como regla general: usa Result para los fallos que quien llama podría querer manejar (una entrada incorrecta, un archivo que no existe), y deja panic! para situaciones que indican un error real en el código (un bug), en las que seguir adelante sería peor que parar.
En cuanto tus funciones pueden fallar de varias maneras distintas, un simple error String deja de ser suficiente. La página de errores personalizados te enseña a definir tu propio tipo de error, y la de anyhow y thiserror te muestra los crates que usa la mayoría de los proyectos de Rust para ahorrarse el código repetitivo.
Práctica
Sección titulada «Práctica»1. Comprobar una edad
Sección titulada «1. Comprobar una edad»Escribe fn parse_age(input: &str) -> Result<u32, String>. Debe devolver Err con un mensaje útil si el texto no es un número, o si el número pasa de 150. Pruébala con "34", "abc" y "200". Pista: usa match sobre input.parse::<u32>(), y dentro de la rama Ok usa un if para comprobar el tamaño.
Ver solución
fn parse_age(input: &str) -> Result<u32, String> { match input.parse::<u32>() { Ok(age) => { if age > 150 { Err(format!("{age} es demasiado para ser una edad real")) } else { Ok(age) } } Err(_) => Err(format!("'{input}' no es un número")), }}
fn main() { for input in ["34", "abc", "200"] { match parse_age(input) { Ok(age) => println!("Edad: {age}"), Err(e) => println!("Error: {e}"), } }}Salida:
Edad: 34Error: 'abc' no es un númeroError: 200 es demasiado para ser una edad real2. Sumar dos números con ?
Sección titulada «2. Sumar dos números con ?»Escribe fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>, que convierta los dos strings en números y devuelva su suma. Usa ? en lugar de match. Imprime el resultado para ("10", "20") y para ("10", "veinte"). Pista: necesitas un ? por cada parse, y la última línea envuelve la suma en Ok(...).
Ver solución
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> { let x = a.parse::<i32>()?; let y = b.parse::<i32>()?; Ok(x + y)}
fn main() { println!("{:?}", add_strings("10", "20")); println!("{:?}", add_strings("10", "veinte"));}Salida:
Ok(30)Err(ParseIntError { kind: InvalidDigit })ParseIntError { kind: InvalidDigit } es cómo Rust muestra el error con {:?}: el tipo de fallo es “dígito no válido”.
Resumen
Sección titulada «Resumen»Result<T, E>representa una operación que puede salir bien (Ok) o fallar (Err) con un valor de error.matchmaneja los dos casos de forma explícita, igual que conOption.- El operador
?devuelve elErrautomáticamente, así que no necesitas unmatchdespués de cada llamada que puede fallar. - Prefiere
Resultapanic!para los errores de los que quien llama podría recuperarse.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Cuáles son las dos variantes de Result<T, E>?
2. ¿Qué hace el operador ? dentro de una función que devuelve un Result?
3. Según esta página, ¿cuándo conviene usar Result en lugar de panic!?
4. En el ejemplo de divide de esta página, ¿qué devuelve divide(10.0, 0.0)?
5. ¿Por qué el operador ? solo funciona dentro de una función que también devuelve un Result (o un Option)?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»- Errores recuperables con Result — El libro de Rust (en inglés): el capítulo oficial sobre
Result,unwrap,expecty el operador?, con ejemplos que abren archivos. std::result— documentación de la biblioteca estándar (en inglés): un repaso de cómo trabajar conResult, incluidos métodos comomap_err,unwrap_oryok.- Result — Rust by Example (en inglés): ejemplos ejecutables de cómo manejar un
Resulty devolverlo desdemain. - The ? operator — Rust by Example (en inglés): una mirada breve y centrada en cómo
?sustituye a los bloquesmatchanidados.