En esta página

Tipos de datos compuestos y funciones

14 min lectura TextoCap. 1 — Fundamentos de Rust

Tipos de datos compuestos

Rust tiene dos tipos de datos compuestos integrados en el lenguaje: tuplas y arrays. Ambos tienen tamaño fijo conocido en tiempo de compilación y viven en la pila (stack).

Tuplas

Una tupla agrupa valores de tipos distintos. Su tamaño y tipos son parte de la firma de tipo de la tupla:

fn main() {
    // Tupla con tres tipos distintos
    let persona: (String, u8, f64) = (
        String::from("Ana García"),
        30,
        1.75,
    );
    
    // Acceso por índice con punto
    println!("Nombre: {}", persona.0);
    println!("Edad: {}", persona.1);
    println!("Altura: {:.2}m", persona.2);
    
    // Desestructuración (destructuring)
    let (nombre, edad, altura) = persona;
    println!("{nombre} tiene {edad} años y mide {altura:.2}m");
    
    // Tupla vacía: () se llama "unit" — el tipo de retorno implícito de funciones sin retorno
    let nada: () = ();
    
    // Tupla con un solo elemento (nota la coma)
    let singleton = (42,);
    println!("Singleton: {}", singleton.0);
}

Las tuplas son especialmente útiles para retornar múltiples valores desde una función, sin necesidad de crear un struct.

Arrays

Los arrays en Rust tienen longitud fija conocida en tiempo de compilación. Todos los elementos deben ser del mismo tipo:

fn main() {
    // Declaración explícita: [tipo; longitud]
    let dias: [&str; 7] = [
        "Lunes", "Martes", "Miércoles", "Jueves",
        "Viernes", "Sábado", "Domingo"
    ];
    
    // Inicializar todos los elementos con el mismo valor
    let zeros: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
    let unos = [1_u8; 10];        // [1, 1, 1, ..., 1] x10
    
    // Acceso por índice — Rust verifica límites en tiempo de ejecución
    println!("Primer día: {}", dias[0]);
    println!("Último día: {}", dias[6]);
    
    // Longitud
    println!("Días en la semana: {}", dias.len());
    
    // Iteración
    for dia in &dias {
        println!("{dia}");
    }
    
    // Slices: referencias a una porción del array
    let fin_de_semana: &[&str] = &dias[5..7];
    println!("Fin de semana: {:?}", fin_de_semana);
}

La diferencia entre arrays y vectores (Vec<T>) es que los arrays tienen tamaño fijo. Los vectores pueden crecer y se almacenan en el heap. Para la mayoría de colecciones dinámicas usarás Vec<T>.

Funciones en Rust

Las funciones se declaran con la palabra clave fn. A diferencia de algunos lenguajes, todos los parámetros requieren anotación de tipo explícita — Rust no infiere tipos de parámetros para mantener las firmas de función siempre claras y auto-documentadas.

// Función sin parámetros ni retorno (retorna implícitamente ())
fn saludar() {
    println!("¡Hola desde una función!");
}

// Función con parámetros y tipo de retorno
fn elevar_al_cuadrado(n: i32) -> i32 {
    n * n // Sin punto y coma: expresión de retorno
}

// Múltiples parámetros
fn formatear_temperatura(celsius: f64, unidad: &str) -> String {
    match unidad {
        "F" => format!("{:.1}°F", celsius * 1.8 + 32.0),
        "K" => format!("{:.1}K", celsius + 273.15),
        _   => format!("{:.1}°C", celsius),
    }
}

fn main() {
    saludar();
    
    let n = 5;
    println!("{n}² = {}", elevar_al_cuadrado(n));
    
    println!("{}", formatear_temperatura(100.0, "F"));
    println!("{}", formatear_temperatura(0.0, "K"));
}

Expresiones vs declaraciones

Esta distinción es fundamental en Rust y diferente a muchos otros lenguajes:

  • Una declaración realiza una acción pero no produce un valor. Termina con ;
  • Una expresión evalúa a un valor. No termina con ;
fn main() {
    // Declaración: let es una declaración
    let x = 5;
    
    // Expresión: el bloque { } es una expresión
    let y = {
        let temp = x * 2;
        temp + 1  // Sin ;: esto es el valor del bloque
    };
    println!("y = {y}"); // y = 11
    
    // if también es una expresión en Rust
    let es_par = if x % 2 == 0 { "par" } else { "impar" };
    println!("x es {es_par}");
    
    // Comparar con:
    let z = {
        let temp = x * 2;
        temp + 1; // Con ;: el bloque produce ()
    };
    // z es de tipo ()
}

Esta característica hace que Rust sea muy expresivo. Puedes usar if, bloques, match y casi cualquier construcción del lenguaje como parte de una expresión.

Retorno anticipado con `return`

Usa return cuando necesitas salir de una función antes de llegar al final:

fn dividir(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        return f64::NAN; // Retorno anticipado
    }
    
    // Retorno normal al final
    a / b
}

fn clasificar_numero(n: i32) -> &'static str {
    if n < 0 {
        return "negativo";
    }
    if n == 0 {
        return "cero";
    }
    "positivo" // Retorno implícito al final
}

fn main() {
    println!("{}", dividir(10.0, 3.0));
    println!("{}", dividir(10.0, 0.0));
    
    for n in [-5, 0, 7] {
        println!("{n} es {}", clasificar_numero(n));
    }
}

Funciones con múltiples valores de retorno

Rust no tiene retornos múltiples nativos, pero las tuplas los emulan perfectamente:

fn estadisticas(datos: &[f64]) -> (f64, f64, f64) {
    let n = datos.len() as f64;
    let suma: f64 = datos.iter().sum();
    let promedio = suma / n;
    
    let min = datos.iter().cloned().fold(f64::INFINITY, f64::min);
    let max = datos.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    
    (promedio, min, max)
}

fn main() {
    let mediciones = [23.5, 18.2, 31.7, 25.0, 19.8, 27.3];
    let (promedio, min, max) = estadisticas(&mediciones);
    
    println!("Promedio: {promedio:.2}");
    println!("Mínimo: {min:.2}");
    println!("Máximo: {max:.2}");
    println!("Rango: {:.2}", max - min);
}

Funciones anidadas y closures

Las funciones pueden anidarse dentro de otras funciones. Son útiles para encapsular lógica auxiliar:

fn procesar_datos(entrada: &[i32]) -> Vec<i32> {
    // Función auxiliar anidada
    fn es_valido(n: &i32) -> bool {
        *n > 0 && *n < 1000
    }
    
    fn transformar(n: i32) -> i32 {
        n * 2 + 1
    }
    
    entrada
        .iter()
        .filter(|n| es_valido(n))
        .map(|&n| transformar(n))
        .collect()
}

fn main() {
    let datos = [-5, 10, 500, 1500, 42, 0, 99];
    let resultado = procesar_datos(&datos);
    println!("{:?}", resultado);
}

El tipo `!` (never)

Rust tiene un tipo especial ! llamado "never" que representa funciones que nunca retornan:

fn entrar_en_panico() -> ! {
    panic!("¡Algo salió muy mal!");
}

fn bucle_infinito() -> ! {
    loop {
        // Esto nunca termina
    }
}

fn main() {
    // El tipo ! es compatible con cualquier tipo esperado
    let x: i32 = if true { 42 } else { panic!("imposible") };
    println!("{x}");
}

Las macros panic!, todo!, unimplemented! y unreachable! todas tienen tipo !.


Con estos fundamentos sobre tipos compuestos y funciones, estás listo para el concepto más importante de Rust: ownership, el sistema que hace posible la seguridad de memoria sin recolector de basura.

Expresiones vs declaraciones
En Rust casi todo es una expresión que produce un valor. Un bloque { ... } es una expresión cuyo valor es la última línea sin punto y coma. Añadir ; convierte una expresión en una declaración que produce ().
No mezcles return implícito y explícito
Usa return solo para retornos anticipados (early return). La última línea del cuerpo de una función sin ; es el retorno normal. Mezclarlos en la misma posición es redundante y no idiomático en Rust.
rust
// Las funciones se declaran con fn
// Los parámetros SIEMPRE requieren anotación de tipo
fn sumar(a: i32, b: i32) -> i32 {
    a + b  // Sin punto y coma: expresión de retorno
}

fn calcular_area(base: f64, altura: f64) -> f64 {
    let area = base * altura / 2.0;
    area  // retorno implícito
}

// Puede retornar múltiples valores con tupla
fn min_max(lista: &[i32]) -> (i32, i32) {
    let min = *lista.iter().min().unwrap();
    let max = *lista.iter().max().unwrap();
    (min, max)
}

fn main() {
    let resultado = sumar(3, 7);
    println!("3 + 7 = {resultado}");

    let area = calcular_area(5.0, 4.0);
    println!("Área = {area:.2}");

    let nums = [3, 1, 4, 1, 5, 9, 2, 6];
    let (min, max) = min_max(&nums);
    println!("Min = {min}, Max = {max}");
}