Rust

Rust

Tutorial Rust: Estructuras de control iterativo

Aprende las estructuras de control iterativo en Rust: loop, while, for, controladores de flujo y ranges para programación eficiente.

Aprende Rust y certifícate

loop, while, for

En Rust, los bucles son estructuras fundamentales que permiten ejecutar un bloque de código repetidamente. A diferencia de otros lenguajes, Rust ofrece tres tipos de bucles principales, cada uno con características únicas que los hacen adecuados para diferentes situaciones.

El bucle loop

El bucle loop es la estructura iterativa más básica en Rust. Este bucle ejecuta un bloque de código indefinidamente hasta que se encuentra explícitamente con una instrucción break.

fn main() {
    let mut contador = 0;
    
    loop {
        println!("Valor actual: {}", contador);
        contador += 1;
        
        if contador == 5 {
            break; // Sale del bucle cuando contador llega a 5
        }
    }
    
    println!("Bucle finalizado");
}

Una característica poderosa de los bucles en Rust es que son expresiones, lo que significa que pueden devolver valores. Esto se logra proporcionando un valor a la instrucción break:

fn main() {
    let mut contador = 0;
    
    let resultado = loop {
        contador += 1;
        
        if contador == 10 {
            break contador * 2; // Devuelve contador * 2 al salir del bucle
        }
    };
    
    println!("El resultado es: {}", resultado); // Imprime: El resultado es: 20
}

Este enfoque es especialmente útil cuando necesitas buscar un valor o realizar cálculos iterativos y devolver el resultado final.

El bucle while

El bucle while ejecuta un bloque de código mientras una condición específica sea verdadera. Es ideal cuando conoces la condición de terminación pero no necesariamente el número exacto de iteraciones.

fn main() {
    let mut numero = 1;
    
    while numero < 50 {
        // Si es divisible por 7, imprime el número
        if numero % 7 == 0 {
            println!("{} es divisible por 7", numero);
        }
        
        numero += 1;
    }
}

El bucle while es más conciso que un loop con una condición de salida cuando la lógica de terminación es simple. Sin embargo, a diferencia de loop, no es común usar while para devolver valores, aunque técnicamente es posible.

El bucle for

El bucle for en Rust está diseñado específicamente para iterar sobre elementos de una colección o secuencia. La sintaxis es clara y previene errores comunes como acceder fuera de los límites de una colección.

fn main() {
    // Iterando sobre un rango numérico
    for i in 1..6 {
        println!("Número: {}", i);
    }
    
    // Iterando sobre un rango inclusivo (incluye el valor final)
    for letra in 'a'..='e' {
        println!("Letra: {}", letra);
    }
}

El bucle for es especialmente útil porque:

  • Es conciso y elimina la necesidad de gestionar manualmente índices o contadores
  • Previene errores de fuera de rango comunes en otros lenguajes
  • Funciona con cualquier tipo que implemente el trait IntoIterator

Comparación de los tres tipos de bucles

Cada tipo de bucle tiene su caso de uso ideal:

  • loop: Cuando necesitas un bucle infinito o quieres devolver un valor desde el bucle
  • while: Cuando tienes una condición clara de terminación
  • for: Cuando necesitas iterar sobre una secuencia de elementos

Veamos un ejemplo que resuelve el mismo problema con los tres tipos de bucles:

fn main() {
    // Sumar los números del 1 al 5 usando loop
    let mut suma = 0;
    let mut contador = 1;
    
    loop {
        suma += contador;
        contador += 1;
        
        if contador > 5 {
            break;
        }
    }
    
    println!("Suma con loop: {}", suma);
    
    // Sumar los números del 1 al 5 usando while
    let mut suma = 0;
    let mut contador = 1;
    
    while contador <= 5 {
        suma += contador;
        contador += 1;
    }
    
    println!("Suma con while: {}", suma);
    
    // Sumar los números del 1 al 5 usando for
    let mut suma = 0;
    
    for numero in 1..=5 {
        suma += numero;
    }
    
    println!("Suma con for: {}", suma);
}

Como puedes observar, el bucle for proporciona la solución más elegante y menos propensa a errores para este caso particular.

Bucles y mutabilidad

Es importante notar que en Rust, la variable de iteración en un bucle for es inmutable por defecto dentro del cuerpo del bucle:

fn main() {
    for i in 1..6 {
        // i es inmutable aquí
        println!("Valor: {}", i);
        
        // Esto causaría un error de compilación:
        // i += 1;
    }
}

Si necesitas una variable mutable en el cuerpo del bucle, puedes declarar una nueva variable:

fn main() {
    for i in 1..6 {
        let mut valor = i;
        valor *= 2;
        println!("El doble de {} es {}", i, valor);
    }
}

Esta característica ayuda a prevenir modificaciones accidentales de la variable de iteración, lo que podría llevar a comportamientos inesperados o bucles infinitos en otros lenguajes.

Controladores de flujo

Los controladores de flujo son herramientas esenciales que nos permiten modificar el comportamiento normal de los bucles en Rust. Estos mecanismos nos dan un control preciso sobre cuándo continuar con la siguiente iteración o cuándo abandonar completamente un bucle.

break y continue

Rust proporciona dos controladores de flujo principales para manipular la ejecución de bucles:

  • break: Termina inmediatamente el bucle más interno y continúa con el código que sigue después del bucle.
  • continue: Salta el resto del código en la iteración actual y pasa a la siguiente iteración.

Veamos cómo funcionan estos controladores con ejemplos prácticos:

fn main() {
    // Ejemplo de continue
    for i in 1..10 {
        // Salta los números pares
        if i % 2 == 0 {
            continue;
        }
        println!("Número impar: {}", i);
    }
    
    // Ejemplo de break
    let mut suma = 0;
    for i in 1..100 {
        suma += i;
        if suma > 50 {
            println!("La suma superó 50 en la iteración {}", i);
            break;
        }
    }
}

En el primer ejemplo, continue nos permite filtrar valores durante la iteración sin necesidad de anidar todo el código en un bloque condicional. En el segundo ejemplo, break nos permite detener el bucle cuando se cumple una condición específica, evitando iteraciones innecesarias.

Etiquetas de bucle

Cuando trabajamos con bucles anidados, a veces necesitamos más control sobre qué bucle específico queremos afectar con break o continue. Rust resuelve este problema con las etiquetas de bucle, que nos permiten nombrar cada bucle y referenciarlos específicamente:

fn main() {
    // Etiquetamos el bucle externo como 'externo'
    'externo: for x in 1..6 {
        println!("x: {}", x);
        
        // Bucle interno sin etiquetar
        for y in 1..6 {
            println!("  y: {}", y);
            
            if x * y > 10 {
                // Rompe el bucle externo, no solo el interno
                println!("  Rompiendo bucle externo en x={}, y={}", x, y);
                break 'externo;
            }
            
            if y > 2 {
                // Continúa con la siguiente iteración del bucle externo
                println!("  Saltando a la siguiente x");
                continue 'externo;
            }
        }
    }
}

En este ejemplo:

  1. Definimos una etiqueta llamada 'externo' para el primer bucle
  2. Usamos break 'externo' para salir completamente de ambos bucles cuando x * y > 10
  3. Usamos continue 'externo' para saltar a la siguiente iteración del bucle externo cuando y > 2

Las etiquetas de bucle son especialmente útiles en algoritmos complejos donde necesitas controlar con precisión el flujo de ejecución entre múltiples niveles de bucles.

Combinando controladores de flujo con valores de retorno

Como vimos anteriormente, los bucles en Rust son expresiones que pueden devolver valores. Podemos combinar esto con los controladores de flujo para crear patrones elegantes:

fn main() {
    let resultado = 'busqueda: loop {
        for i in 1..100 {
            for j in 1..100 {
                if i * j == 819 {
                    // Rompe el bucle etiquetado y devuelve un valor
                    break 'busqueda (i, j);
                }
            }
        }
        // Si no se encuentra, devuelve esto (aunque en este caso siempre encontrará)
        break (0, 0);
    };
    
    println!("Los números que multiplicados dan 819 son: {} y {}", resultado.0, resultado.1);
}

En este ejemplo, estamos buscando dos números cuyo producto es 819. Cuando los encontramos, usamos break con la etiqueta del bucle más externo para salir de todos los bucles y devolver una tupla con los valores encontrados.

Controladores de flujo condicionales

A menudo necesitamos aplicar controladores de flujo basados en condiciones. Aunque ya hemos visto ejemplos con if, vale la pena destacar algunos patrones comunes:

fn main() {
    let mut contador = 0;
    
    // Bucle que se ejecuta hasta que se cumple una condición
    while contador < 10 {
        contador += 1;
        
        // Salta las iteraciones para múltiplos de 3
        if contador % 3 == 0 {
            println!("Saltando {}", contador);
            continue;
        }
        
        println!("Procesando {}", contador);
        
        // Sale del bucle si encuentra un valor específico
        if contador == 7 {
            println!("¡Encontrado el valor especial!");
            break;
        }
    }
}

Este patrón de filtrado y terminación temprana es muy común en la programación real y permite escribir código más eficiente que no realiza trabajo innecesario.

Uso de controladores de flujo en diferentes tipos de bucles

Los controladores de flujo funcionan de manera similar en los tres tipos de bucles de Rust, pero hay algunos matices a considerar:

fn main() {
    // En bucles loop
    let mut n = 0;
    let resultado = loop {
        n += 1;
        if n % 5 == 0 {
            break n; // Devuelve el primer múltiplo de 5
        }
    };
    println!("Primer múltiplo de 5: {}", resultado);
    
    // En bucles while
    let mut x = 0;
    while x < 10 {
        x += 1;
        if x % 2 == 0 {
            continue; // Salta los números pares
        }
        println!("Número impar en while: {}", x);
    }
    
    // En bucles for
    for i in 0..8 {
        if i < 2 {
            println!("Valor muy pequeño, continuando");
            continue;
        }
        if i > 5 {
            println!("Valor muy grande, saliendo");
            break;
        }
        println!("Procesando valor: {}", i);
    }
}

En este ejemplo, podemos ver cómo los controladores de flujo se adaptan a cada tipo de bucle, permitiéndonos expresar lógica compleja de manera clara y concisa.

Consideraciones de rendimiento

Los controladores de flujo no solo mejoran la legibilidad del código, sino que también pueden tener un impacto positivo en el rendimiento:

fn main() {
    let mut encontrado = false;
    let objetivo = 317;
    
    // Búsqueda en un rango grande
    for i in 1..1000 {
        if i * i == objetivo {
            encontrado = true;
            println!("¡Encontrado! {} es el cuadrado de {}", objetivo, i);
            break; // Evita iteraciones innecesarias
        }
    }
    
    if !encontrado {
        println!("{} no es un cuadrado perfecto en el rango buscado", objetivo);
    }
}

En este caso, break nos permite detener la búsqueda tan pronto como encontramos lo que estamos buscando, evitando cálculos innecesarios y mejorando la eficiencia del programa.

Iteración sobre ranges

Los ranges (rangos) en Rust son una forma elegante de expresar secuencias de valores. Funcionan como una especie de atajo para generar una serie de números o caracteres sin tener que escribirlos explícitamente. Esta característica es particularmente útil cuando necesitamos iterar sobre secuencias en bucles for.

Un range se crea utilizando la sintaxis con dos puntos (.. o ..=). Existen dos tipos principales:

fn main() {
    // Range exclusivo: incluye el inicio pero excluye el final (1, 2, 3, 4)
    let rango_exclusivo = 1..5;
    
    // Range inclusivo: incluye tanto el inicio como el final (1, 2, 3, 4, 5)
    let rango_inclusivo = 1..=5;
    
    println!("Demostración de rangos");
}

Iterando sobre rangos numéricos

La forma más común de utilizar ranges es con el bucle for, que automáticamente recorre cada valor del rango:

fn main() {
    // Iteración sobre un rango exclusivo
    println!("Rango exclusivo (1..5):");
    for numero in 1..5 {
        println!("  Valor: {}", numero);
    }
    
    // Iteración sobre un rango inclusivo
    println!("Rango inclusivo (1..=5):");
    for numero in 1..=5 {
        println!("  Valor: {}", numero);
    }
}

Este código imprimirá los valores del 1 al 4 para el rango exclusivo, y del 1 al 5 para el rango inclusivo.

Rangos con diferentes tipos de datos

Los ranges no están limitados a enteros. Podemos crear rangos con cualquier tipo que implemente el trait Step, como los caracteres:

fn main() {
    // Rango de caracteres (exclusivo)
    println!("Letras de 'a' a 'd' (exclusivo):");
    for letra in 'a'..'e' {
        println!("  Letra: {}", letra);
    }
    
    // Rango de caracteres (inclusivo)
    println!("Letras de 'a' a 'e' (inclusivo):");
    for letra in 'a'..='e' {
        println!("  Letra: {}", letra);
    }
}

Rangos con paso personalizado

Aunque los ranges por sí solos no permiten especificar un paso diferente a 1, podemos combinarlos con el método step_by() para iterar saltando elementos:

fn main() {
    // Números pares del 0 al 10
    println!("Números pares del 0 al 10:");
    for numero in (0..=10).step_by(2) {
        println!("  {}", numero);
    }
    
    // Múltiplos de 3 del 0 al 20
    println!("Múltiplos de 3 del 0 al 20:");
    for numero in (0..=20).step_by(3) {
        println!("  {}", numero);
    }
}

Rangos en orden inverso

Para iterar sobre un rango en orden inverso, podemos usar el método rev():

fn main() {
    println!("Cuenta regresiva:");
    for numero in (1..=10).rev() {
        println!("  {}", numero);
    }
    println!("¡Despegue!");
    
    // También funciona con caracteres
    println!("Alfabeto inverso (de 'e' a 'a'):");
    for letra in ('a'..='e').rev() {
        println!("  {}", letra);
    }
}

Casos de uso prácticos

Los ranges son extremadamente útiles para muchas tareas comunes de programación:

Generación de tablas

fn main() {
    println!("Tabla de multiplicar del 5:");
    for i in 1..=10 {
        println!("  5 × {} = {}", i, 5 * i);
    }
}

Cálculo de sumatorias

fn main() {
    let mut suma = 0;
    
    // Sumar números del 1 al 100
    for numero in 1..=100 {
        suma += numero;
    }
    
    println!("La suma de los números del 1 al 100 es: {}", suma);
    
    // Verificación: fórmula n(n+1)/2
    let verificacion = 100 * 101 / 2;
    println!("Verificación mediante fórmula: {}", verificacion);
}

Generación de patrones

fn main() {
    // Generar un patrón de triángulo
    let altura = 5;
    
    for i in 1..=altura {
        for _ in 1..=i {
            print!("* ");
        }
        println!();
    }
}

Limitaciones de los ranges

Es importante entender algunas limitaciones de los ranges:

  • Los ranges por sí mismos no almacenan todos los valores de la secuencia, sino solo los límites
  • No se pueden crear ranges sin límite superior en un bucle for (como 0..)
  • Los ranges funcionan mejor con tipos que tienen un tamaño fijo y conocido
fn main() {
    // Esto funciona bien
    for i in 1..=5 {
        println!("{}", i);
    }
    
    // Esto causaría un error porque no tiene límite superior
    // for i in 5.. {
    //     println!("{}", i);
    // }
}

Combinando ranges con otras estructuras de control

Los ranges se integran perfectamente con las estructuras de control que ya conocemos:

fn main() {
    // Encontrar el primer número cuyo cuadrado es mayor que 50
    for n in 1..100 {
        if n * n > 50 {
            println!("El primer número cuyo cuadrado es mayor que 50 es: {}", n);
            println!("Su cuadrado es: {}", n * n);
            break;
        }
    }
    
    // Imprimir solo los números impares en un rango
    println!("Números impares entre 1 y 10:");
    for n in 1..=10 {
        if n % 2 == 0 {
            continue;
        }
        println!("  {}", n);
    }
}

Ranges como expresiones

Los ranges también pueden usarse en expresiones condicionales para verificar si un valor está dentro de ciertos límites:

fn main() {
    let puntuacion = 85;
    
    let calificacion = if puntuacion >= 90 {
        "A"
    } else if puntuacion >= 80 && puntuacion < 90 {
        "B"
    } else if puntuacion >= 70 && puntuacion < 80 {
        "C"
    } else if puntuacion >= 60 && puntuacion < 70 {
        "D"
    } else {
        "F"
    };
    
    println!("Con una puntuación de {}, la calificación es: {}", puntuacion, calificacion);
    
    // Forma más concisa usando ranges y match
    let calificacion = match puntuacion {
        90..=100 => "A",
        80..=89 => "B",
        70..=79 => "C",
        60..=69 => "D",
        _ => "F",
    };
    
    println!("Usando match: Con una puntuación de {}, la calificación es: {}", puntuacion, calificacion);
}

Los ranges son una herramienta fundamental en Rust que simplifica muchas tareas comunes de programación. Su integración con los bucles for hace que la iteración sobre secuencias sea intuitiva y menos propensa a errores, permitiéndonos escribir código más limpio y expresivo.

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 iterativo 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 las diferencias y usos de los bucles loop, while y for en Rust.
  • Aprender a utilizar controladores de flujo como break, continue y etiquetas de bucle para controlar la ejecución.
  • Entender cómo los bucles en Rust pueden devolver valores y cómo aprovechar esta característica.
  • Conocer la sintaxis y aplicaciones de los rangos (ranges) para iterar secuencias numéricas y de caracteres.
  • Aplicar buenas prácticas para evitar errores comunes y mejorar la legibilidad y eficiencia del código iterativo.