Lección 14 de 26 Colecciones y errores
Strings en profundidad
- Un String es texto que puede crecer: vive en el heap y es dueño de su contenido.
- push_str añade un texto al final y push, un solo carácter (un char, con comillas simples).
- Por dentro es UTF-8: cada carácter ocupa de 1 a 4 bytes. En "año", la ñ ocupa 2.
- Por eso len() cuenta bytes (4) y chars().count(), caracteres (3). Y palabra[0] no compila.
Trabajar con texto en Rust tiene fama de complicado. No es que Rust lo complique: es que el texto es complicado (acentos, eñes, emojis, alfabetos de todo el mundo…) y Rust no te lo esconde. En cuanto entiendes un par de ideas, todo encaja.
En esta lección vas a ver cómo crear y modificar strings, cómo concatenarlos, por qué no puedes acceder a una letra con s[0] y los métodos que más vas a usar en el día a día.
String y &str: repaso
Ya conoces los dos tipos de texto de Rust:
String: un texto propio, guardado en el heap, que puede crecer y modificarse.&str: una vista prestada de un texto que está en otro sitio. Los literales como"hola"son&str.
La regla práctica: usa String cuando necesites ser dueño del texto o modificarlo, y &str en los parámetros de las funciones que solo lo leen. Una función que recibe &str acepta tanto literales como &String.
Crear un String
fn main() {
let vacio = String::new();
let saludo = String::from("hola");
let nombre = "Ferris".to_string();
println!("[{vacio}] {saludo} {nombre}");
}String::from("…") y "…".to_string() hacen lo mismo: crean un String nuevo copiando el texto del literal.
Añadir texto: push_str y push
push_str añade un texto al final y push añade un solo carácter (un char, con comillas simples):
fn main() {
let mut s = String::from("Hola");
s.push_str(", mundo");
s.push('!');
println!("{s}");
}push_str recibe un &str, así que no se queda con el texto que le pasas: puedes seguir usándolo después.
Concatenar: + y format!
Puedes unir strings con +, pero tiene una peculiaridad:
fn main() {
let hola = String::from("Hola, ");
let mundo = String::from("mundo");
let saludo = hola + &mundo;
println!("{saludo}");
}El de la izquierda tiene que ser un String y el de la derecha un &str (por eso &mundo). + toma posesión del primero, le añade el segundo al final y te lo devuelve: no copia nada, reutiliza su memoria. A cambio, hola queda movido:
fn main() {
let hola = String::from("Hola, ");
let mundo = String::from("mundo");
let saludo = hola + &mundo;
println!("{hola}");
println!("{saludo}");
}Para combinar varios valores es mucho más cómodo format!. Funciona igual que println!, pero en lugar de imprimir devuelve un String, y no mueve nada:
fn main() {
let nombre = String::from("Ferris");
let lenguaje = "Rust";
let patas = 8;
let frase = format!("{nombre} programa en {lenguaje} con {patas} patas");
println!("{frase}");
println!("{nombre} sigue disponible");
}UTF-8: por qué no puedes hacer s[0]
En muchos lenguajes s[0] te da la primera letra. En Rust no compila:
fn main() {
let s = String::from("hola");
let primera = s[0];
println!("{primera}");
}El motivo es que los strings de Rust se guardan en UTF-8, donde cada carácter ocupa entre 1 y 4 bytes: las letras sin acento ocupan 1, la ñ o la é ocupan 2, el símbolo € ocupa 3 y un emoji, 4. Por eso «la posición 0» es ambigua: ¿el byte 0 o el carácter 0? Rust prefiere no adivinar.
Lo mismo pasa con len(): devuelve el número de bytes, no de caracteres. Para contar caracteres usa chars().count():
fn main() {
let palabra = "año";
println!("{} bytes", palabra.len());
println!("{} caracteres", palabra.chars().count());
}Los rangos sí funcionan (&s[0..1]), pero cuentan bytes. Si el corte cae en mitad de un carácter, el programa hace panic:
fn main() {
let s = "año";
let trozo = &s[0..2];
println!("{trozo}");
}Recorrer los caracteres: chars
Para trabajar letra a letra usa chars(), que recorre el texto carácter a carácter, sea cual sea su tamaño en bytes:
fn main() {
for c in "¡olé!".chars() {
print!("[{c}]");
}
println!();
let primera = "ñu".chars().next();
println!("{primera:?}");
}chars().next() te da el primer carácter como un Option<char>: None si el texto está vacío. Si alguna vez necesitas los bytes, también existe bytes().
Métodos más usados
fn main() {
let entrada = " Aprende Rust con Ferris ";
let limpio = entrada.trim();
println!("[{limpio}]");
println!("{}", limpio.to_uppercase());
println!("{}", limpio.contains("Rust"));
println!("{}", limpio.starts_with("Aprende"));
println!("{}", limpio.replace("Ferris", "midudev"));
}
| Método | Qué hace | Devuelve |
|---|---|---|
trim() | Quita los espacios del principio y del final | &str |
to_uppercase() / to_lowercase() | Pasa a mayúsculas / minúsculas | String |
contains("x") | ¿Contiene el texto x? | bool |
starts_with("x") / ends_with("x") | ¿Empieza / termina por x? | bool |
replace("a", "b") | Cambia cada a por b | String |
find('x') | Posición (en bytes) de x | Option<usize> |
Fíjate en la última columna: trim devuelve un &str porque es solo una vista del texto original, sin copiar nada. to_uppercase y replace crean un texto nuevo, así que devuelven un String.
Trocear: split y split_whitespace
split corta el texto cada vez que encuentra un separador. split_whitespace corta por los espacios, sin importar cuántos haya seguidos:
fn main() {
for fruta in "manzana,pera,uva".split(',') {
println!("{fruta}");
}
let palabras: Vec<&str> = " hola mundo rust ".split_whitespace().collect();
println!("{palabras:?}");
}Los dos se pueden recorrer con for. Y si quieres guardar los trozos, collect() los reúne en un vector; tienes que indicar el tipo (Vec<&str>) para que sepa qué construir.
Convertir texto en número: parse
Con parse conviertes un texto en un número. Como el texto puede no ser un número válido, no devuelve un Option sino un Result: Ok(valor) si ha ido bien o Err(error) si no. Se trata con match, igual que un Option:
fn main() {
for texto in ["42", "-7", "hola"] {
match texto.parse::<i32>() {
Ok(numero) => println!("{texto} → {}", numero * 2),
Err(_) => println!("{texto} no es un número"),
}
}
}::<i32> le dice a parse a qué tipo convertir. Verás Result a fondo en la lección de manejo de errores.
Consejo: Cuando tengas dudas entre
Stringy&stren un parámetro, empieza con&str. Si el compilador te pide unString, te lo dirá con un mensaje claro.
Hora de practicar
Ejercicios
0/4 resueltos
Reto 1 de 4 Resuelto
String vacío saludo, construye Hola, Ferris! con push_str("Hola, "), push_str(&nombre) y push('!'). Después crea ficha con format! para que valga Ferris tiene 8 patas, usando las variables nombre y patas. Imprime las dos.Hola, Ferris! Ferris tiene 8 patas
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.