rust-slices-gestion-memoria

Slices en Rust: referencias seguras a texto y arrays

  • 5 min

Un slice es una referencia a una parte continua de una colección o cadena.

Ya sabemos que el Ownership controla quién limpia la memoria y el Borrowing nos permite prestar datos. A veces no queremos prestar todo el objeto, sino solo una parte.

Imagina que tienes un libro enorme (String).

  • Si pasas el libro por valor (Move), lo regalas.
  • Si pasas una referencia al libro (&String), dejas que lo lean entero.
  • ¿Y si solo quieres que lean el capítulo 3?

Una opción sería copiar el texto del capítulo 3 en una variable nueva, pero eso duplica memoria. En Rust, podemos usar slices (rebanadas) para crear una vista prestada.

Un slice es una referencia a una secuencia contigua de elementos en lugar de a la colección entera. Es como una “ventana” sobre los datos originales: no posee los elementos y guarda un puntero junto con una longitud.

String slices (&str)

El uso más común de los slices es con cadenas de texto. Seguramente te has fijado en que a veces usamos String y otras veces &str. Vamos a entender la diferencia por fin.

Supongamos que tenemos un String en el Heap:

fn main() {
    let s = String::from("Hola Mundo");

    let hola = &s[0..4];  // Slice de los índices 0, 1, 2, 3
    let mundo = &s[5..10]; // Slice de los índices 5 al 9

    println!("Primera parte: {}", hola);
}
Copied!

La sintaxis [start..end] crea un rango.

  • hola es un slice &str.
  • Internamente, hola contiene dos datos en el Stack:
  1. Un puntero al byte donde empieza la ‘H’.
  2. Una longitud (4 bytes).

No hemos copiado “Hola”. El slice sigue apuntando a los bytes que pertenecen a s, por lo que el compilador impide modificar de forma incompatible la cadena original mientras usemos esa referencia.

Atajos de sintaxis Rust es inteligente con los rangos:

  • [0..2] es igual a [..2] (desde el principio).
  • [3..len] es igual a [3..] (hasta el final).
  • [..] es un slice de todo el contenido.

Literales de str

Aquí viene una revelación importante. Cuando escribes esto:

let s = "Hola mundo";
Copied!

¿De qué tipo es s? No es un String. Es un &str.

Es un &'static str: una referencia a bytes UTF-8 incluidos en los datos estáticos del programa y disponibles durante toda su ejecución. Por eso un literal de cadena es inmutable y no necesita una reserva dinámica.

String vs &str

Esta es una de las dudas más frecuentes.

  • String: Es propietario de un búfer en el Heap. Puede crecer, encogerse y modificarse, y reservar memoria tiene un coste.
  • &str: Es una vista (Slice). Es ligero (solo puntero + longitud). No es dueño de los datos. Normalmente es lo que quieres pasar como argumento a tus funciones.

Criterio práctico: Si escribes una función que solo necesita leer un texto, usa &str como parámetro, no &String.

// ❌ Restrictivo: solo acepta Strings del Heap
fn saludar(nombre: &String) { ... }

// ✅ Flexible: acepta String, partes de String y literales
fn saludar(nombre: &str) { ... }
Copied!

Otros slices (&[T])

Los slices no son exclusivos de las cadenas de texto. Funcionan con secuencias contiguas, como los arrays y los vectores.

Imagina que tienes un array de 5 números y quieres pasarle a una función solo los 3 del medio.

fn main() {
    let a = [10, 20, 30, 40, 50];

    // Tipo: &[i32]
    let slice = &a[1..4];

    assert_eq!(slice, &[20, 30, 40]);
}
Copied!

El tipo se denota como &[T], donde T es el tipo de dato (en este caso &[i32]).

Esto es tremendamente potente para escribir APIs eficientes. Si tu función acepta &[i32], puedes llamarla pasando:

  1. Una referencia a un array entero (&[1, 2]).
  2. Una referencia a un vector (&vec).
  3. Un slice parcial de cualquiera de los dos.

Slices y el Borrow Checker

Como los slices son referencias, están sujetos a las reglas de Borrowing que vimos en el artículo anterior. Y aquí es donde Rust te salva de errores graves de memoria.

Imagina este escenario (común en C++):

  1. Tienes un Vector.
  2. Creas un puntero al primer elemento.
  3. Destruyes el vector o fuerzas una reasignación de su búfer.
  4. Intentas leer el puntero.
  5. Resultado en C++: Use After Free (Crash o vulnerabilidad).

Veamos qué pasa en Rust:

fn main() {
    let mut s = String::from("hola mundo");

    let palabra = &s[..4]; // Préstamo inmutable (slice)

    s.clear(); // ❌ ERROR DE COMPILACIÓN

    println!("La palabra es: {}", palabra);
}
Copied!

El error será: “cannot borrow s as mutable because it is also borrowed as immutable”.

  1. palabra pide prestado s (inmutable).
  2. s.clear() intenta pedir prestado s (mutable) para borrarlo.
  3. Rust dice: “¡Alto!”. Mientras palabra esté viva (se usa en el println), no puedes modificar s. palabra dejaría de apuntar a datos válidos.

El sistema de préstamos garantiza que un slice de código seguro no quede apuntando a memoria liberada o invalidada.

Cuidado con UTF-8

Una advertencia final específica de los Strings. Rust usa UTF-8 por defecto. Algunos caracteres (como los emojis o letras con tilde) ocupan más de 1 byte.

Si intentas hacer un slice que corta un carácter por la mitad, Rust entrará en pánico (panic!) en tiempo de ejecución.

let s = "Mañana"; // La 'ñ' ocupa 2 bytes
// let slice = &s[0..3]; // 💀 Peligro: Podrías estar partiendo la ñ
Copied!

Por suerte, es raro tener que manipular índices de strings a mano en Rust. Solemos usar iteradores o métodos que manejan UTF-8 correctamente.