Iteração sobre string em Rust: como funciona por baixo dos panos
Você já se perguntou o que realmente acontece quando você escreve for c in "Привет".chars()? Se você veio do Python ou JavaScript, onde strings são apenas arrays de caracteres, Rust pode parecer um professor rigoroso que faz você pensar antes de cada ação. E isso é ótimo!
A pergunta "How do you iterate over a string by character" no Stack Overflow acumulou milhares de visualizações. E não é à toa. Em Rust não existe um simples string[i] — e há boas razões para isso. Vamos entender como iterar uma string corretamente, de forma eficiente e sem surpresas.
Por que Rust não permite simplesmente pegar um caractere por índice?
Tudo se resume à codificação UTF-8. Diferente de muitas linguagens, Rust armazena strings em UTF-8, não em ASCII ou UCS-2. Isso significa que um caractere pode ocupar de 1 a 4 bytes. Por exemplo:
'a'— 1 byte'é'— 2 bytes'中'— 3 bytes'😊'— 4 bytes
Se Rust permitisse string[0], então "😊"[0] retornaria não o emoji, mas metade de sua representação em bytes — lixo. Portanto, Rust protege você desses erros no nível do compilador.
Principais formas de iteração sobre string
Rust oferece três métodos principais de iteração, cada um com seu propósito. A escolha depende do que você precisa: caracteres, bytes ou grafemas.
1. Iteração por caracteres (char)
A forma mais comum é o método .chars(). Ele retorna um iterador sobre caracteres Unicode (tipo char).
fn main() { let text = "Привет, мир! 🌍"; for c in text.chars() { println!("{}", c); } // Saída: // П // р // и // в // е // т // , // (espaço) // м // и // р // ! // (espaço) // 🌍}Por baixo dos panos, .chars() analisa a sequência de bytes UTF-8 e retorna cada caractere válido. Se a string tiver bytes inválidos, o iterador pode retornar std::char::REPLACEMENT_CHARACTER ('�').
Importante: .chars() é O(n). O iterador percorre todos os bytes, decodificando-os em tempo real. Para strings curtas é instantâneo, mas para strings longas (megabytes de texto) pode ser perceptível.
2. Iteração por bytes (bytes)
Se você precisa de bytes brutos, use .bytes(). É uma forma rápida, mas você obtém bytes, não caracteres.
fn main() { let text = "Hello"; for byte in text.bytes() { println!("{}", byte); } // Saída: // 72 (H) // 101 (e) // 108 (l) // 108 (l) // 111 (o)}Para texto em inglês, isso equivale à iteração por caracteres. Mas para russo ou emojis, não. Por exemplo, "Привет" em bytes terá 12 bytes (2 por caractere), e "🌍" — 4 bytes.
Quando usar: quando você trabalha com dados binários, protocolos ou deseja máximo desempenho sem decodificação UTF-8.
3. Iteração por grafemas (grapheme clusters)
Aqui fica mais interessante. Às vezes, um "caractere" visualmente consiste em vários caracteres Unicode. Por exemplo, a bandeira de um país ou uma letra com sinais diacríticos: "é" pode ser representado como um único caractere é (U+00E9) ou como uma combinação de e + ◌́ (U+0065 + U+0301).
Para trabalhar com grafemas, é necessário adicionar a crate unicode-segmentation:
// Cargo.toml// [dependencies]// unicode-segmentation = "1.10"
use unicode_segmentation::UnicodeSegmentation;
fn main() { let text = "é"; // chars() retornará 1 s