Aprende Rust

Lección 12 de 26 Structs, enums y patrones

Option: adiós a null

  • 4 min de lectura
  • 4 ejercicios
  1. En Rust no existe null. Si un valor puede faltar, se usa Option<T>: una caja que puede venir llena o vacía.
  2. Some(5): la caja trae un valor dentro. Aquí numero es un Option<i32> que contiene un 5.
  3. None: la caja está vacía. Y un Option<i32> no es un i32: no puedes usarlo sin abrir la caja.
  4. Al abrirla decides qué hacer en cada caso, con match, if let o unwrap_or(0), que da un valor por defecto.
1/4

En 1965, Tony Hoare inventó la referencia nula, el famoso null. Años después la llamó su «error del billón de dólares» (billion-dollar mistake), por todos los fallos, cuelgues y agujeros de seguridad que ha provocado desde entonces. Si has programado en JavaScript, Java o C, conoces el problema: cualquier variable puede valer null y, si te olvidas de comprobarlo, el programa explota en ejecución.

Rust no tiene null. Cuando un valor puede no existir, lo dice su tipo: Option<T>. Y el compilador no te deja usar ese valor sin comprobar antes si hay algo dentro.

Some y None

Option es un enum de la biblioteca estándar con dos variantes:

enum Option<T> {
    None,
    Some(T),
}
  • Some(valor): hay un valor, y va dentro.
  • None: no hay nada.

La T significa «cualquier tipo»: un Option<i32> puede contener un i32, un Option<String> un String, etc. Verás estos tipos genéricos a fondo más adelante. Option se usa tanto que está siempre disponible: escribes Some y None directamente, sin importar nada ni poner Option:: delante.

fn main() {
    let numero = Some(5);
    let letra = Some('ñ');
    let nada: Option<i32> = None;

    println!("{numero:?} {letra:?} {nada:?}");
}

Con None tienes que indicar el tipo, porque Rust no puede adivinar qué clase de valor falta.

Un Option<i32> no es un i32

Son tipos distintos. No puedes sumarlos ni usar uno donde se espera el otro:

fn main() {
    let x: i32 = 5;
    let y: Option<i32> = Some(3);
    let suma = x + y;
    println!("{suma}");
}

Parece incómodo, pero es justo la gracia: para llegar al i32 tienes que sacarlo del Option, y para eso tienes que decidir qué hacer si es None. El despiste que en otros lenguajes provoca el error, en Rust ni siquiera compila.

Funciones que devuelven Option

Cuando una función puede no tener un resultado válido, devuelve un Option. Y quien la llama usa match para tratar los dos casos:

fn dividir(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

fn main() {
    match dividir(10, 2) {
        Some(resultado) => println!("10 / 2 = {resultado}"),
        None => println!("No se puede dividir entre cero"),
    }

    match dividir(7, 0) {
        Some(resultado) => println!("7 / 0 = {resultado}"),
        None => println!("No se puede dividir entre cero"),
    }
}

La biblioteca estándar usa Option en muchísimos sitios. Por ejemplo, find busca un carácter en un texto y devuelve su posición… si lo encuentra:

fn main() {
    let frase = "hola mundo";
    println!("{:?}", frase.find('m')); // Some(5)
    println!("{:?}", frase.find('z')); // None
}

if let Some(x)

Si solo te interesa el caso en que hay valor, if let es más corto que un match:

fn main() {
    let frase = "hola mundo";

    if let Some(posicion) = frase.find(' ') {
        println!("El primer espacio está en la posición {posicion}");
    }
}

Métodos útiles

Option trae métodos para los casos más habituales sin tener que escribir un match cada vez.

Con unwrap_or das un valor por defecto para cuando no hay nada. unwrap_or_default usa el valor por defecto del tipo: 0 para números, "" para String, false para bool:

fn main() {
    let apodo: Option<&str> = None;
    println!("Hola, {}", apodo.unwrap_or("invitado"));

    let puntos: Option<u32> = None;
    println!("Puntos: {}", puntos.unwrap_or_default());
}

Con map transformas el valor de dentro, si lo hay. Un None se queda como None:

fn main() {
    let precio = Some(20);
    let con_iva = precio.map(|p| p * 121 / 100);
    println!("{con_iva:?}");

    let sin_precio: Option<i32> = None;
    println!("{:?}", sin_precio.map(|p| p * 121 / 100));
}

|p| p * 121 / 100 es una closure, una función anónima: recibe p y devuelve p * 121 / 100. Las verás a fondo más adelante; por ahora basta con saber leerlas.

Por último, is_some() e is_none() responden con un bool si hay valor o no, sin sacarlo.

MétodoSi es Some(x)Si es None
unwrap_or(v)xv
unwrap_or_default()xEl valor por defecto del tipo
map(f)Some(f(x))None
is_some()truefalse
is_none()falsetrue
unwrap()xPanic
expect("mensaje")xPanic con tu mensaje

unwrap y expect: úsalos con cuidado

unwrap() saca el valor de un Some, pero si es None el programa se detiene con un panic. expect hace lo mismo y te deja elegir el mensaje de error:

fn main() {
    let vacio: Option<i32> = None;
    let valor = vacio.expect("debería haber un valor");
    println!("{valor}");
}

Son útiles en pruebas rápidas o cuando estás seguro de que hay un valor (y entonces mejor expect, con un mensaje que explique por qué). En el resto de casos, prefiere match, if let o unwrap_or.

Ojo: Un unwrap() sobre un None es exactamente el error de null que Rust quería evitar, solo que con otro nombre. Si escribes muchos, es una señal de que estás esquivando al compilador en lugar de aprovecharlo.

El operador ? con Option

Dentro de una función que devuelve Option, el operador ? saca el valor de un Some o, si es None, hace que la función devuelva None en ese mismo momento:

fn dividir(a: i32, b: i32) -> Option<i32> {
    if b == 0 { None } else { Some(a / b) }
}

// Divide a entre b, y el resultado entre c
fn dividir_dos_veces(a: i32, b: i32, c: i32) -> Option<i32> {
    let primera = dividir(a, b)?;
    let segunda = dividir(primera, c)?;
    Some(segunda)
}

fn main() {
    println!("{:?}", dividir_dos_veces(100, 5, 2)); // Some(10)
    println!("{:?}", dividir_dos_veces(100, 0, 2)); // None
}

Sin ? necesitarías dos match anidados. Lo verás mucho más en la lección de manejo de errores, donde ? brilla con Result.

Hora de practicar

Ejercicios

0/4 resueltos

Reto 1 de 4

Completa primer_negativo para que devuelva el primer número negativo de la lista dentro de un Some, o None si no hay ninguno.
Salida esperada
Some(-2)
None

Al ejecutar, unos tests ocultos comprueban tus funciones.

src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos