13  Coincidencia de patrones

En muchos programas es necesario ejecutar acciones diferentes según el valor de una variable. En otros lenguajes esto suele resolverse mediante cadenas de instrucciones if o con construcciones como switch.

Rust ofrece una herramienta mucho más potente: la expresión match. Además de comparar valores concretos, match puede reconocer la forma de un dato (pattern matching o coincidencia de patrones), extraer la información que contiene y obligar al programador a contemplar todos los casos posibles.

Aunque match puede utilizarse con tipos sencillos, como enteros o caracteres, resulta especialmente útil al trabajar con enum, ya que permite distinguir cada una de sus variantes de forma clara y segura.

En este capítulo aprenderemos a utilizar match, veremos cómo devolver valores mediante esta expresión, cómo emplear patrones comodín y cómo extraer los datos asociados a una variante. Finalmente estudiaremos las formas abreviadas if let y while let, que simplifican algunos usos muy frecuentes.

13.1 Definición de match

La expresión match permite seleccionar entre varias alternativas según el valor de una expresión. Su funcionamiento recuerda al de la instrucción switch presente en otros lenguajes, aunque es mucho más potente gracias al uso de patrones.

La expresión match comienza con la palabra reservada match, seguida de una expresión y de un bloque entre llaves que contiene una serie de ramas (arms). Cada rama está formada por un patrón, el operador =>, la expresión correspondiente y una coma. Excepto la última rama donde la coma es opcional.

El operador => (fat arrow) no debe confundirse con el operador -> (arrow), utilizado, por ejemplo, para indicar el tipo devuelto por una función.

match expresión {
    patrón1 => expresión1,
    patrón2 => expresión2,
    ...
}

Al ejecutarse, en primer lugar se evalúa la expresión situada tras la palabra reservada match. A continuación, su valor se compara con cada uno de los patrones, en el orden en que aparecen. Cuando uno de ellos coincide, se evalúa la expresión correspondiente y el match finaliza.

El siguiente ejemplo identifica algunos de los puertos bien conocidos (well-known ports) más habituales en las aplicaciones de red. Como el valor de puerto es 443, se ejecuta únicamente la rama correspondiente al protocolo HTTPS.

fn main() {
    let puerto = 443;

    match puerto {
        22 => println!("SSH"),
        25 => println!("SMTP"),
        53 => println!("DNS"),
        80 => println!("HTTP"),
        443 => println!("HTTPS"),
        _ => println!("Servicio desconocido"),
    }
}

En este ejemplo aparece el patrón especial _, denominado patrón comodín (wildcard pattern). Este patrón coincide con cualquier valor que no haya coincidido con ninguna de las ramas anteriores. Su uso resulta muy habitual para proporcionar un caso por defecto, equivalente al default de otros lenguajes.

_ => println!("Servicio desconocido"),

Además, Rust exige que un match contemple todos los valores posibles de la expresión que se está evaluando. Esta propiedad se conoce como exhaustividad. Si falta alguna posibilidad y no existe un patrón comodín que la cubra, el programa no compilará. Observa que borrando la rama correspondiente al comodín, el programa no compila.

13.2 match con el enumerado Option<T>

Uno de los usos más frecuentes de match consiste en procesar valores de tipos enumerados como por ejemplo Option<T>. Como vimos en la Sección 12.4, un Option puede contener un valor (Some) o no contener ninguno (None). El siguiente programa muestra cómo tratar ambos casos:

fn main() {
    let numero = Some(10);

    match numero {
        Some(n) => println!("Valor: {}", n),
        None => println!("No hay ningún valor"),
    }
}
  • Si la variable numero contiene un valor, se ejecuta la primera rama del match. La variable n recibe el valor almacenado en Some y puede utilizarse libremente dentro de esa rama.

  • Si, por el contrario, numero contiene None, se ejecuta la segunda rama.

Este patrón aparece con mucha frecuencia en Rust, ya que numerosas funciones y métodos devuelven un Option para indicar que un resultado puede existir o no. En capítulos posteriores veremos varios ejemplos de ello, como el método Vec::get.

13.3 match como expresión

Como vimos en la Sección 8.1.4 y en la Sección 8.2.1, if y loop son expresiones. match también lo es. Esto significa que produce un valor, por lo que puede utilizarse en cualquier contexto donde se espere una expresión.

Es habitual emplearlo para inicializar una variable, como en el siguiente ejemplo. Observarmos que la expresión match devuelve una cadena distinta según el valor de puerto, que se asigna a la variable servicio.

fn main() {
    let puerto = 443;

    let servicio = match puerto {
        22 => "SSH",
        25 => "SMTP",
        53 => "DNS",
        80 => "HTTP",
        443 => "HTTPS",
        _ => "Desconocido",
    };

    println!("Servicio: {}", servicio);
}

Como cualquier otra expresión en Rust, todas las ramas de un match deben producir valores del mismo tipo. De este modo, el compilador puede determinar el tipo del valor devuelto por la expresión completa.

El ejemplo anterior es correcto porque todas las ramas devuelven una cadena de tipo &str. En cambio, el siguiente código produce un error de compilación, ya que unas ramas devuelven una cadena y otras un número entero:

fn main() {
    let puerto = 443;

    let servicio = match puerto {
        22 => "SSH",
        23 => 23,
        24 => 23,
        25 => "SMTP",
        53 => "DNS",
        80 => "HTTP",
        443 => "HTTPS",
        _ => "Desconocido",
    };

    println!("Servicio: {}", servicio);
}

13.4 Extracción de datos asociados

En el Capítulo 12 vimos que las variantes de un enum pueden llevar asociados uno o varios datos. Una de las principales ventajas de match es que permite acceder directamente a dichos datos cuando una variante coincide con un patrón.

En el siguiente ejemplo, Mensaje es un enum con tres variantes: una que contiene un valor de tipo String, otra que contiene un valor de tipo i32 y una tercera que no lleva datos asociados.

enum Mensaje {
    Texto(String),
    Numero(i32),
    Salir,
}

fn main() {
    let mensaje = Mensaje::Texto(String::from("Hola"));

    match mensaje {
        Mensaje::Texto(texto) => {
            println!("Texto recibido: {}", texto);
        }
        Mensaje::Numero(n) => {
            println!("Número recibido: {}", n);
        }
        Mensaje::Salir => {
            println!("Fin del programa");
        }
    }
}

La expresión match distingue las tres variantes y extrae los datos asociados cuando existen.

  • En las dos primeras ramas aparecen los patrones Mensaje::Texto(texto) y Mensaje::Numero(n). Cuando la variante coincide, las variables texto y n se crean automáticamente y reciben los datos asociados al enum. Estas variables pueden utilizarse libremente dentro de la expresión correspondiente.

  • La variante Mensaje::Salir no lleva datos asociados, por lo que su patrón se limita al nombre de la variante.

13.5 if let

En ocasiones solo interesa actuar cuando un valor coincide con un determinado patrón, ignorando el resto de los casos. Aunque esto puede hacerse con match, es necesario añadir una rama comodín para contemplar las demás posibilidades.

Por ejemplo, el siguiente código muestra el contenido del mensaje solo cuando este es de tipo Texto:

enum Mensaje {
    Texto(String),
    Numero(i32),
    Salir,
}

fn main() {
    let mensaje = Mensaje::Numero(42);

    match mensaje {
        Mensaje::Texto(texto) => {
            println!("Texto recibido: {}", texto);
        }
        _ => {}
    }
}

Como este uso es muy frecuente, Rust proporciona la construcción if let, que equivale al match anterior:

enum Mensaje {
    Texto(String),
    Numero(i32),
    Salir,
}

fn main() {
    let mensaje = Mensaje::Numero(42);

    if let Mensaje::Texto(texto) = mensaje {
        println!("Texto recibido: {}", texto);
    }
}

La sintaxis de if let recuerda a una asignación, pero el operador = no asigna ningún valor. Su función es comprobar si la expresión situada a la derecha coincide con el patrón situado a la izquierda.

  • Si la coincidencia tiene éxito, las variables que aparecen en el patrón quedan inicializadas con los datos extraídos de la expresión.

  • Cuando la expresión no coincide con el patrón, el bloque no se ejecuta y la ejecución continúa con la instrucción siguiente.

Al igual que un if convencional, if let puede ir seguido de una cláusula else, que se ejecutará cuando la expresión no coincida con el patrón: en muchos casos, if let con else constituye una alternativa más legible a un match cuando solo interesa tratar explícitamente una de las variantes.

if let Mensaje::Texto(texto) = mensaje {
    println!("Texto recibido: {}", texto);
} else {
    println!("El mensaje no contiene texto");
}

13.6 while let

La construcción while let combina un bucle while con una comprobación mediante patrones. Mientras la expresión coincida con el patrón indicado, el cuerpo del bucle se ejecutará repetidamente. Su sintaxis es análoga a la de if let.

fn main() {
    let mut pila = vec![10, 20, 30];

    while let Some(valor) = pila.pop() {
        println!("{valor}");
    }
}

Este ejemplo utiliza conceptos que todavía no conocemos, pero merece la pena observarlo porque representa uno de los usos más habituales de while let en Rust. El método pop() extrae el último elemento del vector, utilizado aquí como una pila, y lo devuelve envuelto en Some. Cuando el vector queda vacío, devuelve None, lo que hace que el bucle termine automáticamente.

Más adelante estudiaremos los vectores y el tipo Option, y retomaremos este ejemplo para ver con precisión cómo funciona.

13.7 Resumen

En este capítulo hemos estudiado la coincidencia de patrones, uno de los mecanismos más potentes y característicos de Rust.

Las ideas principales son las siguientes:

  • La expresión match permite ejecutar un bloque de código distinto para cada posible patrón.
  • Todos los casos deben quedar cubiertos, lo que garantiza la exhaustividad y evita errores durante la compilación.
  • match también es una expresión, por lo que puede devolver un valor.
  • Los patrones pueden utilizarse para descomponer valores complejos, como los almacenados en un enum.
  • Las construcciones if let y while let proporcionan una forma más concisa de trabajar con patrones cuando solo interesa una posibilidad.
  • Estas construcciones son especialmente útiles para manipular valores de tipo Option<T> y Result<T, E>.

La coincidencia de patrones es una de las características que hacen que Rust resulte especialmente expresivo y seguro. A medida que avancemos en el lenguaje, aparecerá con frecuencia como una herramienta fundamental para trabajar con tipos de datos complejos.