Aprende Rust

Lección 14 de 26 Colecciones y errores

Strings en profundidad

  • 4 min de lectura
  • 4 ejercicios
  1. Un String es texto que puede crecer: vive en el heap y es dueño de su contenido.
  2. push_str añade un texto al final y push, un solo carácter (un char, con comillas simples).
  3. Por dentro es UTF-8: cada carácter ocupa de 1 a 4 bytes. En "año", la ñ ocupa 2.
  4. Por eso len() cuenta bytes (4) y chars().count(), caracteres (3). Y palabra[0] no compila.
1/4

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étodoQué haceDevuelve
trim()Quita los espacios del principio y del final&str
to_uppercase() / to_lowercase()Pasa a mayúsculas / minúsculasString
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 bString
find('x')Posición (en bytes) de xOption<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 String y &str en un parámetro, empieza con &str. Si el compilador te pide un String, te lo dirá con un mensaje claro.

Hora de practicar

Ejercicios

0/4 resueltos

Reto 1 de 4

Partiendo del 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.
Salida esperada
Hola, Ferris!
Ferris tiene 8 patas
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