Primeros pasos en Rust - Estructuras de Control

Apuntes de Rust - Parte 5

Estructuras de Control

La estructuras de control permiten ejecutar código basado en una condición y/o de repetir código mientras se cumpla una condición. Estas estructuras son fundamentales en la mayoría de los lenguajes de programación.

En Rust, las construcciones más comunes para controlar el flujo de ejecución son las expresiones if, match y los ciclos. Rust tiene tres tipos de ciclos: loop, while y for.

Condicionales

En Rust, puedes usar la estructura de control if para crear condicionales (como en otros lenguajes de programación). La sintaxis básica de un condicional en Rust es la siguiente:

if condición {
    // Código a ejecutar si la condición es verdadera
} else {
    // Código a ejecutar si la condición es falsa
}

Un ejemplo:

fn es_par_impar(numero: i32) {
    if numero % 2 == 0 {
        println!("El número {} es par", numero);
    } else {
        println!("El número {} es impar", numero);
    }
}

fn main() {
    let numero1 = 10;
    let numero2 = 7;

    es_par_impar(numero1);
    es_par_impar(numero2);
}

La función es_par_impar, recibe un parámetro nombrado numero de tipo i32, que representa el número a verificar. La estructura de control if para comprobar si el número es divisible por 2 (la condición!). Si es así, se imprime el mensaje "El número es par". De lo contrario, se imprime el mensaje "El número es impar".

Puede usar varias condiciones combinando las expresiones if, else if y else. Por ejemplo:

fn obtener_dia_semana(numero: i32) -> String {
    if numero == 1 {
        String::from("Lunes")
    } else if numero == 2 {
        String::from("Martes")
    } else if numero == 3 {
        String::from("Miércoles")
    } else if numero == 4 {
        String::from("Jueves")
    } else if numero == 5 {
        String::from("Viernes")
    } else if numero == 6 {
        String::from("Sábado")
    } else if numero == 7 {
        String::from("Domingo")
    } else {
        String::from("Ingresa un valor válido (1 al 7)")
    }
}

La instrucción match

La instrucción match en Rust permite validar coincidencias de patrones y tomar decisiones en función de los diferentes casos que pueden ocurrir. Permite comparar un valor con una serie de patrones predefinidos y ejecutar el bloque de código correspondiente al patrón que coincide con el valor.

La sintaxis básica de la instrucción match es la siguiente:

match valor {
    patrón1 => {
        // Bloque de código para el patrón1
    },
    patrón2 => {
        // Bloque de código para el patrón2
    },
    // ... más patrones ...
    _ => {
        // Bloque de código por defecto si no se cumple ninguno de 
        // los patrones anteriores
    }
}

El uso de la instrucción match es útil cuando se trabaja con tipos enumerados (enums) o estructuras de datos complejas, ya que permite realizar un análisis exhaustivo de todas las posibilidades y actuar en consecuencia. También es una alternativa más legible y segura que el encadenamiento de declaraciones if-else.

match se asemeja al bloque switch que se encuentra en otros lenguajes de programación. Ambas construcciones permiten tomar decisiones basadas en el valor de una expresión y ejecutar diferentes bloques de código según los casos que se encuentren.

Ejemplo:

fn obtener_dia_semana(numero: i32) -> String {
    match numero {
        1 => String::from("Lunes"),
        2 => String::from("Martes"),
        3 => String::from("Miércoles"),
        4 => String::from("Jueves"),
        5 => String::from("Viernes"),
        6 => String::from("Sábado"),
        7 => String::from("Domingo"),
        _ => String::from("Ingresa un valor válido (1 al 7)"),
    }
}

La instrucción loop

La instrucción loop permite que se ejecute un bloque de código una y otra vez hasta que se le indique explícitamente que se detenga.

Ejemplo:

fn main() {
    let mut contador = 0;

    loop {
        println!("El contador es: {}", contador );
        contador += 1;

        if contador == 10 {
            break;
        }
    }
}

La instrucción loop en Rust crea un ciclo infinito, es decir, un ciclo que se repite de manera indefinida hasta que se encuentre una instrucción break dentro del bloque de código.

fn factorial(numero: u32) -> u32 {
    let mut resultado = 1;

    if numero == 0 {
        return resultado ;
    }

    let mut i = 1;
    loop {
        resultado*= i;
        i += 1;

        if i > numero {
            break;
        }
    }

    resultado
}

fn main() {
    let numero = 5;
    let factorial_numero = factorial(numero);

    println!("El factorial de {} es: {}", numero, factorial_numero);
}

La instrucción while

La instrucción while se utiliza para ejecutar un bloque de código repetidamente mientras se cumple una condición específica. La estructura básica de un ciclo while es la siguiente:

while condición {
    // Código a ejecutar mientras la condición sea verdadera
}

Ejemplo:

fn factorial(numero: u32) -> u32 {
    let mut resultado = 1;
    let mut i: u32 = 1;

    while i <= numero {
        resultado *= i;
        i += 1;
    }

    resultado
}

fn main() {
    let numero = 5;
    let factorial_resultado = factorial(numero);

    println!("El factorial de {} es: {}", numero, factorial_resultado);
}

La instrucción for

En Rust, el ciclo for se utiliza para iterar sobre una secuencia de elementos. A diferencia de algunos otros lenguajes de programación, el ciclo for en Rust no se utiliza para contar iteraciones, sino para recorrer una colección de elementos.

La sintaxis básica de un ciclo for en Rust es la siguiente:

for elemento in secuencia {
    // Código a ejecutar en cada iteración
}

Ejemplo:

fn main() {
    let lista_de_numeros = [1, 2, 3, 4, 5];

    for numero in lista_de_numeros {
        println!("Número: {}", numero);
    }
}

Ahora un ejemplo, utilizando funciones, arreglos, el ciclo for y condicionales:

fn encontrar_numero_mas_alto(numeros: &[i32]) {
    let mut maximo = numeros[0]; 

    for &numero in numeros {
        if numero > maximo {
            maximo = numero;
        }
    }

    println!("El número más alto es: {}", maximo);
}

fn main() {
    let numeros = [17, 5, 3, 31, 37, 2, 1];
    encontrar_numero_mas_alto(&numeros);
}

Las referencias

En el ejemplo anterior vemos el uso del & en el código, esto se conoce como referencia. En Rust, las referencias son una forma de hacer referencia a un valor sin tomar posesión de él. Proporcionan una manera de acceder a los datos sin moverlos o copiarlos (de o en la memoria), lo que permite un mejor control sobre la propiedad y el rendimiento en el código.

En Rust, las referencias se crean utilizando el operador & seguido del valor al que se quiere hacer referencia. Hay dos tipos de referencias: referencias inmutables y referencias mutables.

  • Referencias inmutables (&T): Permiten acceder a los datos de solo lectura. Esto garantiza la inmutabilidad y la ausencia de condiciones de carrera.

  • Referencias mutables (&mut T): Permiten acceder y modificar los datos referenciados. Sin embargo, solo se permite una referencia mutable a la vez en un ámbito determinado. Esto garantiza la exclusividad y previene las condiciones de carrera, ya que solo un código puede modificar los datos a la vez.

Ejemplo de referencias inmutables:

fn main() {
    let mut x = 5; // Variable original

    // Creamos una referencia inmutable a `x`
    let referencia = &x;

    // Imprimimos el valor de `x` a través de la referencia
    println!("El valor de x es: {}", *referencia);
}

Ejemplo de referencias mutables:

fn main() {
    let mut x = 5; // Variable original

    // Creamos una referencia mutable a `x`
    let referencia_mut: &mut i32 = &mut x;

    // Modificamos `x` a través de la referencia mutable
    *referencia_mut += 1;

    // Imprimimos el nuevo valor de `x`
    println!("El nuevo valor de x es: {}", x);
}

Nota: recuerde ejecutar cargo run para ejecutar su aplicación.

Y sí, las referencias en Rust se asemejan a los punteros en otros lenguajes de programación, pero con algunas diferencias clave y restricciones adicionales impuestas por el sistema de préstamo de Rust.

Las principales diferencias entre las referencias en Rust y los punteros en otros lenguajes son:

  1. Restricciones de préstamo: Rust tiene un sistema de préstamo que permite el préstamo de referencias de manera segura y controlada. Este sistema garantiza que las reglas de préstamo, como la exclusividad de las referencias mutables, se cumplan en tiempo de compilación.

  2. Sin aritmética de punteros: A diferencia de los punteros en otros lenguajes, las referencias en Rust no admiten aritmética de punteros ni operaciones directas de manipulación de memoria.

  3. Control de tiempo de vida: En Rust, las referencias tienen un tiempo de vida, que está determinado por el ámbito en el que se crean. Esto ayuda al compilador a realizar análisis estático y garantizar que las referencias sean válidas durante el tiempo que se utilizan.

  4. Propiedad y préstamo: En Rust, las referencias se utilizan para prestar temporalmente la propiedad de los datos a una función o contexto específico. Esto permite un control preciso sobre la propiedad de los datos y evita problemas como fugas de memoria y condiciones de carrera.

Por cierto, sobre las condiciones de carrera:

Referencias

  • The Rust Programming Language. 2nd Edition by Steve Klabnik and Carol Nichols, with contributions from the Rust Community. 2023

Did you find this article valuable?

Support Jesse Padilla by becoming a sponsor. Any amount is appreciated!