Tipos compuestos primitivos: tuplas y arrays
Las tuplas heterogéneas y su desestructuración, y los arrays de tamaño fijo [T; N] con el tamaño en el tipo, frente a los slices y Vec que llegan más adelante.
Los escalares guardan un valor; los tipos compuestos agrupan varios. Rust tiene dos compuestos primitivos, ambos de tamaño fijo conocido en compilación y ambos alojados en la pila: la tupla, que reúne valores de tipos distintos, y el array, que reúne valores del mismo tipo. Entenderlos bien —y ver por qué su tamaño vive en el tipo— prepara el terreno para los slices y Vec que vendrán después.
- Construir y desestructurar tuplas, incluida la tupla unidad
(). - Acceder a campos de tupla por posición y anidar patrones.
- Declarar arrays
[T; N]con el tamaño en el tipo y la sintaxis de repetición. - Entender la comprobación de límites y por qué array no es slice ni
Vec.
Tuplas: agrupar tipos distintos
Una tupla junta un número fijo de valores, posiblemente de tipos diferentes, en un único valor compuesto. Su tipo es la secuencia de los tipos de sus elementos:
fn main() {
let punto: (i32, f64, char) = (42, 3.14, 'R');
// acceso por posición con .índice:
println!("{} {} {}", punto.0, punto.1, punto.2);
}
La forma idiomática de leer una tupla no es por índices, sino por desestructuración: un patrón let que reparte los campos en nombres:
fn main() {
let (x, y, z) = (42, 3.14, 'R'); // un let, tres bindings
println!("{x} {y} {z}");
let ((a, b), c) = ((1, 2), 3); // los patrones anidan
let (_, importante) = (0, 99); // _ descarta lo que no usas
println!("{a} {b} {c} {importante}");
}
El uso más frecuente de las tuplas es devolver varios valores de una función sin declarar un struct para algo efímero:
fn dividir(dividendo: i32, divisor: i32) -> (i32, i32) {
(dividendo / divisor, dividendo % divisor) // cociente y resto
}
fn main() {
let (cociente, resto) = dividir(17, 5);
println!("{cociente} resto {resto}"); // 3 resto 2
}
La tupla vacía () se llama tipo unidad y su único valor es (). Es lo que devuelve toda expresión que no produce un valor útil: una función sin -> tipo devuelve (), y println! como sentencia se evalúa a (). No es un null ni un void mágico: es un valor de pleno derecho que ocupa cero bytes. Cuando estudies genéricos, () será la elección natural para “no me importa este parámetro de tipo”.
Arrays: mismo tipo, tamaño fijo en el tipo
Un array agrupa N elementos del mismo tipo T, y su tipo se escribe [T; N]. Lo decisivo: el tamaño N forma parte del tipo. [i32; 3] y [i32; 4] son tipos distintos e incompatibles, y N debe conocerse en compilación.
fn main() {
let dias: [&str; 3] = ["lun", "mar", "mié"];
let ceros = [0; 5]; // sintaxis de repetición: [0,0,0,0,0]
let bytes: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
println!("{}", dias[0]); // indexación con usize, base 0
println!("{}", dias.len()); // 3, conocido en compilación
}
La sintaxis [valor; N] inicializa N copias del valor —útil para buffers—. Los arrays viven en la pila, sin asignación de heap, lo que los hace baratísimos, y son iterables directamente. Su tamaño fijo los hace ideales para datos cuya cardinalidad es constante y sabida: los meses del año, una matriz 4x4, una tabla de búsqueda.
La comprobación de límites
Indexar fuera de rango no es comportamiento indefinido como en C: Rust comprueba los límites y provoca un panic controlado si te sales. La seguridad de memoria no se negocia.
fn main() {
let a = [10, 20, 30];
let i = 5;
println!("{}", a[i]); // panic: index out of bounds: len is 3 but index is 5
}
Si el índice es una constante que el compilador puede evaluar, el error se detecta incluso en compilación. Para acceso sin riesgo de panic existe .get(i), que devuelve Option<&T>: None en vez de reventar.
let a = [10, 20, 30];
match a.get(5) {
Some(v) => println!("{v}"),
None => println!("fuera de rango, pero sin panic"),
}
flowchart TD
A[Datos agrupados] --> B{Tipos iguales}
B -->|No, heterogeneo| C[Tupla T1 T2 T3]
B -->|Si, homogeneo| D{Tamano fijo en compilacion}
D -->|Si, en la pila| E[Array T por N]
D -->|No, crece en heap| F[Vec T mas adelante]
E --> G[Prestar como slice ref T]Por qué array no es slice ni Vec
El array parece rígido comparado con las colecciones dinámicas, y esa rigidez es el punto. Conviene situarlo frente a lo que estudiarás después:
- Un array
[T; N]tiene tamaño fijo en el tipo y vive en la pila. No puede crecer. - Un slice
&[T]es una vista prestada sobre una secuencia contigua: no posee los datos, y su longitud no está en el tipo sino que se lleva en tiempo de ejecución (es un puntero gordo: dirección + longitud). Un array se convierte (coacciona) en slice al prestarlo:&arro&arr[1..3]. - Un
Vec<T>posee sus datos en el heap y puede crecer y encogerse.
fn suma(datos: &[i32]) -> i32 { // acepta slice: sirve a arrays y Vec
datos.iter().sum()
}
fn main() {
let arr = [1, 2, 3, 4];
println!("{}", suma(&arr)); // el array se presta como &[i32]
}
Esa función suma toma un &[i32] y por eso funciona con un array y con un Vec sin cambiar nada: el slice es el denominador común. Ese es el patrón que verás una y otra vez —poseer con un tipo concreto, prestar como slice— y por eso empezar por el array de tamaño fijo es empezar por los cimientos.
Que [i32; 3] y [i32; 4] sean tipos distintos incomoda al principio —“¿por qué el compilador es tan quisquilloso con la longitud?”— hasta que ves lo que compra. El tamaño de un array es una parte de su tipo, lo que significa que el compilador conoce la longitud de cada array sin ejecutar nada, puede colocarlo entero en la pila sin tocar el heap, puede desenrollar bucles, eliminar comprobaciones de límites cuando el índice es demostrablemente válido, y rechazar en compilación un montón de errores de longitud que en otros lenguajes solo aparecen al ejecutar. Es la misma filosofía que recorre todo Rust: subir información al sistema de tipos para que el compilador demuestre lo que en otros lenguajes solo se espera. La tupla hace lo análogo con la heterogeneidad —agrupa tipos distintos con aridad fija y desestructurable— y el array con la homogeneidad y la cardinalidad. Cuando más adelante cambies a slices y Vec, no estarás abandonando estos primitivos: estarás pagando conscientemente el coste de mover parte de esa información del tipo al tiempo de ejecución, a cambio de flexibilidad. Entender el array de tamaño fijo es entender qué renuncias exactamente cada vez que eliges un Vec.
- Escribe una función que devuelva
(i32, i32, i32)con mínimo, máximo y suma de tres números, y desestructura el resultado en tres nombres. - Desestructura una tupla anidada
((1, 2), (3, 4))en cuatro variables con un sololet. - Crea
[0u8; 8]con la sintaxis de repetición e imprime sulen. Cambia el tamaño y observa que el tipo cambia. - Provoca un panic de índice fuera de rango y luego evítalo reescribiéndolo con
.get(i)y unmatch. - Escribe
fn media(d: &[f64]) -> f64y llámala con un array y (mentalmente) con unVec, comprobando que el slice unifica ambos.