Iteración sobre cadenas en Rust: cómo funciona bajo el capó
¿Alguna vez te has preguntado qué sucede realmente cuando escribes for c in "Привет".chars()? Si vienes de Python o JavaScript, donde las cadenas son simplemente arreglos de caracteres, Rust puede parecerte un maestro estricto que te obliga a pensar antes de cada acción. ¡Y eso es genial!
La pregunta "How do you iterate over a string by character" en Stack Overflow ha acumulado miles de visitas. Y no es para menos. En Rust no existe un simple string[i] — y hay buenas razones para ello. Analicemos cómo iterar una cadena de forma correcta, eficiente y sin sorpresas.
¿Por qué Rust no te deja simplemente tomar un carácter por índice?
Todo se debe a la codificación UTF-8. A diferencia de muchos lenguajes, Rust almacena las cadenas en UTF-8, no en ASCII o UCS-2. Esto significa que un solo carácter puede ocupar de 1 a 4 bytes. Por ejemplo:
'a'— 1 byte'é'— 2 bytes'中'— 3 bytes'😊'— 4 bytes
Si Rust permitiera hacer string[0], entonces "😊"[0] devolvería no el emoji, sino la mitad de su representación en bytes — basura. Por eso Rust te protege de estos errores a nivel del compilador.
Formas principales de iterar sobre una cadena
Rust ofrece tres métodos principales de iteración, cada uno con su propósito. La elección depende de lo que necesites exactamente: caracteres, bytes o grafemas.
1. Iteración por caracteres (char)
La forma más común es el método .chars(). Devuelve un iterador sobre caracteres Unicode (tipo char).
fn main() { let text = "Привет, мир! 🌍"; for c in text.chars() { println!("{}", c); } // Salida: // П // р // и // в // е // т // , // (espacio) // м // и // р // ! // (espacio) // 🌍}Bajo el capó, .chars() analiza la secuencia de bytes UTF-8 y devuelve cada carácter válido. Si la cadena contiene bytes no válidos, el iterador puede devolver std::char::REPLACEMENT_CHARACTER ('�').
Importante: .chars() es O(n). El iterador recorre todos los bytes, decodificándolos sobre la marcha. Para cadenas cortas es instantáneo, pero para cadenas largas (megabytes de texto) puede ser notable.
2. Iteración por bytes (bytes)
Si necesitas los bytes sin procesar, usa .bytes(). Es una forma rápida, pero obtienes bytes, no caracteres.
fn main() { let text = "Hello"; for byte in text.bytes() { println!("{}", byte); } // Salida: // 72 (H) // 101 (e) // 108 (l) // 108 (l) // 111 (o)}Para texto en inglés, esto equivale a iterar por caracteres. Pero para ruso o emojis, no. Por ejemplo, "Привет" en bytes serán 12 bytes (2 por carácter), y "🌍" — 4 bytes.
Cuándo usarlo: cuando trabajas con datos binarios, protocolos o deseas el máximo rendimiento sin decodificar UTF-8.
3. Iteración por grafemas (grapheme clusters)
Y esto ya es más interesante. A veces un "carácter" visualmente consiste en varios caracteres Unicode. Por ejemplo, la bandera de un país o una letra con signos diacríticos: "é" puede representarse como un solo carácter é (U+00E9) o como una combinación de e + ◌́ (U+0065 + U+0301).
Para trabajar con grafemas, necesitas agregar la crate unicode-segmentation:
// Cargo.toml// [dependencies]// unicode-segmentation = "1.10"
use unicode_segmentation::UnicodeSegmentation;
fn main() { let text = "é"; // chars() devolverá 1 s