12  enum

Este capítulo tratará los enum de Rust, abreviatura de enumerations (enumeraciones). Un enum define un tipo cuyos valores pueden adoptar una variante entre varias posibles, establecidas por el programador. Obsérvese la diferencia con los struct, estudiados en el Capítulo 10, cuyos objetos contienen simultáneamente todos sus campos.

Los enum son un mecanismo muy versátil y aparecen con frecuencia tanto en programas escritos por el usuario como en la propia biblioteca estándar de Rust. En este capítulo estudiaremos cómo definir enum y crear valores de este tipo. En el Capítulo 13 veremos cómo trabajar con ellos mediante match, en la Sección 12.4 estudiaremos el enumerado Option y en el Sección 17.1, el enumerado Result<T, E>.

12.1 Definición

La definición de un enum se compone de:

  • La palabra reservada enum.
  • El nombre del tipo. Como vimos en la Sección 4.2, el convenio establece usar PascalCase.
  • Una lista de variantes encerradas entre llaves. Cada variante identifica uno de los posibles valores que pueden tomar los objetos de ese tipo.
enum EstadoConexion {
    Free,
    Listening,
    Connecting,
    Connected,
    Disconnecting,
}

En este ejemplo, EstadoConexion es un nuevo tipo definido por el programador. Sus únicos valores posibles son Free, Listening, Connecting, Connected y Disconnecting.

12.2 Creación de valores

Una vez definido un enum, pueden crearse objetos de ese tipo mediante el operador de resolución de ámbito (::), indicando el nombre del tipo seguido del nombre de la variante:

let estado1 = EstadoConexion::Free;
let estado2 = EstadoConexion::Listening;
let estado3 = EstadoConexion::Connected;

En todos los casos, el objeto creado pertenece al tipo EstadoConexion; lo único que cambia es la variante elegida.

Obsérvese que el nombre de la variante debe ir precedido por el nombre del enum. Esto evita posibles conflictos cuando distintos enum contienen variantes con el mismo nombre.

12.3 Enumeraciones con datos asociados

A diferencia de las enumeraciones tradicionales de lenguajes como C, C++, Java o C#, las variantes de un enum de Rust no tienen por qué limitarse a representar un valor: también pueden almacenar datos asociados. Cada variante puede definir sus propios tipos de datos, que se especifican entre paréntesis.

El siguiente ejemplo representa distintos eventos que pueden producirse en una aplicación de red:

enum Evento {
    Conexion(String, u16),
    Desconexion,
    Error(String),
}

En este caso, la variante Conexion almacena una dirección IP y un número de puerto, mientras que Error contiene un mensaje de error. La variante Desconexion, por el contrario, no almacena ningún dato adicional.

Los valores se crean proporcionando los datos requeridos por cada variante:

let e1 = Evento::Conexion("192.168.1.10".to_string(), 8080);
let e2 = Evento::Desconexion;
let e3 = Evento::Error("Tiempo de espera agotado".to_string());

Cada una de estas expresiones crea un objeto del tipo Evento, aunque la información almacenada sea distinta en cada caso.

12.4 El enumerado Option

12.4.1 El problema de representar datos inexistentes

En muchos programas es necesario representar la posibilidad de que un dato no exista. Por ejemplo, una aplicación diseñada suponiendo que todas las personas tienen dos apellidos puede mostrar incorrectamente el nombre de un ciudadano de un país donde solo se utiliza uno, presentándolo como John Smith NULL, sustituyendo el segundo apellido por una cadena vacía u obligándole a escribir dos apellidos.

Este tipo de situaciones aparecen con frecuencia: una búsqueda que no encuentra resultados, un usuario que no ha indicado su número de teléfono o un vector al que se intenta acceder con un índice fuera de rango. En todos estos casos, la ausencia de un valor no debería considerarse un error, sino una posibilidad que el programa debe contemplar explícitamente.

Durante décadas, muchos lenguajes de programación han intentado resolver este problema introduciendo un valor especial, como nil, null, undefined o None, para representar la ausencia de un dato. Este enfoque supuso un avance respecto a utilizar cadenas vacías, números arbitrarios u otros valores de relleno.

Sin embargo, estos valores especiales siguen siendo una fuente habitual de errores. El programador debe recordar en todo momento que una variable puede contener un dato válido o un valor nulo, y olvidar comprobar este último caso puede provocar fallos de ejecución o vulnerabilidades de seguridad.

Rust adopta un enfoque diferente, mucho más seguro. En lugar de introducir un valor especial, la posibilidad de que un dato exista o no forma parte de su propio tipo mediante el enumerado Option<T>. De este modo, el compilador obliga al programador a contemplar explícitamente ambos casos.

12.4.2 Uso de Option

Option<T> es uno de los enumerados más utilizados en Rust y está definido en la biblioteca estándar. Empleado para representar la presencia o ausencia de un valor, su definición simplificada es la siguiente:

enum Option<T> {
    Some(T),
    None,
}

Donde T representa el tipo del dato que puede estar contenido en el Option. Por ejemplo, Option<i32> representa un entero que puede existir o no, mientras que Option<String> representa una cadena de texto que también puede estar ausente.

Como cualquier otro enumerado, Option<T> puede tomar un valor entre varios, en este caso dos:

  • Some(valor), cuando existe un valor de tipo T.
  • None, cuando no existe ningún valor.

Por ejemplo:

fn main() {
    let a: Option<i32> = Some(10);
    let b: Option<i32> = None;

    println!("{:?}", a);
    println!("{:?}", b);
}

Esta forma de declarar un Option en realidad no es la más habitual. Lo normal es dejar que el compilador deduzca el tipo cuando el valor es Some(...). Sin embargo, para None es necesario indicar el tipo de forma explícita, precisamente porque no existe ningún valor a partir del cual pueda deducirse.

fn main() {
    let a = Some(10);
    let b: Option<i32> = None;

    println!("{:?}", a);
    println!("{:?}", b);
}

En ambos casos, el tipo de a es exactamente el mismo: Option<i32>. La única diferencia es que, para la variable a, el compilador lo deduce automáticamente.

12.4.3 El método unwrap()

En muchas ocasiones sabemos con certeza que un Option contiene un valor, es decir, que es Some. En esos casos puede utilizarse el método unwrap(), que devuelve el valor contenido en el Option.

fn main() {
    let x = Some(42);
    println!("{:?}", x); // Enumerado completo
    println!("{}", x.unwrap()); // Valor Some

    let y: Option<i32> = None;
    println!("{}", y.unwrap()); // Panic
}

Si unwrap() se aplica sobre un valor None, el programa finaliza inmediatamente produciendo un panic. Por tanto, si existe la posibilidad de que el Option contenga None, es preferible utilizar otras construcciones del lenguaje, como match o if let, que permiten tratar explícitamente ambos casos. Las estudiaremos en el Capítulo 13.

12.4.4 Bloques impl

También es posible asociar métodos y métodos asociados a un enum mediante bloques impl, igual que se hace con los struct. Sin embargo, estos métodos suelen utilizar la construcción match, que estudiaremos en el próximo capítulo.

12.4.5 Variantes heterogéneas

Por último, no todas las variantes de un enum tienen por qué almacenar el mismo número de datos, ni siquiera datos del mismo tipo. Cada variante puede definir libremente la información que lleva asociada.

12.5 Resumen

En este capítulo hemos introducido los enum, un mecanismo que permite definir tipos cuyos valores pueden pertenecer a una de varias variantes posibles.

Las ideas principales son las siguientes:

  • Un enum representa un conjunto de variantes mutuamente excluyentes.
  • Cada variante puede no contener datos o almacenar información adicional de cualquier tipo.
  • Los enum permiten modelar de forma clara situaciones en las que un valor puede encontrarse en distintos estados.
  • Option<T> es un enum de la biblioteca estándar que representa la presencia (Some) o ausencia (None) de un valor.
  • El uso de Option<T> evita recurrir a valores especiales o nulos para indicar que un dato no existe.
  • Cuando se conoce con certeza que un Option contiene un valor, puede obtenerse mediante unwrap(), aunque su uso debe hacerse con precaución.

En el siguiente capítulo veremos cómo trabajar de forma cómoda y segura con los enum mediante el patrón match, que permite seleccionar el código adecuado para cada una de sus variantes.