El operador ?

Existe una forma más corta de gestionar un valor de tipo Result (y Option). más corta que match y que if let. Es el "operador interrogación" y es ?. Después de una función devuelve un resultado, se puede añadir ? para:

  • devolver el valor contenido en Ok, si es el tipo concreto de Result.
  • o elevar el error si es Err.

En otras palabras, hace casi todo por ti.

Por ejemplo, se puede probar esto con .parse(). En el siguiente ejemplo se presenta la función parse_str que intenta convertir un &str a i32:

use std::num::ParseIntError;

fn parse_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<i32>()?; // Aquí está el ?
    Ok(parsed_number)
}

fn main() {}

Esta función toma un &str. Si es Ok, devuelve un i32 envuelto en un Ok. Si es un Err, devuelve ParseIntError. Al intentar obtener el número y haberse añadido ? lo que se hace es comprobar si esta llamada a parse ha devuelto Ok y obtener el contenido de este. Si no es correcto, devolverá un error y finalizará en la linea de parse. Pero si es correcto avanza hasta la línea siguiente para devolver Ok(i32). Es necesario envolver el número para que sea compatible con el valor de retorno de la función Result<i32, ParseIntError>, no i32.

Ahora, se puede probar a ejecutar la función. Se observa a continuación lo que sucede con un vector de &str.

fn parse_str(input: &str) -> Result<i32, std::num::ParseIntError> {
    let parsed_number = input.parse::<i32>()?;
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Siete", "8", "9.0", "bien", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{:?}", parsed);
    }
}

Que imprime:

Err(ParseIntError { kind: InvalidDigit })
Ok(8)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Ok(6060)

¿Cómo se puede conocer que el error de esta función parse era std::num::ParseIntError? Una forma fácil es "pedírselo" al compilador:

fn main() {
    let failure = "No es un número".parse::<i32>();
    failure.rbrbrb(); // ⚠️ Compilador: "¿Qué es rbrbrb()???"
}

El compilador se "queja" con:

error[E0599]: no method named `rbrbrb` found for enum `std::result::Result<i32, std::num::ParseIntError>` in the current scope
 --> src\main.rs:3:13
  |
3 |     failure.rbrbrb();
  |             ^^^^^^ method not found in `std::result::Result<i32, std::num::ParseIntError>`

Así se deduce que la definición de tipo que devuelve parse es std::result::Result<i32, std::num::ParseIntError>.

No se necesita escribir std::result::Result porque Result está siempre visible en el alcance (alcance = listo para su uso). Rust hace esto para todos los tipos que se utilizan mucho para que no sea necesario escribir o usar std::result::Result, std::collections::Vec, etc.

Aún no se han presentado recursos como los ficheros, por lo que el operador ? no parece aún muy útil. El siguiente ejemplo muestra cómo se puede ilustrar este uso en una sola línea. En lugar de construir un i32 con .parse(), se hace mucho más. Se construye un u16, luego se convierte a String, después se convierte a u32 y de nuevo se pasa a String para, finalmente, convertirlo en un i32.

use std::num::ParseIntError;

fn parse_str(input: &str) -> Result<i32, ParseIntError> {
    let parsed_number = input.parse::<u16>()?.to_string().parse::<u32>()?.to_string().parse::<i32>()?; // Se añade ? a cada función para validar la corrección y pasar el valor
    Ok(parsed_number)
}

fn main() {
    let str_vec = vec!["Siete", "8", "9.0", "bien", "6060"];
    for item in str_vec {
        let parsed = parse_str(item);
        println!("{:?}", parsed);
    }
}

Esto imprime lo mismo, pero después de haber procesado tres Result en una sola línea. Después, se mostrará un ejemplo parecido con ficheros, puesto que las funciones asociadas siempre devuelven Result ya que pueden ir mal muchas cosas cuando se accede a ellos.

Se puede imaginar lo siguiente: se quiere abrir un fichero, escribir y cerrarlo. Lo primero es encontrar el fichero con éxito (eso es un Result); después, escribir en él sin fallos (esto es otro Result). Con ? se puede hacer todo en una sola línea.

Cuando panic y unwrap son buenos

Rust dispone de una macro panic! que se puede utilizar para que "entre en pánico".

fn main() {
    panic!("¡hora de entrar en pánico!");
}

Cuando se ejecuta este programa se muestra el siguiente mensaje: thread 'main' panicked at '¡hora de entrar en pánico!', src/main.rs:2:5.

Se muestra el programa, la línea de código y columna 2:5 en el que se "entró en pánico". Con esta información, se puede buscar la línea de código y arreglarla.

panic! tiene utilidad para asegurar que algo no cambia. Por ejemplo, la siguiente función imprime_tres_cosas siempre imprime los índices [0], [1] y [2] de un vector. Esto está bien siempre que reciba un vector con tres elementos.

fn imprime_tres_cosas(vector: Vec<i32>) {
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10];
    imprime_tres_cosas(my_vec);
}

Esto imprime 8, 9 y 10 correctamente.

Si posteriormente se escribe más código y se olvida que my_vec tiene que ser siempre de tres elementos, se puede acabar un vector de seis elementos como en el caso siguiente:

fn imprime_tres_cosas(vector: Vec<i32>) {
  println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
  let my_vec = vec![8, 9, 10, 10, 55, 99]; // Ahora tiene seis elementos
  imprime_tres_cosas(my_vec);
}

Con este ejemplo, parece que todo está bien, no hay error. Pero si es importante que el vector solo tenga tres cosas, no se detectaría el fallo. Se podría hacer lo siguiente:

fn imprime_tres_cosas(vector: Vec<i32>) {
    if vector.len() != 3 {
        panic!("my_vec siempre tiene que tener tres elementos") // entrará en pánico si tiene longitud 3
    }
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10];
    imprime_tres_cosas(my_vec);
}

Ahora, si el vector tuviera seis elementos, el programa fallaría (N.T. a las condiciones que se deben cumplir siempre en una función -en sus parámetros o valores de retorno o durante su ejecución- se le denomina "invariante". Rust proporciona macros específicas para ello assert! y assert_debug!):

    // ⚠️
fn imprime_tres_cosas(vector: Vec<i32>) {
    if vector.len() != 3 {
        panic!("my_vec siempre tiene que tener tres")
    }
    println!("{}, {}, {}", vector[0], vector[1], vector[2]);
}

fn main() {
    let my_vec = vec![8, 9, 10, 10, 55, 99];
    imprime_tres_cosas(my_vec);
}

Este programa devuelve thread 'main' panicked at 'my_vec siempre tiene que tener tres', src/main.rs:4:9. Gracias a panic!, se alerta de que solo debería tener tres elementos el vector my_vec. Es una macro útil para meter recordatorios en el código.

Existen otras tres macros que son similares y que se utilizan mucho durante las pruebas. Son: assert!, assert_eq! y assert_ne!. Esto es lo que hacen:

  • assert!(): el programa entra en pánico cuando la evaluación del contenido entre () no es cierta.
  • assert_eq!(): el programa entra en pánico cuando los dos elementos contenidos entre () no son iguales.
  • assert_ne!(): el programa entra en pánico cuando los dos elementos contenidos entre () son iguales.

Algunos ejemplos son:

fn main() {
    let my_name = "Loki Laufeyson";

    assert!(my_name == "Loki Laufeyson");
    assert_eq!(my_name, "Loki Laufeyson");
    assert_ne!(my_name, "Mithridates");
}

El código anterior funciona sin problemas ya que en este caso todas las macros comprueban con éxito los valores que reciben como parámetro.

También es posible añadir un mensaje a cada macro:

fn main() {
    let my_name = "Loki Laufeyson";

    assert!(
        my_name == "Loki Laufeyson",
        "{} should be Loki Laufeyson",
        my_name
    );
    assert_eq!(
        my_name, "Loki Laufeyson",
        "{} and Loki Laufeyson should be equal",
        my_name
    );
    assert_ne!(
        my_name, "Mithridates",
        "You entered {}. Input must not equal Mithridates",
        my_name
    );
}

Estos mensajes se mostrarán solamente si el programa entra en pánico (no se cumple la condición que establece la macro). Por eso, si se ejecuta lo siguiente:

fn main() {
    let my_name = "Mithridates";

    assert_ne!(
        my_name, "Mithridates",
        "You enter {}. Input must not equal Mithridates",
        my_name
    );
}

Se mostrará:

thread 'main' panicked at 'assertion failed: `(left != right)`
  left: `"Mithridates"`,
 right: `"Mithridates"`: You entered Mithridates. Input must not equal Mithridates', src\main.rs:4:5

Esta respuesta está diciendo que se esperaba que los valores fuesen distintos left != right, pero se han recibido valores iguales.

También es útil el uso de unwrap cuando se escribe un programa y se quiere que falle (entre en pánico) cuando haya un problema. Posteriormente, cuando el código esté completo, conviene eliminar unwrap por una alternativa más robusta que impida que el programa se pare.

También se puede usar expect, que es como unwrap, pero permite que se le pase un mensaje que se mostrará en el momento del error. Los libros de texto suelen tener el siguiente aviso: "si se utiliza unwrap() mucho, al menos se debería usar expect() para tener mensajes de error adecuados.

Esto fallará:

   // ⚠️
fn get_cuatro(input: &Vec<i32>) -> i32 {
    let cuatro = input.get(3).unwrap();
    *cuatro
}

fn main() {
    let my_vec = vec![9, 0, 10];
    let cuatro = get_cuatro(&my_vec);
}

El mensaje de error es: thread 'main' panicked at 'called Option::unwrap() on a None value', src\main.rs:7:18.

   // ⚠️
fn get_cuatro(input: &Vec<i32>) -> i32 {
    let cuatro = input.get(3).expect("Input vector needs at least 4 items");
    *cuatro
}

fn main() {
    let my_vec = vec![9, 0, 10];
    let cuatro = get_cuatro(&my_vec);
}

Vuelve a fallar, pero el mensaje de error es mejor: thread 'main' panicked at 'Input vector needs at least 4 items', src\main.rs:7:18. .expect() es un poco mejor que .unwrap(), pero sigue fallando con None. A continuación se muestra un ejemplo de una mala práctica una función que intenta unwrap dos veces. Toma como parámetros un Vec<Option<i32>>, por lo que puede que cada parte contenta un Some<i32> o un None.

fn intenta_dos_unwraps(input: Vec<Option<i32>>) {
    println!("Índice 0 es: {}", input[0].unwrap());
    println!("Índice 1 es: {}", input[1].unwrap());
}

fn main() {
    let vector = vec![None, Some(1000)]; // Este vector tiene un None, por lo que entrará en pánico
    intenta_dos_unwraps(vector);
}

El mensaje es "thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:2:42. No se puede saber en qué unwrap sucedió hasta revisar la línea. Sería mejor valorar la longitud y no unwrap. Pero con .expect() al menos será un poco mejor:

fn intenta_dos_unwrap(input: Vec<Option<i32>>) {
    println!("Index 0 is: {}", input[0].expect("¡El primer unwrap contenía un None!"));
    println!("Index 1 is: {}", input[1].expect("¡El segundo unwrap contenía un None!"));
}

fn main() {
    let vector = vec![None, Some(1000)];
    intenta_dos_unwrap(vector);
}

Ahora el resultado es algo mejor: thread 'main' panicked at '¡El primer unwrap contenía un None!', src/main.rs:2:41. Y se dispone también de la línea en la que ha sucedido para encontrarlo.

También se puede utilizar unwrap_or si siempre existe un valor a usar por defecto cuando se devuelve None. Esta función no entra en pánico. Esto es:

  1. Bueno, porque el programa no entra en pánico, pero...
  2. no tan bueno si se quiere detectar un problema en el código.

En todo caso, lo habitual es que no se quiera que el programa entre en pánico, por lo que unwrap_or es muy útil.

fn main() {
    let my_vec = vec![8, 9, 10];

    let cuatro = my_vec.get(3).unwrap_or(&0); // Si .get no funciona, 
    // se toma por defecto el &0.
    // .get devuelve una referencia, por eso se necesita &0

    println!("{}", cuatro);
}

Esto imprime 0 porque .unwrap_or(&0) devuelve 0 cuando el Option es un None.