Tuplas, arrays, slices y primeros préstamos en Rust — Capítulo 6

De tuplas a colecciones dinámicas


En el contexto de un libro sobre programación en Rust, este capítulo se centra en las estructuras de datos fundamentales que permiten agrupar y manipular elementos de manera eficiente. Estas construcciones son esenciales para modelar datos compuestos y colecciones, proporcionando una base sólida antes de explorar conceptos más avanzados como la gestión de memoria y los patrones de concurrencia. Su comprensión facilita el diseño de programas seguros y expresivos, alineándose con los principios de Rust de prevenir errores en tiempo de compilación.

Tuplas

Las tuplas en Rust representan una forma sencilla de agrupar valores de tipos posiblemente heterogéneos en una estructura única y fija. Se declaran mediante paréntesis que encierran los valores separados por comas, y su tipo se infiere o se anota explícitamente como (T1, T2, ..., Tn), donde cada Ti es un tipo. Esta construcción es similar a las tuplas en lenguajes como Python o Haskell, pero en Rust enfatiza la inmutabilidad por defecto y la verificación estática de tipos.

Por ejemplo, una tupla que combina un entero y una cadena se define así:

let punto: (i32, f64, &str) = (5, 3.14, "origen");

El acceso a los elementos se realiza mediante notación de punto seguida del índice, comenzando en cero: punto.0 devuelve el primer elemento. Las tuplas unitarias, como (), representan el tipo vacío y se utilizan en contextos donde no se requiere valor de retorno. Un aspecto sutil es que las tuplas no permiten mutación directa de sus componentes a menos que la tupla completa sea mutable; por instancia, si se declara let mut coords = (1, 2);, entonces coords.0 = 3; es válido.

En casos borde, una tupla con un solo elemento debe incluir una coma para distinguirla de una expresión entre paréntesis: (42,) es una tupla unitaria, mientras que (42) es simplemente el valor 42. Esta distinción previene ambigüedades en la sintaxis. Las tuplas son útiles para retornos múltiples de funciones, como en:

fn dividir(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}

Aquí, el resultado se desestructura fácilmente: let (cociente, resto) = dividir(10, 3);No se permite desestructuración parcial sin patrones completos, lo que asegura exhaustividad en el manejo de datos.

Arrays [T; N]

Los arrays en Rust son colecciones de longitud fija de elementos del mismo tipo, denotados como [T; N], donde T es el tipo de los elementos y N es una constante de tiempo de compilación que indica el tamaño. Esta estructura difiere de arrays en C, donde el tamaño puede ser dinámico pero propenso a errores de desbordamiento; en Rust, el tamaño fijo garantiza verificaciones en tiempo de compilación.

La inicialización se realiza con corchetes: let numeros: [i32; 3] = [1, 2, 3];. Para arrays grandes con valores repetidos, se usa la sintaxis [valor; N], como [0; 100] para un array de cien ceros. El acceso se hace mediante índices: numeros[0], y cualquier intento de acceso fuera de límites genera un error en tiempo de compilación si es detectable, o un pánico en tiempo de ejecución de lo contrario.

Un detalle importante es que los arrays implementan el trait Copy si sus elementos lo hacen, permitiendo copias implícitas. Por ejemplo:

let a = [1, 2, 3];
let b = a;  // Copia, no movimiento, si i32 es Copy 

Sin embargo, arrays de más de 32 elementos no implementan Copy por defecto, requiriendo clonación explícita. En comparaciones con otros lenguajes, los arrays de Rust se asemejan a los de Go en su fijeza, pero incorporan chequeos de seguridad integrados. Para iteración, se puede usar un bucle for:

for &num in numeros.iter() {
    // Procesar num
}

Esto introduce una vista inmutable, aunque el capítulo posterior profundizará en iteradores. Los arrays son ideales para datos de tamaño conocido, como matrices fijas.

Slices &[T]

Los slices proporcionan una vista referencial sobre una porción contigua de una colección, como un array o un vector, sin poseer los datos subyacentes. Se denotan como &[T] para slices inmutables o &mut [T] para mutables, y se crean mediante el operador de rango .. aplicado a una referencia. Esta abstracción es comparable a las slices en Go o las vistas en Python, pero en Rust asegura que no se modifiquen datos sin permiso explícito.

Por instancia, dado un array let arr = [1, 2, 3, 4];, un slice se obtiene como &arr[1..3], que representa [2, 3]. La sintaxis de rangos incluye .. para completo, start.. para desde inicio hasta fin, ..end para desde cero hasta end-1, y start..end para subrangos. Los índices deben ser válidos; de lo contrario, se produce un pánico en tiempo de ejecución.

Los slices permiten acceso indexado similar a arrays: slice[0]. Un ejemplo mínimo ilustra su uso en funciones:

fn suma_slice(s: &[i32]) -> i32 {
    let mut total = 0;
    for &num in s.iter() {
        total += num;
    }
    total
}

Aquí, s es una referencia inmutable, permitiendo leer sin alterar el original. Para mutación, se usa &mut [T]:

fn incrementar_slice(s: &mut [i32]) {
    for num in s.iter_mut() {
        *num += 1;
    }
}

La desreferenciación con * es necesaria para mutar elementos. Los slices facilitan el paso de subcolecciones sin copias, mejorando la eficiencia.

Vec Básico

Vec<T> es una colección dinámica de elementos del mismo tipo, similar a std::vector en C++ o listas en Python, pero con garantías de seguridad en Rust. Se crea con Vec::new() o el macro vec!, como let mut v: Vec<i32> = vec![1, 2, 3];. La mutabilidad es requerida para operaciones que alteren el tamaño, como push o pop.

Los métodos básicos incluyen push para añadir al final, pop para remover y retornar el último elemento, y len para obtener la longitud. Por ejemplo:

let mut v = Vec::new();
v.push(5);
v.push(6);
let ultimo = v.pop();  // Some(6)

El acceso se realiza con &v[i] para inmutable o &mut v[i] para mutable, pero se prefiere get para manejo seguro: v.get(0) retorna Option<&T>Índices inválidos con [] causan pánico, mientras get retorna None.

Vec<T> se expande automáticamente, reallocando memoria si es necesario, lo que lo hace adecuado para colecciones de tamaño variable. Un uso básico en iteración:

for &item in &v {
    // Procesar item
}

Esto itera sobre referencias inmutables. En comparación con arrays, Vec<T> ofrece flexibilidad a costa de overhead de heap, pero mantiene chequeos de límites.

Introducción a Préstamos con Slices y Vec

Los préstamos en Rust permiten referencias seguras a datos sin transferir propiedad, introducidos aquí mínimamente para slices y Vec<T>. Un préstamo inmutable &T permite lectura múltiple, mientras &mut T permite modificación exclusiva. Estas referencias se aplican a slices y vectores para pasar datos a funciones sin copias.

Por ejemplo, una función que toma un slice prestado:

fn imprimir_vec(v: &Vec<i32>) {
    for &num in v {
        println!("{}", num);
    }
}

Aquí, v es un préstamo inmutable, permitiendo acceso sin mutar el vector original. Para mutación:

fn agregar_elemento(v: &mut Vec<i32>, valor: i32) {
    v.push(valor);
}

Solo un préstamo mutable puede existir a la vez por ámbito, previniendo aliasing. Esto asegura que no haya lecturas concurrentes durante mutaciones. En slices, los préstamos permiten subvistas: let s = &v[1..]; crea un slice prestado.

Los préstamos expiran al final del ámbito, liberando el acceso. Un caso sutil: préstamos anidados, como prestar un slice de un vector prestado, son válidos mientras no violen la exclusividad mutable. Esta mecánica habilita el uso de funciones y métodos sin bloquear el flujo del programa, preparando el terreno para patrones más complejos.

Con estos fundamentos en estructuras de datos y préstamos básicos establecidos, el siguiente capítulo explorará la propiedad y el movimiento en Rust, extendiendo estas ideas para manejar recursos de manera segura y eficiente.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio