Este documento es una guía sobre el manejo de errores, genéricos, traits y pruebas en el lenguaje de programación Rust.
Manejo de Errores
Rust distingue dos tipos de errores:
- Recuperables: Errores que, tras una intervención, permiten continuar la ejecución del programa (ej. archivo no encontrado).
- No recuperables: Errores que deben forzar la terminación del programa (ej. acceso fuera de límites de un array).
Para errores recuperables, Rust utiliza el tipo Result<T, E>. Para errores no recuperables, se emplea la macro panic!.
Errores No Recuperables y panic!
La ejecución de panic! puede ocurrir por dos motivos:
- Invocación explícita de la macro
panic!. - Ocurrencia de un error grave en el programa (ej. acceso inválido a un array).
Por defecto, Rust muestra un mensaje de error, realiza el desenrollado (unwinding) de la pila, libera recursos y termina el programa. La variable de entorno RUST_BACKTRACE puede ser utilizada para obtener información detallada de la pila de llamadas, facilitando la depuración.
Desenvolvimiento vs. Abortar:
El comportamiento por defecto ante un panic es el desenrollado, donde Rust retrocede en la pila de llamadas liberando memoria y recursos. Una alternativa es la opción abort, que termina el programa inmediatamente sin liberar recursos; el sistema operativo se encarga de la limpieza. Para habilitar esta opción, se añade panic = 'abort' en la sección [profile.release] del archivo Cargo.toml.
Ejemplo de llamada explícita a panic!:
fn main() {
panic!("¡Colisión y quema!");
}
Al ejecutar este código, se obtendrá una salida similar a:
thread 'main' panicked at '¡Colisión y quema!', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Rastreo de Pila (Backtrace)
A diferencia de C, donde el aceso fuera de límites de un array es un comportamiento indefinido (potencialmente resultando en desbordamiento de búfer), en Rust provoca un error que detiene la ejecución.
Para obtener el rastreo de pila, se utiliza la varible de entorno RUST_BACKTRACE:
RUST_BACKTRACE=1: Muestra información básica.RUST_BACKTRACE=full: Muestra información completa.
El rastreo de pila lista las funciones llamadas. Las líneas anteriores indican las funciones que llamaron a la línea actual, y las líneas posteriores indican las funciones llamadas por la línea actual.
Errores Recuperables y Result
El tipo Result<T, E>
Result<T, E> es un enum definido como:
enum Result<T, E> {
Ok(T),
Err(E),
}
Ok(T) envuelve un valor de tipo T en caso de éxito, y Err(E) envuelve un valor de tipo E en caso de error.
Ejemplo abriendo un archivo:
use std::fs::File;
fn main() {
let result_archivo = File::open("saludo.txt");
let archivo = match result_archivo {
Ok(f) => f,
Err(error) => panic!("Problema al abrir el archivo: {:?}", error),
};
}
- Si la apertura es exitosa,
Okcontendrá el descriptor del archivo. - Si falla,
Errcontendrá la información del error del sistema operativo.
Manejo Detallado de Errores
Para diferenciar entre distintos tipos de errores, se pueden anidar sentencias match:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let result_archivo = File::open("saludo.txt");
let archivo = match result_archivo {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("saludo.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problema al crear el archivo: {:?}", e),
},
other_error => {
panic!("Problema al abrir el archivo: {:?}", other_error);
}
},
};
}
ErrorKind es un enum que requiere ser importado explícitamente.
unwrap() y expect()
Estos métodos ofrecen una forma más concisa de manejar errores en Result, recurriendo a panic! si el valor es Err.
use std::fs::File;
fn main() {
let archivo = File::open("saludo.txt").unwrap();
}
Si el archivo no existe, esto producirá un panic con el error del sistema.
use std::fs::File;
fn main() {
let archivo = File::open("saludo.txt")
.expect("¡Fallo al leer el archivo!");
}
expect() es similar a unwrap() pero permite un mensaje de error personalizado, siendo preferido en producción por su claridad.
Propagación de Errores y el Operador ?
Propagación de Errores
En lugar de manejar un error dentro de una función, es común propagarlo a la función que la llama. Esto permite un control más centralizado del flujo de errores.
use std::fs::File;
use std::io::{self, Read};
fn leer_nombre_usuario() -> Result<String, io::Error> {
let result_archivo = File::open("nombre_usuario.txt");
let mut archivo_usuario = match result_archivo {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut nombre = String::new();
match archivo_usuario.read_to_string(&mut nombre) {
Ok(_) => Ok(nombre),
Err(e) => Err(e),
}
}
El Operador ?
El operador ? simplifica la propagación de errores, actuando de manera similar a un match que retorna Err tempranamente.
use std::fs::File;
use std::io::{self, Read};
fn leer_nombre_usuario() -> Result<String, io::Error> {
let mut archivo_usuario = File::open("nombre_usuario.txt")?;
let mut nombre = String::new();
archivo_usuario.read_to_string(&mut nombre)?;
Ok(nombre)
}
La versión encadenada es aún más concisa:
use std::fs::File;
use std::io::{self, Read};
fn leer_nombre_usuario() -> Result<String, io::Error> {
let mut nombre = String::new();
File::open("nombre_usuario.txt")?.read_to_string(&mut nombre)?;
Ok(nombre)
}
El operador ? también maneja la conversión de tipos de error usando el trait From.
Condiciones para Usar ?
El operador ? solo puede ser usado en funciones cuyo tipo de retorno sea compatible con el valor devuelto por la operación.
Por ejemplo, en la función main, que retorna ():
use std::fs::File;
fn main() {
let archivo = File::open("saludo.txt")?; // Error: main no puede retornar Result
}
Para solucionar esto, la función main puede retornar un tipo Result:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let archivo = File::open("saludo.txt")?;
Ok(())
}
Si main retorna Ok(()), el código de salida es 0. Si retorna Err, es un valor distinto de cero.
Genéricos, Traits y Lifetimes
Genéricos
Los genéricos permiten escribir código que opera sobre tipos abstractos, reduciendo la duplicación.
Definición de Genéricos
Se utilizan corchetes angulares <> para definir parámetros de tipo genéricos, comúnmente representados por T.
Genéricos en Funciones
fn encontrar_mayor<T: PartialOrd>(lista: &[T]) -> &T {
let mut mayor = &lista[0];
for item in lista {
if item > mayor {
mayor = item;
}
}
mayor
}
Se añade la restricción PartialOrd para asegurar que los tipos puedan ser comparados.
Genéricos en Estructuras
struct Punto<T> {
x: T,
y: T,
}
struct PuntoDos<T, U> {
x: T,
y: U,
}
Punto<T> usa un solo tipo para ambos campos, mientras que PuntoDos<T, U> permite tipos diferentes.
Genéricos en Enums
Los enums Option<T> y Result<T, E> son ejemplos comunes de uso de genéricos.
Genéricos en Métodos
impl<T> Punto<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Punto<f32> {
fn distancia_origen(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
El primer bloque implementa un método genérico para cualquier tipo T. El segundo implementa un método específico solo para Punto<f32>.
Traits
Los traits definen funcionalidades compartidas entre tipos.
Definición de Traits
pub trait Resumen {
fn resumir(&self) -> String;
}
Implementación de Traits
pub struct Tweet {
pub autor: String,
pub contenido: String,
pub longitud: u32,
}
impl Resumen for Tweet {
fn resumir(&self) -> String {
format!("{} - {}", self.autor, self.contenido)
}
}
La implementación de traits está sujeta a la regla del "huérfano" (orphan rule), que limita la implementación de traits en tipos si ni el trait ni el tipo son de nuestro crate actual.
Pruebas
Las pruebas en Rust verifican que el código funcione como se espera.
Funciones de Prueba
Las funciones de prueba se marcan con el atributo #[test].
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
Macro assert!
Evalúa una expresión booleana. Si es true, la prueba continúa; si es false, llama a panic!.
Macros assert_eq! y assert_ne!
Comprueban la igualdad o desigualdad entre dos valores. El orden de los argumentos no importa.
pub fn sumar_dos(a: i32) -> i32 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suma_correcta() {
assert_eq!(sumar_dos(2), 4);
}
}
Mensajes de Error Personalizados
Se pueden añaddir mensajes personalizados a las macros de aserción.
#[test]
fn saludo_contiene_nombre() {
let resultado = saludar("Ana");
assert!(
resultado.contains("Ana"),
"El saludo no contiene el nombre, valor: `{}`",
resultado
);
}
#[should_panic]
Este atributo indica que la función de prueba debe fallar (hacer panic). Si no hace panic, la prueba falla.
#[derive(Debug)]
struct Adivinanza {
valor: i32,
}
impl Adivinanza {
fn new(valor: i32) -> Adivinanza {
if valor < 1 || valor > 100 {
panic!("El valor debe estar entre 1 y 100, se recibió: {}.", valor);
}
Adivinanza { valor }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn valor_mayor_a_100() {
Adivinanza::new(200);
}
}
Pruebas con Result<T, E>
Se pueden escribir pruebas que retornen Result.
#[cfg(test)]
mod tests {
#[test]
fn funciona_con_result() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("2 + 2 no es 4"))
}
}
}
Las pruebas que retornan Result no pueden usar #[should_panic].
Controlando la Ejecución de Pruebas
Paralelo vs. Secuencial
Por defecto, las pruebas se ejecutan en paralelo. Para ejecutarlas secuencialmente, se usa cargo test -- --test-threads=1.
Mostrar Salida
Para ver la salida de las pruebas que pasan, se usa cargo test -- --show-output.
Ejecutar Pruebas por Nombre
Se pueden ejecutar pruebas específicas nombrando la función o usando patrones.
cargo test nombre_prueba: Ejecuta una prueba específica.cargo test patron: Ejecuta pruebas cuyo nombre coincida con el patrón.
Ignorar Pruebas
El atributo #[ignore] omite una prueba.
#[test]
#[ignore]
fn prueba_ignorada() { /* ... */ }
Organización del Código de Pruebas
Pruebas Unitarias
Se organizan en módulos tests dentro de cada archivo, marcados con #[cfg(test)]. Permiten probar funciones privadas.
pub fn sumar_dos(a: i32) -> i32 {
sumador_interno(a, 2)
}
fn sumador_interno(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interno() {
assert_eq!(4, sumador_interno(2, 2));
}
}
Pruebas de Integración
Se ubican en el directorio tests y tratan el código como una librería externa, probando solo las funciones públicas.
Estructura de directorios:
├── src
│ └── lib.rs
└── tests
├── integration_test.rs
└── common
└── mod.rs
Las pruebas de integración solo aplican a crates de librería (lib crates).