Ir al contenido

Slices en Rust

Un slice (porción) te permite tomar prestada una parte de un array o de un vector, una especie de “ventana” a una parte de los datos, sin copiarlos ni quedarte con su ownership. Si ya usaste el borrowing (préstamo) con un solo valor, un slice es la misma idea aplicada a un rango de elementos.

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // los elementos de los índices 1, 2 y 3
println!("{:?}", middle);
}

Salida:

[20, 30, 40]
▶ Pruébalo tú mismo

1..4 es el mismo tipo de rango que viste en los bucles for: empieza en el índice 1 y se detiene antes del índice 4, así que obtienes los índices 1, 2 y 3.

Algunos atajos que vas a ver a menudo:

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // desde el principio hasta el índice 3 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // desde el índice 2 hasta el final -> [30, 40, 50]
println!("{:?}", &numbers[..]); // todo completo -> [10, 20, 30, 40, 50]
}
▶ Pruébalo tú mismo

En realidad ya has estado usando slices: ¡&str es un slice de un String (o de otra cadena)! Por eso los dos tipos se llevan tan bien:

fn main() {
let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3];
println!("{first_word}"); // "The"
}
▶ Pruébalo tú mismo

Aquí dejamos el texto en inglés a propósito. Los rangos de un &str cuentan bytes, no letras, y caracteres como la “á” o la “ñ” ocupan 2 bytes en UTF-8. Con un texto en español, un rango como 0..3 podría caer en medio de una letra y el programa haría panic. Lo verás con más detalle en la página de Strings.

Podrías pensar en resolver esto copiando la parte que quieres en un Vec o un String nuevo. Los slices evitan eso por completo: solo apuntan a una parte de los datos que ya existen, igual que una referencia (&) apunta a un único valor. Eso significa que no hay memoria extra, no hay copias, y siguen valiendo las reglas de borrowing que ya conoces de la página de Borrowing.

Los slices son especialmente útiles como parámetros de función, porque aceptan tanto arrays como vectores:

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ Pruébalo tú mismo

&[i32] acepta un slice tanto de un array como de un Vec: una sola función, y las dos llamadas funcionan.

Modificar datos a través de un slice mutable

Sección titulada «Modificar datos a través de un slice mutable»

Hasta ahora, nuestros slices solo han leído datos. Un slice mutable, &mut [T], permite que una función modifique los elementos que recibió, pero solo esos:

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // solo los índices 1 y 2
println!("{:?}", scores);
double_all(&mut scores); // el vector completo
println!("{:?}", scores);
}

Salida:

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ Pruébalo tú mismo

La primera llamada pasa &mut scores[1..3], así que double_all solo ve los índices 1 y 2. Todo lo que queda fuera de ese rango está a salvo. Dentro de la función, el slice vuelve a empezar en el índice 0: para double_all, es simplemente una lista de dos números.

Esto es seguro gracias a una regla que conocerás a fondo en la página de Borrowing: mientras la función tiene un slice &mut, nada más puede usar esos elementos, así que dos partes de tu código nunca pueden modificarlos a la vez. Además, el vector tiene que declararse con mut, o Rust no te dejará tomarlo prestado de forma mutable.

Crea el vector vec![3, 6, 9, 12, 15, 18]. Imprime su primera mitad y su segunda mitad como dos slices separados. Pista: calcula let mid = numbers.len() / 2; y usa ..mid y mid...

Ver solución
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("Primera mitad: {:?}", first_half);
println!("Segunda mitad: {:?}", second_half);
}

Salida:

Primera mitad: [3, 6, 9]
Segunda mitad: [12, 15, 18]
▶ Pruébalo tú mismo

Como mid se calcula a partir de len(), esto sigue funcionando si añades o quitas números. Con una longitud impar, el elemento que sobra acaba en la segunda mitad.

Escribe una función largest(numbers: &[i32]) -> i32 que devuelva el número más grande. Llámala con un array, con un vector y con un slice que tenga solo el primer elemento del vector. Pista: empieza con numbers[0] y recorre el resto, como harías con un array.

Ver solución
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

Salida:

9
35
10
▶ Pruébalo tú mismo

Al recorrer un slice, recibes una referencia a cada elemento (&i32), no el número en sí. *n significa “el valor al que apunta n”, así que *n > biggest compara dos números normales. Esta versión hace panic con un slice vacío, porque numbers[0] no existe. Más adelante aprenderás a devolver un Option para ese caso.

  • Un slice (&[T]) toma prestada una parte de un array o vector sin copiarla.
  • 1..4 selecciona los índices del 1 al 3; omite uno de los lados de .. para ir desde el principio o hasta el final.
  • &str es en sí mismo un slice de texto; por eso las cadenas y los slices se comportan de forma tan parecida.
  • Usa &[T] como tipo de parámetro cuando quieras que una función acepte tanto arrays como vectores.
  • Un slice mutable (&mut [T]) permite que una función modifique solo los elementos que recibió.

Comprueba lo aprendido

1. ¿Qué te permite hacer un slice con una parte de un array o vector?

2. ¿Qué índices incluye &numbers[1..4]?

3. ¿Por qué &[i32] es una buena elección como tipo de parámetro de una función?

Puntuación: 0 / 3