Aprende Rust

Lección 08 de 26 Ownership y préstamos

Slices y el tipo &str

  • 4 min de lectura
  • 3 ejercicios
  1. Un String guarda el texto como bytes seguidos. Cada byte tiene un índice, empezando por el 0.
  2. &s[0..4] es un slice: una vista de los bytes del 0 al 3. El final, el 4, no se incluye.
  3. &s[5..10] ve "mundo". Un slice no copia el texto: guarda un puntero y una longitud. Su tipo es &str.
  4. Con arrays funciona igual: &numeros[1..4] es un &[i32] que ve el 20, el 30 y el 40.
1/4

Un slice (porción) es una referencia a una parte contigua de una colección: unas letras de un texto o unos elementos de un array. Como cualquier referencia, no tiene la propiedad de los datos: solo los mira.

Son tan habituales que el tipo de texto que más vas a usar en Rust, &str, es en realidad un slice. Para entender por qué existen, empieza por un problema concreto.

El problema: devolver un índice

Quieres una función que reciba un texto y devuelva su primera palabra. Sin slices, lo más que puedes devolver es la posición donde acaba:

fn main() {
    let mut frase = String::from("hola mundo");
    let fin = primera_palabra(&frase); // 4

    frase.clear(); // vacía el String: ahora es ""

    // fin sigue valiendo 4, pero ya no hay ninguna palabra
    println!("La primera palabra acababa en {fin}");
}

fn primera_palabra(s: &String) -> usize {
    let bytes = s.as_bytes(); // el texto como array de bytes
    for i in 0..bytes.len() {
        if bytes[i] == b' ' { // b' ' es el byte del espacio
            return i;
        }
    }
    s.len() // sin espacios: todo el texto es una palabra
}

Funciona, pero el número fin no está conectado con frase. Si el texto cambia, el índice queda desincronizado y nadie te avisa. Con slices, el compilador sí lo hará.

Slices de String

Un slice de un String se crea con un rango entre corchetes:

fn main() {
    let s = String::from("hola mundo");
    let hola = &s[0..4];
    let mundo = &s[5..10];
    println!("{hola} | {mundo}");
}

&s[inicio..fin] toma los bytes desde inicio hasta fin, sin incluir fin. El slice no copia el texto: solo guarda un puntero al primer byte y una longitud.

El rango tiene atajos para empezar desde el principio o llegar hasta el final:

SliceEquivale aCon "hola mundo"
&s[..4]&s[0..4]"hola"
&s[5..]&s[5..s.len()]"mundo"
&s[..]&s[0..s.len()]"hola mundo" (todo)

primera_palabra con slices

Ahora la función puede devolver directamente la palabra. El tipo de un slice de texto se escribe &str:

fn primera_palabra(s: &String) -> &str {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}

El slice está ligado al String original, porque es un préstamo. Si intentas vaciar el texto mientras la palabra sigue en uso, el borrow checker lo detecta:

fn main() {
    let mut frase = String::from("hola mundo");
    let palabra = primera_palabra(&frase);
    frase.clear(); // necesita un préstamo mutable...
    println!("La primera palabra es: {palabra}"); // ...pero palabra sigue en uso
}

fn primera_palabra(s: &String) -> &str {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
    }
    &s[..]
}
error[E0502]: cannot borrow `frase` as mutable because it is also borrowed as immutable

Es la regla de la lección anterior: no puede haber un préstamo mutable (clear) mientras exista uno inmutable (palabra). Un fallo que antes pasaba desapercibido ahora es un error de compilación.

&str y String

Los literales de texto también son slices:

let saludo: &str = "hola";

saludo es un &str que apunta al trozo del binario donde está guardado el texto. Por eso los literales no se pueden modificar: un &str es una referencia de solo lectura.

String&str
PropiedadEs dueño de su textoLo toma prestado
¿Puede crecer?Sí, con push_strNo
Dónde está el textoEn el heapDonde viva el original: heap, binario…
Cómo se creaString::from("hola")"hola" o &s[..]

Recibe &str en tus funciones

Si una función solo necesita leer texto, haz que reciba &str en lugar de &String. Así acepta cualquier texto:

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

    println!("{}", primera_palabra(&frase)); // &String: se convierte solo
    println!("{}", primera_palabra(&frase[5..])); // slice de un String
    println!("{}", primera_palabra("adiós amigos")); // literal
}

fn primera_palabra(s: &str) -> &str {
    let bytes = s.as_bytes();
    for i in 0..bytes.len() {
        if bytes[i] == b' ' {
            return &s[..i];
        }
    }
    s // ya es un &str: no hace falta escribir &s[..]
}

Cuando pasas un &String donde se espera un &str, Rust lo convierte automáticamente. Al revés no funciona: un literal no es un String, así que una función que pide &String no acepta "adiós amigos".

Consejo: En los parámetros, prefiere &str a &String. Tu función será más flexible y no pierdes nada.

Slices de arrays

Los slices no son solo para texto. Un slice de un array de i32 tiene el tipo &[i32]:

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

    let centro = &numeros[1..4]; // 20, 30 y 40
    println!("Elementos del centro: {}", centro.len());
    println!("Suma del centro: {}", sumar(centro));
    println!("Suma total: {}", sumar(&numeros));
}

fn sumar(valores: &[i32]) -> i32 {
    let mut total = 0;
    for v in valores {
        total += v;
    }
    total
}

Igual que con &str, una función que recibe &[i32] acepta tanto un array completo como un trozo. Más adelante verás que también sirve para vectores.

Cuidado con UTF-8

Los índices de un slice de texto cuentan bytes, no letras. En UTF-8, las letras sin tilde ocupan 1 byte, pero ñ, á o los emojis ocupan 2 o más. Si cortas por la mitad de un carácter, el programa se detiene con un error en tiempo de ejecución (un panic):

fn main() {
    let s = String::from("ñandú");
    let trozo = &s[0..1]; // la ñ ocupa los bytes 0 y 1
    println!("{trozo}");
}
end byte index 1 is not a char boundary; it is inside 'ñ' (bytes 0..2) of `ñandú`

&s[0..2] sí funcionaría y devolvería "ñ". Con textos en español esto pasa a menudo, así que corta solo en posiciones que sepas seguras, como la de un espacio, que siempre ocupa 1 byte. En la lección de strings verás cómo recorrer un texto letra a letra.

Hora de practicar

Ejercicios

0/3 resueltos

Reto 1 de 3

Usando slices de frase, imprime cada una de sus tres palabras en una línea: aprende, rust y hoy. No escribas las palabras a mano: recórtalas de frase con rangos como &frase[..7].
Salida esperada
aprende
rust
hoy
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos