panic! o no panic!

Entonces, ¿cómo decides cuándo debes llamar a panic! y cuándo debes devolver Result? Cuando el código entra en panic, no hay forma de recuperarse. Podrías llamar a panic! para cualquier situación de error, ya sea que haya una forma posible de recuperarse o no, pero entonces estás tomando la decisión de que una situación es irreparable en nombre del código que llama. Cuando eliges devolver un valor Result, le das al código que llama opciones. El código que llama podría elegir intentar recuperarse de una manera que sea apropiada para su situación, o podría decidir que un valor Err en este caso es irreparable, por lo que puede llamar a panic! y convertir su error recuperable en uno irreparable. Por lo tanto, devolver Result es una buena opción predeterminada cuando estás definiendo una función que podría fallar.

En situaciones como ejemplos, código de prototipo y pruebas, es más apropiado escribir código que entre en panic en lugar de devolver un Result. Veamos por qué, luego discutiremos situaciones en las que el compilador no puede darse cuenta de que la falla es imposible, pero tú como humano puedes. El capítulo concluirá con algunas pautas generales sobre cómo decidir si entrar en panic en el código de la biblioteca.

Ejemplos, código de prototipo y test

Cuando estás escribiendo un ejemplo para ilustrar algún concepto, también incluir código de manejo de errores robusto puede hacer que el ejemplo sea menos claro. En los ejemplos, se entiende que una llamada a un método como unwrap que podría entrar en panic se entiende como un marcador de posición para la forma en que desea que su aplicación maneje los errores, que puede diferir según lo que el resto de su código está haciendo.

De manera similar, los métodos unwrap y expect son muy útiles cuando se prototipa, antes de que estés listo para decidir cómo manejar los errores. Dejan marcadores claros en tu código para cuando estés listo para hacer que tu programa sea más robusto.

Si una llamada a un método falla en una prueba, querrás que toda la prueba falle, incluso si ese método no es la funcionalidad en prueba. Debido a que panic! es la forma en que una prueba se marca como fallida, llamar a unwrap o expect es exactamente lo que debería suceder.

Casos en los que tienes mas informacion que el compilador

También sería apropiado llamar a unwrap o expect cuando tienes alguna otra lógica que garantiza que el Result tendrá un valor Ok, pero la lógica no es algo que el compilador entiende. Aún tendrás un valor Result que debes manejar: la operación que estás llamando aún tiene la posibilidad de fallar en general, incluso si es lógicamente imposible en tu situación particular. Si puedes asegurar inspeccionando manualmente el código que nunca tendrás una variante Err, es perfectamente aceptable llamar a unwrap, e incluso mejor documentar la razón por la que crees que nunca tendrás una variante Err en el texto de expect. Aquí hay un ejemplo:

fn main() {
    use std::net::IpAddr;

    let home: IpAddr = "127.0.0.1"
        .parse()
        .expect("Hardcoded IP address should be valid");
}

Aquí estamos creando una instancia IpAddr analizando una cadena codificada. Podemos ver que 127.0.0.1 es una dirección IP válida, por lo que es aceptable usar expect aquí. Sin embargo, tener una cadena válida codificada no cambia el tipo de retorno del método parse: aún obtenemos un valor Result, y el compilador aún nos hará manejar el Result como si la variante Err fuera una posibilidad porque el compilador no es lo suficientemente inteligente como para ver que esta cadena es siempre una dirección IP válida. Si la cadena de dirección IP proviniera de un usuario en lugar de estar codificada en el programa y, por lo tanto, tuviera una posibilidad de falla, definitivamente querríamos manejar el Result de una manera más robusta en su lugar. Mencionar la suposición de que esta dirección IP está codificada nos indicará que cambiemos expect a un mejor código de manejo de errores si en el futuro necesitamos obtener la dirección IP de otra fuente.

Pautas para el manejo de errores

Es aconsejable que tu código entre en panic cuando sea posible que tu código termine en un estado incorrecto. En este contexto, un estado incorrecto es cuando se ha roto alguna suposición, garantía, contrato o invariante, como cuando se pasan valores no válidos, valores contradictorios o valores faltantes a tu código, más uno o más de los siguientes:

  • El mal estado es algo inesperado, a diferencia de algo que probablemente suceda ocasionalmente, como un usuario que ingresa datos en el formato incorrecto.
  • Tu código después de este punto debe confiar en no estar en este mal estado, en lugar de verificar el problema en cada paso.
  • No hay una buena manera de codificar esta información en los tipos que usas. Trabajaremos a través de un ejemplo de lo que queremos decir en la sección “Codificación de estados y comportamientos como tipos” del Capítulo 17.

Si alguien llama a tu código y pasa valores que no tienen sentido, es mejor devolver un error si puedes para que el usuario de la biblioteca pueda decidir qué hacer en ese caso. Sin embargo, en los casos en que continuar podría ser inseguro o dañino, la mejor opción podría ser llamar a panic! y alertar a la persona que usa tu biblioteca sobre el error en su código para que puedan solucionarlo durante el desarrollo. De manera similar, panic! a menudo es apropiado si estás llamando a un código externo que está fuera de tu control y devuelve un estado no válido que no tienes forma de solucionar.

Sin embargo, cuando se espera que falle, es más apropiado devolver un Result que hacer una llamada a panic!. Los ejemplos incluyen un analizador que recibe datos con formato incorrecto o una solicitud HTTP que devuelve un estado que indica que has alcanzado un límite de velocidad. En estos casos, devolver un Result indica que el fallo es una posibilidad esperada que el código llamado decidida cómo manejarlo.

Cuando tu código realiza una operación que podría poner a un usuario en riesgo si se llama con valores no válidos, tu código debe verificar primero que los valores sean válidos y entrar en panic si los valores no son válidos. Esto es principalmente por razones de seguridad: intentar operar con datos no válidos puede exponer tu código a vulnerabilidades. Esta es la razón principal por la que la biblioteca estándar llamará a panic! si intentas un acceso a memoria fuera de los límites: intentar acceder a la memoria que no pertenece a la estructura de datos actual es un problema de seguridad común. Las funciones suelen tener contratos: su comportamiento solo está garantizado si las entradas cumplen con requisitos particulares. Entrar en panic cuando se viola el contrato tiene sentido porque una violación del contrato siempre indica un error del lado del llamador y no es un tipo de error que deseas que el código llamado tenga que manejar explícitamente. De hecho, no hay una manera razonable para que el código de llamada se recupere; los programadores que llaman deben corregir el código. Los contratos para una función, especialmente cuando una violación causará un panic, deben explicarse en la documentación de la API de la función.

Sin embargo, tener muchas comprobaciones de errores en todas tus funciones sería verboso y molesto. Afortunadamente, puedes usar el sistema de tipos de Rust (y, por lo tanto, la comprobación de tipos realizada por el compilador) para hacer muchas de las comprobaciones por ti. Si tu función tiene un tipo particular como parámetro, puedes proceder con la lógica de tu código sabiendo que el compilador ya se ha asegurado de que tengas un valor válido. Por ejemplo, si tienes un tipo en lugar de un Option, tu programa espera tener algo en lugar de nada. Tu código entonces no tiene que manejar dos casos para las variantes Some y None: solo tendrá un caso para tener definitivamente un valor. El código que intenta pasar nada a tu función ni siquiera se compilará, por lo que tu función no tiene que verificar ese caso en tiempo de ejecución. Otro ejemplo es usar un tipo de entero sin signo como u32, que garantiza que el parámetro nunca sea negativo.

Creacion de tipos personalizados para validacion

Tomemos la idea de usar el sistema de tipos de Rust para garantizar que tengamos un valor válido un paso más allá y veamos cómo crear un tipo personalizado para validación. Recuerda el juego de adivinanzas en el Capítulo 2 en el que nuestro código le pidió al usuario que adivinara un número entre 1 y 100. Nunca validamos que la suposición del usuario estuviera entre esos números antes de verificarla con nuestro número secreto; solo validamos que la suposición fuera positiva. En este caso, las consecuencias no fueron muy graves: nuestra salida de “Demasiado alto” o “Demasiado bajo” seguiría siendo correcta. Pero sería una mejora útil guiar al usuario hacia suposiciones válidas y tener un comportamiento diferente cuando un usuario adivina un número que está fuera del rango en comparación con cuando un usuario escribe, por ejemplo, letras en su lugar.

Una forma de hacer esto sería analizar la suposición como un i32 en lugar de solo un u32 para permitir números potencialmente negativos, y luego agregar una verificación de que el número esté en el rango, de esta manera:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        // --snip--

        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        if guess < 1 || guess > 100 {
            println!("The secret number will be between 1 and 100.");
            continue;
        }

        match guess.cmp(&secret_number) {
            // --snip--
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

La expresión if verifica si nuestro valor está fuera del rango, le dice al usuario sobre el problema y llama a continue para iniciar la siguiente iteración del ciclo y pedir otra suposición. Después de la expresión if, podemos continuar con las comparaciones entre guess y el número secreto sabiendo que guess está entre 1 y 100.

Sin embargo, esta no es una solución ideal: si fuera absolutamente crítico que el programa solo operara en valores entre 1 y 100, y tuviera muchas funciones con este requisito, tener una verificación como esa en cada función sería tedioso (y podría afectar el rendimiento).

En su lugar, podemos crear un nuevo tipo y poner las verificaciones en una función para crear una instancia del tipo en lugar de repetir las verificaciones en cada función. De esa manera, es seguro que las funciones utilicen el nuevo tipo en sus firmas y utilicen los valores que reciben con confianza. El Listado 9-13 muestra una forma de definir un tipo Guess que solo creará una instancia de Guess si la función new recibe un valor entre 1 y 100.

#![allow(unused)]
fn main() {
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}
}

Listing 9-13: Un tipo Guess que solo continuará con valores entre 1 y 100

Primero, definimos un struct llamado Guess que tiene un campo llamado value que contiene un i32. Aquí es donde se almacenará el número.

Luego implementamos una función asociada llamada new en Guess que crea instancias de valores Guess. La función new está definida para tener un parámetro llamado value de tipo i32 y para devolver un Guess. El código en el cuerpo de la función new prueba value para asegurarse de que esté entre 1 y 100. Si value no pasa esta prueba, hacemos una llamada panic!, que alertará al programador que está escribiendo el código de llamada que tiene un error que debe corregir, porque crear un Guess con un value fuera de este rango violaría el contrato en el que Guess::new se basa. Las condiciones en las que Guess::new podría entrar en pánico deben discutirse en la documentación de la API de cara al público; cubriremos las convenciones de documentación que indican la posibilidad de un panic! en la documentación de la API que creas en el Capítulo 14. Si value pasa la prueba, creamos un nuevo Guess con su campo value establecido en el value y devolvemos el Guess.

A continuación, implementamos un método llamado value que toma prestado self, no tiene otros parámetros y devuelve un i32. Este tipo de método se llama a veces getter, porque su propósito es obtener algunos datos de sus campos y devolverlos. Este método público es necesario porque el campo value del struct Guess es privado. Es importante que el campo value sea privado para que el código que usa el struct Guess no pueda establecer value directamente: el código fuera del módulo debe usar la función Guess::new para crear una instancia de Guess, lo que garantiza que no hay forma de que un Guess tenga un value que no haya sido verificado por las condiciones en la función Guess::new.

Una función que tiene un parámetro o devuelve solo números entre 1 y 100 podría entonces declarar en su firma que toma o devuelve un Guess en lugar de un i32 y no necesitaría hacer ninguna verificación adicional en su cuerpo.

Resumen

Las características de manejo de errores de Rust están diseñadas para ayudarte a escribir un código más robusto. La macro panic! indica que tu programa está en un estado que no puede manejar y te permite indicarle al proceso que se detenga en lugar de intentar continuar con valores no válidos o incorrectos. El enum Result usa el sistema de tipos de Rust para indicar que las operaciones pueden fallar de una manera que tu código podría recuperar. Puedes usar Result para decirle al código que llama a tu código que necesita manejar el éxito o el error de manera potencial. Usar panic! y Result en las situaciones apropiadas hará que tu código sea más confiable ante los problemas inevitables.

Ahora que has visto formas útiles en que la biblioteca estándar usa generics con los enums Option y Result, hablaremos sobre cómo funcionan los generics y cómo puedes usarlos en tu código.