Lección 08 de 26 Ownership y préstamos
Slices y el tipo &str
- Un String guarda el texto como bytes seguidos. Cada byte tiene un índice, empezando por el 0.
- &s[0..4] es un slice: una vista de los bytes del 0 al 3. El final, el 4, no se incluye.
- &s[5..10] ve "mundo". Un slice no copia el texto: guarda un puntero y una longitud. Su tipo es &str.
- Con arrays funciona igual: &numeros[1..4] es un &[i32] que ve el 20, el 30 y el 40.
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:
| Slice | Equivale a | Con "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 immutableEs 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 | |
|---|---|---|
| Propiedad | Es dueño de su texto | Lo toma prestado |
| ¿Puede crecer? | Sí, con push_str | No |
| Dónde está el texto | En el heap | Donde viva el original: heap, binario… |
| Cómo se crea | String::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
&stra&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 Resuelto
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].aprende rust hoy
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.