Rust

Rust

Tutorial Rust: Estructuras de control condicional

Aprende las estructuras de control condicional en Rust con if/else como expresiones y match para un código seguro y expresivo.

Aprende Rust y certifícate

if/else como expresiones

En Rust, una de las características más distintivas es que las estructuras de control como if/else no son simples sentencias que ejecutan bloques de código, sino que son expresiones que pueden devolver valores. Esta característica fundamental diferencia a Rust de muchos otros lenguajes y permite escribir código más conciso y expresivo.

Cuando hablamos de expresiones en Rust, nos referimos a fragmentos de código que producen un valor. A diferencia de las sentencias, que simplemente ejecutan acciones sin devolver resultados. Esta distinción es crucial para entender cómo funcionan los condicionales en Rust.

Estructura básica de if/else como expresiones

La sintaxis básica de un condicional if/else en Rust es similar a la de otros lenguajes:

if condicion {
    // código si la condición es verdadera
} else {
    // código si la condición es falsa
}

Sin embargo, lo que hace especial a Rust es que esta estructura completa puede evaluarse a un valor, permitiendo asignarlo a una variable:

let numero = 5;
let mensaje = if numero > 0 {
    "positivo"
} else {
    "no positivo"
};

println!("El número es {}", mensaje); // Imprime: El número es positivo

En este ejemplo, la variable mensaje recibe el valor que resulta de evaluar la expresión if/else. Observa que no necesitamos usar return ni ninguna otra palabra clave especial.

Reglas importantes para if/else como expresiones

Cuando usamos if/else como expresiones, debemos seguir algunas reglas importantes:

  • Todas las ramas deben devolver valores del mismo tipo. El compilador de Rust verificará esto estrictamente.
  • La última expresión de cada bloque (sin punto y coma) es el valor que devuelve ese bloque.
  • Si una expresión termina con punto y coma, se convierte en una sentencia y devuelve () (unit type, similar a void).

Veamos un ejemplo que ilustra estas reglas:

let numero = 10;
let resultado = if numero > 5 {
    "mayor que cinco"  // Sin punto y coma, devuelve este valor
} else {
    "cinco o menos"    // Sin punto y coma, devuelve este valor
};

// Esto NO compilaría porque los tipos no coinciden:
// let incorrecto = if numero > 5 {
//     42  // Tipo: i32
// } else {
//     "cinco o menos"  // Tipo: &str
// };

Múltiples ramas con else if

Podemos encadenar múltiples condiciones usando else if, y toda la estructura sigue siendo una expresión:

let temperatura = 25;
let descripcion = if temperatura < 0 {
    "congelante"
} else if temperatura < 10 {
    "muy frío"
} else if temperatura < 20 {
    "fresco"
} else if temperatura < 30 {
    "agradable"
} else {
    "caluroso"
};

println!("El clima es {}", descripcion); // Imprime: El clima es agradable

Uso de bloques de código más complejos

Cada rama de un if/else puede contener bloques de código con múltiples expresiones. En este caso, el valor de la última expresión (sin punto y coma) será el valor devuelto por esa rama:

let numero = 42;
let descripcion = if numero > 100 {
    let mitad = numero / 2;
    println!("Es un número grande, su mitad es {}", mitad);
    "número grande"  // Esta es la expresión que se devuelve
} else {
    let doble = numero * 2;
    println!("Es un número pequeño, su doble es {}", doble);
    "número pequeño"  // Esta es la expresión que se devuelve
};

println!("Descripción: {}", descripcion); // Imprime: Descripción: número pequeño

Uso práctico en inicialización condicional

Una aplicación común de if/else como expresiones es la inicialización condicional de variables:

let limite = 100;
let valor_ingresado = 120;

let valor_validado = if valor_ingresado <= limite {
    valor_ingresado
} else {
    limite
};

println!("Valor validado: {}", valor_validado); // Imprime: Valor validado: 100

Este patrón es muy útil para validar entradas o establecer valores predeterminados de manera concisa.

Combinación con operadores lógicos

Podemos combinar condiciones usando operadores lógicos como && (AND) y || (OR):

let edad = 25;
let tiene_licencia = true;

let puede_conducir = if edad >= 18 && tiene_licencia {
    true
} else {
    false
};

println!("¿Puede conducir? {}", puede_conducir); // Imprime: ¿Puede conducir? true

Expresiones if sin else

También podemos usar if sin else como una expresión, pero en este caso, la rama implícita else devuelve () (unit type):

let x = 10;
let resultado = if x > 5 {
    "mayor que cinco"
};

// resultado será de tipo Option<&str>, con valor Some("mayor que cinco")
// Si la condición fuera falsa, resultado sería ()

Sin embargo, este uso es menos común y generalmente se prefiere incluir siempre una rama else cuando se usa if como expresión para asignar valores.

El enfoque de Rust de tratar los condicionales como expresiones hace que el código sea más declarativo y conciso, eliminando la necesidad de construcciones especiales como el operador ternario (?:) presente en otros lenguajes.

Condicionales inline

Los condicionales inline en Rust son una forma concisa de utilizar expresiones if/else en una sola línea de código. Esta técnica aprovecha la naturaleza expresiva de los condicionales en Rust para crear código más compacto y legible en situaciones donde la lógica condicional es simple.

La sintaxis básica de un condicional inline sigue este patrón:

let resultado = if condicion { valor_si_verdadero } else { valor_si_falso };

Esta forma compacta es especialmente útil cuando queremos asignar un valor a una variable basándonos en una condición simple. Veamos algunos ejemplos prácticos:

let edad = 20;
let estado = if edad >= 18 { "adulto" } else { "menor" };
println!("La persona es {}", estado); // Imprime: La persona es adulto

En este ejemplo, la variable estado recibe un valor diferente dependiendo de si la condición edad >= 18 es verdadera o falsa, todo en una sola línea de código.

Reemplazo del operador ternario

A diferencia de lenguajes como C, JavaScript o Java, Rust no tiene un operador ternario (condicion ? valor_si_verdadero : valor_si_falso). Esto es porque los condicionales inline con if/else cumplen exactamente la misma función de manera más coherente con el resto de la sintaxis del lenguaje.

Comparemos cómo se vería el mismo código en JavaScript y en Rust:

JavaScript:

// JavaScript con operador ternario
let puntuacion = 85;
let resultado = puntuacion >= 60 ? "aprobado" : "reprobado";

Rust:

// Rust con condicional inline
let puntuacion = 85;
let resultado = if puntuacion >= 60 { "aprobado" } else { "reprobado" };

La versión de Rust es igualmente concisa pero mantiene la consistencia con el resto de las estructuras del lenguaje.

Uso en expresiones más complejas

Los condicionales inline pueden formar parte de expresiones más complejas, como argumentos de funciones o parte de otras operaciones:

let x = 5;
let y = 10;

println!("El mayor es: {}", if x > y { x } else { y }); // Imprime: El mayor es: 10

let suma = (if x > 0 { x } else { 0 }) + y;
println!("Suma: {}", suma); // Imprime: Suma: 15

En el primer ejemplo, el condicional inline se usa directamente como argumento de la macro println!. En el segundo, forma parte de una expresión aritmética.

Anidación de condicionales inline

Aunque es posible anidar condicionales inline, esto puede reducir la legibilidad rápidamente:

let valor = 42;

// Anidación de condicionales inline (evitar si afecta la legibilidad)
let descripcion = if valor < 0 { 
    "negativo" 
} else if valor == 0 { 
    "cero" 
} else if valor < 10 { 
    "dígito positivo" 
} else { 
    "número positivo grande" 
};

println!("{} es {}", valor, descripcion); // Imprime: 42 es número positivo grande

Cuando la lógica condicional se vuelve más compleja, es mejor usar la estructura de bloques tradicional o considerar el uso de match para mayor claridad.

Uso con operaciones aritméticas

Los condicionales inline son útiles para realizar operaciones aritméticas condicionales:

let temperatura = -5;
let ajuste = if temperatura < 0 { 10 } else { 5 };
let temperatura_ajustada = temperatura + ajuste;

println!("Temperatura ajustada: {}", temperatura_ajustada); // Imprime: Temperatura ajustada: 5

Condicionales inline en asignaciones

Una aplicación común es la asignación condicional de valores dentro de un rango específico:

let entrada_usuario = 150;
let maximo_permitido = 100;

// Limitar un valor a un máximo
let valor_validado = if entrada_usuario <= maximo_permitido { entrada_usuario } else { maximo_permitido };
println!("Valor validado: {}", valor_validado); // Imprime: Valor validado: 100

Mejores prácticas

Al usar condicionales inline, es importante seguir estas recomendaciones:

  • Úsalos solo para lógica condicional simple y clara.
  • Evita anidar demasiados condicionales inline, ya que reduce la legibilidad.
  • Mantén las expresiones dentro de cada rama cortas y concisas.
  • Si la lógica se vuelve compleja, considera usar la forma de bloque tradicional o match.
// Buena práctica: condicional inline simple
let es_par = if numero % 2 == 0 { true } else { false };

// Mejor práctica para casos más complejos: usar bloques
let categoria = if puntuacion < 60 {
    "insuficiente"
} else if puntuacion < 70 {
    "suficiente"
} else if puntuacion < 90 {
    "notable"
} else {
    "sobresaliente"
};

Los condicionales inline son una herramienta poderosa para escribir código Rust más conciso y expresivo cuando se usan apropiadamente. Aprovechan la naturaleza de expresión de los condicionales en Rust para permitir asignaciones y evaluaciones en una forma compacta sin sacrificar la claridad del código.

match fundamentals

La expresión match es una de las características más potentes de Rust para el control de flujo. A diferencia de los condicionales if/else que evalúan expresiones booleanas, match permite comparar un valor contra una serie de patrones y ejecutar código basado en qué patrón coincide.

Estructura básica de match

La sintaxis básica de match sigue este formato:

match valor_a_comparar {
    patron1 => expresion1,
    patron2 => expresion2,
    patron3 => {
        // Bloque de código para casos más complejos
        expresion3
    },
    // Más patrones...
    _ => expresion_predeterminada,
}

Cada línea dentro del bloque match se llama un brazo (arm) y consiste en un patrón seguido de => y la expresión a ejecutar cuando ese patrón coincide.

Veamos un ejemplo simple:

let numero = 3;

let texto = match numero {
    1 => "uno",
    2 => "dos",
    3 => "tres",
    4 => "cuatro",
    5 => "cinco",
    _ => "otro número",
};

println!("{} en texto es: {}", numero, texto); // Imprime: 3 en texto es: tres

En este ejemplo, match compara el valor de numero con cada patrón (los números del 1 al 5) y devuelve el texto correspondiente cuando encuentra una coincidencia.

El patrón comodín (_)

El patrón _ (guion bajo) es el comodín que coincide con cualquier valor que no haya coincidido con los patrones anteriores. Es similar al default en un switch de otros lenguajes:

let codigo_dia = 6;

let dia = match codigo_dia {
    1 => "lunes",
    2 => "martes",
    3 => "miércoles",
    4 => "jueves",
    5 => "viernes",
    6 => "sábado",
    7 => "domingo",
    _ => "día inválido",
};

println!("El día es: {}", dia); // Imprime: El día es: sábado

Exhaustividad: una característica de seguridad

Una de las ventajas más importantes de match en Rust es que el compilador verifica que todos los posibles valores sean manejados. Esto se conoce como exhaustividad y evita errores comunes donde olvidamos manejar ciertos casos:

let booleano = true;

// Este match debe cubrir todos los posibles valores de un booleano
let mensaje = match booleano {
    true => "Es verdadero",
    false => "Es falso",
    // No necesitamos _ aquí porque ya cubrimos todos los casos posibles
};

println!("{}", mensaje); // Imprime: Es verdadero

Si intentáramos omitir alguno de los casos en el ejemplo anterior, el compilador nos mostraría un error indicando que el match no es exhaustivo.

Capturando valores en patrones

Podemos capturar el valor que coincide con un patrón y usarlo en la expresión asociada:

let numero = 15;

let descripcion = match numero {
    0 => "cero",
    1 => "uno",
    n if n < 0 => "negativo",
    n if n < 10 => "dígito simple",
    n => {
        let mensaje = format!("número grande: {}", n);
        mensaje  // Devuelve esta expresión
    }
};

println!("{} es {}", numero, descripcion); // Imprime: 15 es número grande: 15

En este ejemplo, n captura el valor de numero cuando coincide con los patrones condicionales o el patrón por defecto.

Patrones con guardas

Los guardas son condiciones adicionales que se pueden añadir a un patrón usando if:

let temperatura = 25;

let sensacion = match temperatura {
    t if t < 0 => "congelante",
    t if t < 10 => "muy frío",
    t if t < 20 => "fresco",
    t if t < 30 => "agradable",
    t if t < 40 => "caluroso",
    _ => "extremadamente caluroso",
};

println!("La sensación térmica es: {}", sensacion); // Imprime: La sensación térmica es: agradable

Los guardas permiten crear patrones más específicos basados en condiciones arbitrarias.

Múltiples patrones con el operador |

Podemos usar el operador | (OR) para hacer coincidir múltiples patrones en un solo brazo:

let dia_semana = 6;

let tipo_dia = match dia_semana {
    1 | 2 | 3 | 4 | 5 => "día laborable",
    6 | 7 => "fin de semana",
    _ => "día inválido",
};

println!("El día {} es {}", dia_semana, tipo_dia); // Imprime: El día 6 es fin de semana

Rangos en patrones

Para rangos numéricos consecutivos, podemos usar la sintaxis de rango ..=:

let calificacion = 85;

let resultado = match calificacion {
    0..=59 => "suspenso",
    60..=69 => "aprobado",
    70..=79 => "notable",
    80..=89 => "sobresaliente",
    90..=100 => "matrícula de honor",
    _ => "calificación inválida",
};

println!("Con {} puntos: {}", calificacion, resultado); // Imprime: Con 85 puntos: sobresaliente

Comparación con if/else

Aunque if/else y match pueden resolver problemas similares, match brinda varias ventajas:

  • Es más expresivo para comparar un valor contra múltiples posibilidades
  • Garantiza exhaustividad (manejar todos los casos posibles)
  • Permite patrones más sofisticados que simples comparaciones booleanas

Comparemos ambos enfoques:

// Usando if/else
let numero = 3;
let texto;

if numero == 1 {
    texto = "uno";
} else if numero == 2 {
    texto = "dos";
} else if numero == 3 {
    texto = "tres";
} else {
    texto = "otro número";
}

// Usando match (más conciso y expresivo)
let numero = 3;
let texto = match numero {
    1 => "uno",
    2 => "dos",
    3 => "tres",
    _ => "otro número",
};

Uso de match en expresiones

Al igual que if/else, match es una expresión que devuelve un valor, por lo que podemos usarlo en asignaciones o como parte de expresiones más complejas:

let codigo_error = 404;

let mensaje = format!(
    "Error {}: {}",
    codigo_error,
    match codigo_error {
        404 => "No encontrado",
        403 => "Prohibido",
        500 => "Error interno del servidor",
        _ => "Error desconocido",
    }
);

println!("{}", mensaje); // Imprime: Error 404: No encontrado

Patrones con variables

Podemos usar variables en los patrones para capturar valores específicos:

let puntos = 75;

let mensaje = match puntos {
    0 => "Sin puntos",
    1 => "Un punto",
    n @ 2..=10 => format!("Pocos puntos: {}", n),
    n @ 11..=50 => format!("Puntos moderados: {}", n),
    n @ 51..=100 => format!("Muchos puntos: {}", n),
    n => format!("Demasiados puntos: {}", n),
};

println!("{}", mensaje); // Imprime: Muchos puntos: 75

En este ejemplo, usamos la sintaxis n @ patrón para capturar en la variable n el valor que coincide con el patrón.

Consideraciones de rendimiento

El compilador de Rust optimiza las expresiones match para que sean muy eficientes. En muchos casos, especialmente con tipos simples como enteros o enumeraciones, el código generado es tan eficiente como una tabla de saltos o una serie de comparaciones directas.

La expresión match es una herramienta fundamental en Rust que combina potencia, seguridad y expresividad. A medida que avances en tu aprendizaje de Rust, descubrirás usos más sofisticados de match con tipos de datos más complejos, pero incluso con estos fundamentos básicos, ya puedes aprovechar su potencial para escribir código más claro y robusto.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Estructuras de control condicional con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender que en Rust las estructuras if/else son expresiones que devuelven valores.
  • Aprender a usar condicionales inline para asignaciones concisas.
  • Conocer la sintaxis y uso básico de la expresión match para control de flujo.
  • Entender la exhaustividad y patrones en match, incluyendo guardas y rangos.
  • Comparar el uso de if/else y match para diferentes escenarios de control condicional.