Rust

Rust

Tutorial Rust: Funciones

Aprende a declarar funciones en Rust, uso de retorno implícito, ámbito de variables y shadowing para escribir código modular y eficiente.

Aprende Rust y certifícate

Declaración y parámetros

Las funciones son bloques de código reutilizables que realizan tareas específicas. En Rust, las funciones son componentes fundamentales que nos permiten organizar nuestro código de manera eficiente y modular.

Declaración básica de funciones

Para declarar una función en Rust, utilizamos la palabra clave fn seguida del nombre de la función y un par de paréntesis. El cuerpo de la función se encierra entre llaves {}:

fn saludar() {
    println!("¡Hola, mundo!");
}

fn main() {
    saludar(); // Llamada a la función
}

En este ejemplo, hemos definido una función llamada saludar que imprime un mensaje. Luego, desde la función main (punto de entrada de todo programa Rust), llamamos a nuestra función.

Convenciones de nomenclatura

En Rust, los nombres de las funciones siguen la convención snake_case, es decir, todas las letras en minúsculas con palabras separadas por guiones bajos:

fn calcular_promedio() {
    // Código de la función
}

fn validar_usuario() {
    // Código de la función
}

Parámetros de función

Las funciones pueden recibir datos de entrada a través de parámetros. En Rust, debemos especificar el tipo de cada parámetro:

fn saludar_persona(nombre: &str) {
    println!("¡Hola, {}!", nombre);
}

fn main() {
    saludar_persona("Ana");
    saludar_persona("Carlos");
}

En este ejemplo, la función saludar_persona recibe un parámetro nombre de tipo &str (una referencia a una cadena de texto).

Múltiples parámetros

Podemos definir funciones con múltiples parámetros separándolos con comas:

fn sumar(a: i32, b: i32) {
    let resultado = a + b;
    println!("La suma de {} y {} es: {}", a, b, resultado);
}

fn main() {
    sumar(5, 3);
    sumar(-2, 8);
}

Cada parámetro debe tener su propio tipo especificado. En este caso, ambos parámetros son de tipo i32 (enteros de 32 bits con signo).

Tipo de retorno

Para que una función devuelva un valor, debemos especificar su tipo de retorno después de una flecha ->:

fn sumar(a: i32, b: i32) -> i32 {
    a + b  // Retorna la suma de a y b
}

fn main() {
    let resultado = sumar(5, 3);
    println!("El resultado es: {}", resultado);
}

En este ejemplo, la función sumar recibe dos enteros y devuelve otro entero. Observa que no utilizamos la palabra clave return ni un punto y coma al final de la expresión a + b. Esto es porque en Rust, la última expresión de un bloque se retorna implícitamente (hablaremos más sobre esto en la siguiente sección).

Uso explícito de return

Aunque Rust permite el retorno implícito, también podemos usar la palabra clave return para devolver un valor explícitamente, especialmente útil para retornos anticipados:

fn valor_absoluto(numero: i32) -> i32 {
    if numero >= 0 {
        return numero;  // Retorno anticipado
    }
    
    -numero  // Retorno implícito para números negativos
}

fn main() {
    println!("Absoluto de 5: {}", valor_absoluto(5));
    println!("Absoluto de -3: {}", valor_absoluto(-3));
}

Parámetros con valores por defecto

A diferencia de otros lenguajes, Rust no admite parámetros con valores por defecto directamente. Sin embargo, podemos simular este comportamiento usando patrones como:

fn saludar(nombre: &str, saludo: Option<&str>) {
    let saludo_final = saludo.unwrap_or("Hola");
    println!("{}, {}!", saludo_final, nombre);
}

fn main() {
    saludar("Ana", Some("Bienvenida"));  // Con saludo personalizado
    saludar("Carlos", None);             // Con saludo por defecto
}

En este ejemplo, usamos Option<&str> para indicar que el parámetro saludo puede estar presente (Some) o ausente (None).

Funciones sin valor de retorno

Si una función no devuelve ningún valor, podemos omitir la especificación del tipo de retorno o usar el tipo especial () (unidad):

// Estas dos declaraciones son equivalentes
fn imprimir_mensaje(mensaje: &str) {
    println!("{}", mensaje);
}

fn imprimir_mensaje_explicito(mensaje: &str) -> () {
    println!("{}", mensaje);
}

El tipo () representa la ausencia de un valor y es el tipo de retorno implícito cuando no se especifica ninguno.

Paso de parámetros y propiedad

En Rust, cuando pasamos un valor a una función, puede ocurrir una transferencia de propiedad dependiendo del tipo de dato. Por ejemplo:

fn procesar_texto(texto: String) {
    println!("Procesando: {}", texto);
}

fn main() {
    let mensaje = String::from("Hola mundo");
    procesar_texto(mensaje);
    // A partir de aquí, 'mensaje' ya no es utilizable
}

En Rust, pasar un valor a una función puede transferir su propiedad, pero esto lo veremos en módulos posteriores cuando estudiemos el sistema de ownership.

Documentación de funciones

Una buena práctica en Rust es documentar nuestras funciones usando comentarios de documentación (con tres barras ///):

/// Calcula el área de un rectángulo.
///
/// # Argumentos
///
/// * `ancho` - El ancho del rectángulo
/// * `alto` - El alto del rectángulo
///
/// # Ejemplo
///
/// ```
/// let area = calcular_area(5.0, 3.0);
/// assert_eq!(area, 15.0);
/// ```
fn calcular_area(ancho: f64, alto: f64) -> f64 {
    ancho * alto
}

Estos comentarios generan documentación automática cuando compilamos nuestro código con herramientas como cargo doc.

Funciones anidadas

Rust permite definir funciones dentro de otras funciones, lo que resulta útil cuando una función auxiliar solo se necesita en un contexto específico:

fn operaciones_matematicas(a: i32, b: i32) {
    fn sumar(x: i32, y: i32) -> i32 {
        x + y
    }
    
    fn multiplicar(x: i32, y: i32) -> i32 {
        x * y
    }
    
    let suma = sumar(a, b);
    let producto = multiplicar(a, b);
    
    println!("Suma: {}, Producto: {}", suma, producto);
}

fn main() {
    operaciones_matematicas(5, 3);
}

Las funciones anidadas solo son visibles dentro de la función que las contiene.

Retorno implícito

En Rust, una de las características más elegantes del lenguaje es su sistema de retorno implícito, que permite escribir código más conciso y expresivo. A diferencia de otros lenguajes donde siempre necesitas una declaración return explícita, Rust trata la última expresión de un bloque como su valor de retorno.

Expresiones vs. Sentencias

Para entender el retorno implícito, primero debemos distinguir entre expresiones y sentencias en Rust:

  • Una expresión evalúa a un valor
  • Una sentencia realiza una acción pero no produce un valor

Esta distinción es fundamental para comprender cómo funcionan los retornos en Rust:

fn ejemplo_sentencia() {
    let x = 5; // Esto es una sentencia (no produce un valor)
}

fn ejemplo_expresion() -> i32 {
    5 // Esto es una expresión (produce el valor 5)
}

Funcionamiento del retorno implícito

Cuando escribimos una función con un tipo de retorno, la última expresión del bloque (si no termina con punto y coma) se convierte automáticamente en el valor de retorno:

fn calcular_cuadrado(numero: i32) -> i32 {
    numero * numero // Sin punto y coma - esta expresión es el valor de retorno
}

fn main() {
    let resultado = calcular_cuadrado(4);
    println!("El cuadrado es: {}", resultado); // Imprime: El cuadrado es: 16
}

Observa que la expresión numero * numero no termina con punto y coma. Si añadiéramos un punto y coma, convertiríamos la expresión en una sentencia, lo que causaría un error de compilación:

fn calcular_cuadrado_incorrecto(numero: i32) -> i32 {
    numero * numero; // Error: se esperaba un valor de retorno i32, pero se encontró ()
}

Bloques como expresiones

En Rust, los bloques de código también son expresiones, lo que significa que pueden evaluar a un valor. El valor de un bloque es el valor de su última expresión:

fn calcular_valor(x: i32) -> i32 {
    let resultado = {
        let y = x * 2;
        let z = y + 1;
        z * 2 // Valor del bloque
    };
    
    resultado // Valor de retorno de la función
}

fn main() {
    println!("Resultado: {}", calcular_valor(5)); // Imprime: Resultado: 22
}

En este ejemplo, el bloque entre llaves evalúa a z * 2, que luego se asigna a resultado.

Combinando retorno implícito con condicionales

Podemos combinar el retorno implícito con expresiones condicionales para crear código conciso:

fn numero_a_texto(n: i32) -> &'static str {
    if n == 0 {
        "cero"
    } else if n == 1 {
        "uno"
    } else if n == 2 {
        "dos"
    } else {
        "otro número"
    }
}

fn main() {
    println!("{}", numero_a_texto(1)); // Imprime: uno
    println!("{}", numero_a_texto(5)); // Imprime: otro número
}

La expresión if-else completa es evaluada y su resultado se convierte en el valor de retorno de la función.

Expresión match para retornos

La expresión match es particularmente útil con el retorno implícito, ya que permite manejar múltiples casos de forma elegante:

fn clasificar_numero(n: i32) -> &'static str {
    match n {
        0 => "cero",
        n if n > 0 => "positivo",
        _ => "negativo"
    }
}

fn main() {
    println!("{}", clasificar_numero(10));  // Imprime: positivo
    println!("{}", clasificar_numero(-5));  // Imprime: negativo
    println!("{}", clasificar_numero(0));   // Imprime: cero
}

Cada brazo del match es una expresión cuyo valor puede convertirse en el retorno de la función.

Retorno implícito vs. retorno explícito

Aunque el retorno implícito es elegante, a veces es necesario usar return explícito, especialmente para retornos anticipados:

fn encontrar_primer_par(numeros: &[i32]) -> Option<i32> {
    for &num in numeros {
        if num % 2 == 0 {
            return Some(num); // Retorno anticipado cuando encontramos un número par
        }
    }
    
    None // Retorno implícito si no encontramos ningún número par
}

fn main() {
    let nums = [1, 3, 5, 6, 7, 8];
    match encontrar_primer_par(&nums) {
        Some(n) => println!("El primer número par es: {}", n),
        None => println!("No hay números pares")
    }
}

Retorno implícito en funciones sin valor de retorno

En funciones que no especifican un tipo de retorno (o especifican -> ()), el valor de retorno implícito es la unidad ():

fn saludar(nombre: &str) { // Retorna implícitamente ()
    println!("¡Hola, {}!", nombre);
}

fn saludar_explicito(nombre: &str) -> () { // Equivalente a la anterior
    println!("¡Hola, {}!", nombre);
}

Consideraciones prácticas

El retorno implícito hace que el código sea más conciso y expresivo, pero hay algunas consideraciones importantes:

  • Si una función debe retornar un valor pero la última expresión termina con punto y coma, el compilador mostrará un error
  • Es una buena práctica ser consistente: usa retorno implícito para el flujo normal y return explícito solo para retornos anticipados
  • El retorno implícito funciona con cualquier tipo de expresión, incluyendo llamadas a funciones:
fn obtener_valor() -> i32 {
    calcular_valor(5) // Llamada a otra función como retorno implícito
}

fn calcular_valor(base: i32) -> i32 {
    base * 2
}

Uso con operadores lógicos

Los operadores lógicos también pueden formar parte de expresiones de retorno implícito:

fn es_adulto_y_ciudadano(edad: u8, es_ciudadano: bool) -> bool {
    edad >= 18 && es_ciudadano
}

fn main() {
    let puede_votar = es_adulto_y_ciudadano(20, true);
    println!("¿Puede votar? {}", puede_votar); // Imprime: ¿Puede votar? true
}

El retorno implícito es una característica que hace que el código Rust sea más fluido y natural, permitiéndote expresar la lógica de forma más directa sin la necesidad constante de declaraciones return explícitas.

Scope y shadowing

El scope (ámbito) en Rust define la región del código donde una variable es válida y accesible. Entender cómo funciona el scope es fundamental para escribir programas correctos y evitar errores comunes relacionados con la visibilidad de las variables.

Ámbito de variables

En Rust, las variables tienen un ámbito léxico, lo que significa que son válidas desde el punto donde se declaran hasta el final del bloque donde fueron creadas:

fn main() {
    // La variable 'x' no existe aquí
    
    let x = 10; // 'x' nace aquí
    
    println!("x: {}", x); // Podemos usar 'x'
    
    { // Inicio de un nuevo bloque
        // 'x' sigue siendo accesible aquí
        println!("x dentro del bloque interno: {}", x);
        
        let y = 20; // 'y' nace aquí
        println!("y: {}", y);
    } // 'y' muere aquí al finalizar el bloque
    
    // println!("y: {}", y); // Error: 'y' no existe en este ámbito
    
    println!("x sigue siendo accesible: {}", x);
} // 'x' muere aquí al finalizar el bloque main

Cada par de llaves {} crea un nuevo ámbito anidado. Las variables declaradas en un ámbito interno no son accesibles desde ámbitos externos.

Ámbito en funciones

Cada función tiene su propio ámbito, y los parámetros de la función son variables que pertenecen a ese ámbito:

fn calcular_area(ancho: f64, alto: f64) -> f64 {
    // 'ancho' y 'alto' son válidos dentro de esta función
    let area = ancho * alto;
    // 'area' también es válida solo dentro de esta función
    area // Retornamos el valor de 'area'
}

fn main() {
    let resultado = calcular_area(5.0, 3.0);
    println!("El área es: {}", resultado);
    
    // println!("ancho: {}", ancho); // Error: 'ancho' no existe en este ámbito
    // println!("area: {}", area);   // Error: 'area' no existe en este ámbito
}

Shadowing (sombreado) de variables

El shadowing es una característica de Rust que permite declarar una nueva variable con el mismo nombre que una variable existente. La nueva variable "sombrea" a la anterior, ocultándola temporalmente:

fn main() {
    let mensaje = "hola"; // Tipo &str
    println!("Mensaje original: {}", mensaje);
    
    let mensaje = mensaje.len(); // Tipo usize - sombreamos la variable anterior
    println!("Longitud del mensaje: {}", mensaje);
    
    let mensaje = mensaje * 2; // Tipo usize - sombreamos nuevamente
    println!("Longitud duplicada: {}", mensaje);
}

Este código imprime:

Mensaje original: hola
Longitud del mensaje: 4
Longitud duplicada: 8

El shadowing es diferente de la mutabilidad. Con shadowing, estamos creando una nueva variable que reutiliza el mismo nombre, pero puede tener un tipo diferente.

Ventajas del shadowing

El shadowing es especialmente útil en situaciones donde:

  • Queremos transformar una variable pero mantener el mismo nombre
  • Necesitamos cambiar el tipo de una variable sin crear un nuevo nombre
  • Queremos mantener una variable inmutable después de su inicialización
fn main() {
    // Convertir entrada de usuario a número
    let valor = "42"; // Inicialmente es un &str
    println!("Valor como texto: {}", valor);
    
    // Transformamos a número usando shadowing
    let valor = valor.parse::<i32>().unwrap(); // Ahora es un i32
    println!("Valor como número: {}", valor);
    
    // Podemos hacer cálculos con el valor numérico
    let valor = valor * 2; // Seguimos usando el mismo nombre
    println!("Valor duplicado: {}", valor);
}

Shadowing en bloques anidados

El shadowing también funciona en bloques anidados, donde la variable sombreada solo existe dentro de ese bloque:

fn main() {
    let x = 10;
    println!("x fuera: {}", x); // Imprime: x fuera: 10
    
    {
        let x = x * 2; // Sombreamos 'x' dentro de este bloque
        println!("x dentro: {}", x); // Imprime: x dentro: 20
        
        {
            let x = "texto"; // Sombreamos nuevamente con un tipo diferente
            println!("x más adentro: {}", x); // Imprime: x más adentro: texto
        }
        
        println!("x dentro otra vez: {}", x); // Imprime: x dentro otra vez: 20
    }
    
    println!("x fuera otra vez: {}", x); // Imprime: x fuera otra vez: 10
}

Cuando salimos de un bloque, las variables sombreadas dentro de ese bloque dejan de existir, y las variables del ámbito exterior vuelven a ser visibles.

Shadowing vs. mutabilidad

Es importante distinguir entre shadowing y mutabilidad:

fn main() {
    // Usando shadowing
    let valor = 5;
    let valor = valor + 1; // Creamos una nueva variable con el mismo nombre
    
    // Usando mutabilidad
    let mut contador = 5;
    contador = contador + 1; // Modificamos el valor de la misma variable
}

Las diferencias clave son:

  • Con shadowing, creamos una nueva variable que puede tener un tipo diferente
  • Con mutabilidad (mut), modificamos la misma variable, que debe mantener su tipo

Scope en estructuras de control

Las estructuras de control como if, match, loop, while y for también crean sus propios ámbitos:

fn main() {
    let condicion = true;
    
    if condicion {
        let x = 10; // 'x' solo existe dentro de este bloque 'if'
        println!("x dentro del if: {}", x);
    } else {
        let y = 20; // 'y' solo existe dentro de este bloque 'else'
        println!("y dentro del else: {}", y);
    }
    
    // println!("x: {}", x); // Error: 'x' no existe aquí
    // println!("y: {}", y); // Error: 'y' no existe aquí
}

Aplicación práctica: conversión de unidades

Un ejemplo práctico donde el shadowing resulta útil es en la conversión de unidades:

fn convertir_temperatura() {
    let temperatura = 22.5; // Celsius
    println!("Temperatura en Celsius: {}°C", temperatura);
    
    // Convertimos a Fahrenheit usando el mismo nombre
    let temperatura = temperatura * 9.0 / 5.0 + 32.0;
    println!("Temperatura en Fahrenheit: {}°F", temperatura);
    
    // Convertimos a Kelvin
    let temperatura = (temperatura - 32.0) * 5.0 / 9.0 + 273.15;
    println!("Temperatura en Kelvin: {}K", temperatura);
}

fn main() {
    convertir_temperatura();
}

Este código mantiene el nombre significativo temperatura a través de las diferentes representaciones, lo que hace que el código sea más legible y mantenga su intención clara.

Consideraciones sobre el scope y shadowing

  • Las variables en Rust tienen un ámbito léxico definido por bloques de código
  • El shadowing permite reutilizar nombres de variables para diferentes propósitos
  • El shadowing es diferente de la mutabilidad y permite cambiar el tipo de una variable
  • Cuando una variable sale de su ámbito, Rust libera automáticamente los recursos asociados
  • El shadowing puede mejorar la legibilidad del código al mantener nombres significativos

Entender estos conceptos es esencial para escribir código Rust efectivo y evitar errores comunes relacionados con la visibilidad y el ciclo de vida de las variables.

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 Funciones 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 cómo declarar funciones y utilizar parámetros en Rust.
  • Aprender el concepto de retorno implícito y explícito en funciones.
  • Entender el ámbito (scope) de variables y cómo afecta su visibilidad.
  • Conocer el shadowing y su diferencia con la mutabilidad.
  • Aplicar buenas prácticas en la documentación y organización de funciones.