Option: la ausencia de valor sin null
Representa presencia y ausencia con Some y None. Distingue un valor presente vacío, contempla ambas variantes con match y reconoce cuándo desenvolver puede producir panic.
Una consulta puede encontrar un valor o terminar sin encontrarlo. Option<T> hace visible esa posibilidad: Some(valor) representa presencia y None representa ausencia. Un valor presente puede estar vacío; esa diferencia depende del contrato de la consulta.
- Leer
SomeyNonecomo las dos variantes deOption<T>. - Distinguir ausencia de un texto presente vacío.
- Usar un
matchpara responder a ambas variantes. - Reconocer que
unwrap()sobreNonepuede producirpanic.
Preparación antes de practicar
Necesitas reconocer funciones, variantes de un enum y ramas de match. Puedes repasarlas en Funciones básicas, Enums con y sin datos y Match exhaustivo.
Para practicar con Option<String>, también necesitas distinguir el texto propio String de una vista prestada &str. Revisa Ownership paso a paso y Prestar en vez de mover, aunque aparezcan después en el recorrido. Un parámetro &str permite consultar el texto sin consumir el String del llamador; String::from(vista) construye un texto propio a partir de esa vista. Ese resultado puede seguir disponible cuando termina el préstamo de la entrada.
Aquí basta con ese recorte. No necesitas escribir lifetimes explícitos, estudiar la representación en memoria ni aprender Result para comenzar.
Referencias válidas y ausencia explícita
Una referencia válida &T no puede ser nula: debe dar acceso a un valor válido de su tipo. Si ese acceso puede faltar, Option<&T> permite representar la posibilidad en el tipo. Rust también tiene punteros crudos que pueden ser nulos; no debes confundirlos con referencias válidas.
Option tampoco es la única forma de representar todos los estados de un programa. Se usa cuando el contrato necesita distinguir un valor presente de uno ausente. El compilador impide tratar Option<T> directamente como T, pero no garantiza por sí solo que toda decisión sobre la ausencia sea adecuada.
Option: la ausencia como un enum más
Aquí no hay magia. Option<T> es exactamente el tipo suma que construiste en 6.1, con dos variantes y genérico sobre T:
enum Option<T> { None, Some(T),}Some(valor) envuelve un valor presente; None es la ausencia. Ambas están en el preludio, así que las usas sin importar nada. Una función que quizá no tenga respuesta lo declara en su tipo de retorno:
fn primer_par(v: &[i32]) -> Option<i32> { for &x in v { if x % 2 == 0 { return Some(x); // encontrado } } None // recorrimos todo sin éxito}let a = primer_par(&[1, 3, 4, 7]); // Some(4)let b = primer_par(&[1, 3, 5]); // NoneY leerlo es aplicar lo de 6.2 sin ninguna regla nueva: un match que contempla, obligatoriamente, las dos variantes.
match primer_par(&[1, 3, 4, 7]) { Some(par) => println!("el primer par es {par}"), None => println!("no había ningún par"),}La posibilidad de ausencia aparece en la firma. Métodos como slice::first, HashMap::get, str::split_once e Iterator::next devuelven Option; el significado concreto de None lo define cada operación.
Presente vacío no es ausente
Imagina una consulta a un campo escrito como nombre=valor. El contrato dice que encontrar el primer = significa que hay valor, aunque después no haya texto:
| Entrada | Resultado esperado |
|---|---|
alias |
None: no se encontró el separador. |
alias= |
Some(String::new()): se encontró un valor vacío. |
alias=Ana |
Some(String::from("Ana")): se encontró ese texto. |
Some(String::new()) contiene un String válido de longitud cero. None no contiene ningún String. No decidas la presencia preguntando si el texto está vacío: primero aplica el criterio que establece el contrato.
split_once('=') devuelve Some con una pareja de vistas si encuentra el primer separador, y None si no lo encuentra. Puedes contemplar ambas variantes con match:
fn main() { let entrada = "alias="; match entrada.split_once('=') { Some((nombre, valor)) => { let propio = String::from(valor); println!("campo {nombre}: valor presente {propio:?}"); } None => println!("no se encontró un valor"), }}En Some((nombre, valor)), el patrón abre la variante y da nombre a las dos partes de la pareja. Con alias= se ejecuta esa rama y propio queda vacío; la rama None corresponde a una entrada sin separador. Prueba también alias=Ana=otra: el valor conserva todo lo que sigue al primer =.
El compilador no te deja leer el vacío
Esta es la diferencia que lo cambia todo. Option<i32> no es i32; son tipos distintos, y no puedes tratar uno como el otro:
let n: Option<i32> = primer_par(&[1, 3, 4]);// let doble = n * 2;error[E0369]: cannot multiply `Option<i32>` by `{integer}`Un match sobre Option debe cubrir sus variantes, sea con ramas explícitas o con un patrón que cubra las restantes. Así puedes decidir qué hacer cuando falta el valor. También existen if let, let ... else y los métodos que verás en 7.2; no todos obligan a escribir una rama None de la misma forma.
Por ejemplo, unwrap() devuelve el contenido de Some, pero produce panic si recibe None. Esta función compila; llamarla con None falla en ejecución:
fn extraer_sin_comprobar(valor: Option<i32>) -> i32 { valor.unwrap()}Para una ausencia esperada, como no encontrar el separador de un campo, contempla el caso con match. Tener un Option hace visible la posibilidad de ausencia; usar unwrap() no la elimina.
Preguntar sin desenvolver: is_some, is_none, matches!
A veces solo quieres saber si hay valor, sin sacarlo. Para eso están opt.is_some() y opt.is_none(), que devuelven un bool, y la macro matches!(opt, Some(x) if x > 0), que comprueba forma y condición en una sola expresión. Son atajos de lectura: para usar el valor sigues teniendo que abrir el Option (7.2), pero para un assert! o una guarda rápida son lo idiomático.
Qué significa None depende del contrato
Option distingue presencia y ausencia, sin transportar un motivo para None. Que first() sobre un slice vacío dé None, o que buscar una clave ausente en un HashMap dé None, puede ser una respuesta normal y esperada. En otras APIs, None puede señalar que una operación no se pudo realizar: consulta siempre su contrato.
use std::collections::HashMap;let edades = HashMap::from([("ana", 30)]);let ana: Option<&i32> = edades.get("ana"); // Some(&30)let juan: Option<&i32> = edades.get("juan"); // None: ausencia normal, no un falloCuando la ausencia sí necesita justificarse —por qué no se pudo abrir el fichero, qué campo del formulario estaba mal— None se queda corto, porque no puede transportar el motivo. Ese es el territorio de Result<T, E>, que llega en 7.3: el mismo patrón de “un valor envuelto en un enum”, pero con un canal para el error. Para esta práctica basta con la presencia o ausencia. Más adelante estudiarás cómo Result permite transportar información de error.
Ampliación: representación en memoria
Para ciertos tipos, Rust garantiza una representación compacta. Por ejemplo, Option<&i32> tiene el mismo tamaño que &i32: el patrón nulo, que no representa una referencia válida, se utiliza para None. Es un caso de optimización de nicho.
Esa propiedad no se extiende a todo Option<T> ni elimina el coste del trabajo que hagas en cada rama. Construir un String propio a partir de un texto prestado puede reservar memoria y copia su contenido. La representación no es un prerrequisito para distinguir Some de None.
Haz visible la ausencia
- Predice qué variante produce
split_once('=')paraalias,alias=,=Anay la entrada vacía. Explica por quéalias=no es ausencia. - Ejecuta el ejemplo con esas entradas y comprueba qué rama de
matchse usa. Conserva los espacios y separadores posteriores al primero. - Cambia
let entrada = "alias=";por unStringdel llamador y consulta mediante una vista. Construye unStringpropio en la rama presente; comprueba que puedes consultar el texto original después. - Intenta multiplicar un
Option<i32>por dos sin contemplar la variante y lee el error. Después hazlo dentro de unmatch, decidiendo qué devolver cuando no hay número. - Explica qué ocurre al llamar a
extraer_sin_comprobar(None)y por qué no es un error de compilación.
Estas comprobaciones preparan la distinción entre presencia y ausencia con el tipo ya dado. Elegir por tu cuenta el tipo adecuado para otro problema necesita práctica adicional.