Strings, UTF-8 e Texto em Rust
O manuseio de texto confunde mais novatos em Rust do que quase qualquer outra parte da linguagem, não porque a API seja incomumente grande, mas porque Rust se recusa a deixar você fingir que o texto é mais simples do que realmente é. Dois tipos (String e &str), um invariante de armazenamento (UTF-8 válido, sempre), e uma recusa categórica (sem indexação de caractere s[i]) contam toda a história - e cada uma dessas três decisões existe para prevenir uma classe específica de bug real que outras linguagens permitem silenciosamente.
Esta página é a âncora conceitual para a seção de strings e texto. Noções Básicas de Strings mostra a API do dia a dia; esta página explica por que a API é moldada da maneira que é - por que a propriedade divide String de &str da mesma forma que divide qualquer outra coleção Rust, o que a garantia UTF-8 realmente oferece, e por que a indexação por byte entra em pânico em vez de retornar silenciosamente algo errado.
Resumo
Stringé um buffer de bytes próprio, expansível e alocado no heap, garantido como UTF-8 válido;&stré uma visualização emprestada de tal buffer, com a mesma garantia.- Por que Importa: O texto tem largura variável em UTF-8 - um "caractere" pode ter de 1 a 4 bytes - então qualquer API que permitisse indexar por posição de caractere em O(1) estaria mentindo sobre o desempenho ou mentindo sobre a correção; Rust escolhe ser honesto em vez disso.
- Conceitos Chave: Codificação UTF-8, texto próprio vs. emprestado, limite de caractere, coerção
Deref(String->&str), comprimento de byte vs. contagem de caracteres. - Quando Usar:
&strpara parâmetros de função e visualizações somente leitura (flexibilidade máxima, sem alocação);Stringquando você precisa possuir, construir ou mutar texto ao longo do tempo. - Limitações / Compromissos: Nenhuma indexação de caractere O(1) é possível mantendo a garantia UTF-8; o manuseio correto de caracteres percebidos pelo usuário (emojis, marcas combinatórias) requer um crate além da biblioteca padrão.
- Tópicos Relacionados: propriedade e empréstimo, fatias (slices), coleções e iteradores,
Cowpara propriedade condicional.
Fundamentos
String e &str se dividem da mesma forma que Vec<T> e [T] (uma fatia) fazem, e pela mesma razão. String possui um buffer expansível alocado no heap - você pode adicionar a ele, possuí-lo, movê-lo, e ele é descartado quando sai do escopo. &str é uma visualização emprestada de uma sequência de bytes UTF-8, que pode residir dentro do buffer de um String, dentro do binário compilado (um literal &'static str), ou dentro de qualquer outra fonte de bytes que mantenha a garantia UTF-8. Todo String pode produzir uma visualização &str de si mesmo essencialmente de graça (através de Deref), mas nem todo &str tem um String por trás dele - um literal de string "hello" é um &'static str apontando diretamente para os dados somente leitura do programa, sem nenhum String proprietário em lugar nenhum.
Essa divisão é o motivo pelo qual assinaturas de função idiomáticas aceitam &str em vez de String ou &String: um parâmetro &str aceita um empréstimo de um String próprio, um literal de string, ou uma fatia de qualquer um deles, uniformemente, sem forçar o chamador a alocar apenas para chamar sua função. String como tipo de parâmetro é um sinal de que a função especificamente precisa possuir o texto (para armazená-lo, estendê-lo ou passá-lo adiante).
O segundo fato fundamental é a própria garantia UTF-8: o compilador e a biblioteca padrão de Rust tratam "isto é um str" e "isto é UTF-8 válido" como a mesma afirmação. Não há como construir um &str ou String contendo UTF-8 inválido através de código seguro - String::from_utf8 retorna um Result especificamente porque os bytes de entrada podem não ser válidos, e construí-lo a partir de bytes que você não validou requer uma função unsafe (from_utf8_unchecked) que coloca o ônus da prova sobre você.
Mecânicas e Interações
UTF-8 codifica cada valor escalar Unicode ("caractere", em sentido amplo) como um a quatro bytes, com a contagem de bytes determinada pelo ponto de código do caractere. Caracteres ASCII levam exatamente um byte e são byte a byte idênticos a texto ASCII puro; a maioria dos scripts não latinos e emojis levam de dois a quatro bytes. Essa variabilidade é toda a razão pela qual str não pode ser indexado por posição de caractere da maneira que um array pode:
"Hello" -> 5 bytes, 5 chars (tudo ASCII, 1 byte cada)
"Hello 🦀" -> 10 bytes, 7 chars (🦀 sozinho tem 4 bytes)
Um índice de array como arr[3] é rápido precisamente porque o tamanho do elemento é fixo, então o endereço do elemento n é uma simples multiplicação. O caractere n em uma string UTF-8 não tem um deslocamento fixo - encontrá-lo requer caminhar a partir do início (ou de um limite conhecido) e decodificar a largura de bytes de cada caractere ao longo do caminho, o que é uma operação O(n), não O(1). Em vez de oferecer um método chamado .char_at(n) que silenciosamente custa O(n) e atrai as pessoas a chamá-lo em um loop (o que silenciosamente se torna O(n^2)), Rust simplesmente não implementa Index<usize> para str de forma alguma. s[3] é um erro de compilação, não um erro de tempo de execução - a honestidade é imposta o mais cedo possível.
O que str suporta é a indexação por intervalo de bytes (&s[0..3]), que é O(1) exatamente da maneira que você esperaria que o fatiamento fosse, mas vem com uma obrigação: ambos os pontos finais do intervalo devem cair em um limite de caractere - o início de um caractere codificado, não o meio de um. Fatiar no meio de um caractere multibyte entra em pânico em tempo de execução, porque o resultado seria um str contendo uma sequência UTF-8 truncada e inválida, que o próprio invariante do tipo proíbe que exista.
fn safe_prefix(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes { return s; }
let mut end = max_bytes;
// Caminha para trás a partir do deslocamento de byte solicitado até chegarmos a um
// limite de caractere real - o fatiamento de `str` entra em pânico caso contrário, porque
// uma sequência UTF-8 meio codificada violaria a garantia do tipo.
while !s.is_char_boundary(end) { end -= 1; }
&s[..end]
}É por isso também que len() em um str retorna o comprimento de byte, não uma contagem de caracteres: o comprimento de byte é a única medida que é genuinamente O(1), pois é apenas o tamanho do buffer. Obter uma contagem de caracteres requer s.chars().count(), que percorre e decodifica toda a string - uma operação O(n) com um perfil de custo diferente que a API torna visível exigindo uma chamada de método explícita em vez de escondê-la atrás de .len().
Considerações Avançadas e Aplicações
Mesmo "contagem de caracteres" é um alvo escorregadio, porque o Unicode tem três noções comumente confundidas de "qual o comprimento deste texto": bytes (len()), valores escalares Unicode (chars().count()), e caracteres percebidos pelo usuário, ou clusters de grafemas (o que uma pessoa chamaria de "uma letra na tela"). Um único grafema como um emoji com um modificador de tom de pele, ou uma letra latina mais uma marca de acento combinatória, pode ser múltiplos chars e vários bytes a mais, tudo enquanto um humano o lê como um único caractere. A biblioteca padrão para deliberadamente em char (um valor escalar Unicode); a segmentação de grafemas é sensível a localidade e script de maneiras que inchariam std, então ela reside no crate unicode-segmentation em vez disso.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
.len() (bytes) | O(1), tamanho exato do buffer/protocolo | Não é uma "contagem de caracteres" por nenhuma medida humana | Formatos de rede, dimensionamento de buffer, verificações de capacidade |
.chars().count() | Corresponde a valores escalares Unicode | O(n); ainda não são caracteres percebidos pelo usuário para marcas combinatórias/emojis | Validação de restrições em nível de escalar, texto simples não latino |
unicode-segmentation grafemas | Corresponde ao que um usuário percebe como "um caractere" | Dependência externa; O(n) e mais trabalho por etapa | Movimento do cursor, truncamento e limites de comprimento na interface do usuário |
Truncar texto com segurança para exibição é um caso concreto onde tudo isso importa ao mesmo tempo: truncar por contagem de bytes arrisca fatiar no meio de um caractere (um pânico, ou saída corrompida com fatiamento unsafe); truncar por chars().count() ainda pode cortar um cluster de grafemas ao meio (separando uma sequência de emoji ou uma marca combinatória de sua letra base); apenas o truncamento ciente de grafemas corresponde ao que um usuário consideraria um "caractere inteiro" removido de forma limpa. Código de protocolo e formato de arquivo, por outro lado, quase sempre quer o comprimento de byte - ele precisa corresponder ao que está realmente na rede, não ao que um leitor humano contaria.
A interoperação com o sistema operacional é o outro lugar onde a garantia UTF-8 termina visivelmente: caminhos de arquivo e strings em nível de sistema operacional não são garantidos como UTF-8 válido em todas as plataformas (caminhos do Windows são baseados em UTF-16, e caminhos Unix são sequências de bytes arbitrárias), que é exatamente por que std::ffi::OsString e std::path::Path existem como tipos separados de String - eles intencionalmente não carregam o invariante UTF-8, porque não é uma garantia que o sistema operacional realmente fornece.
Conceitos Errôneos Comuns
- "
Stringe&strsão basicamente o mesmo tipo, um apenas tem um&" - Eles diferem da mesma forma queVec<T>e[T]fazem: um possui um buffer expansível, o outro empresta uma visualização; entendê-los através dessa lente (não como "string mutável vs. imutável") esclarece a maioria das confusões sobre qual usar onde. - "
s.len()diz quantos caracteres há na string" - Ele diz o comprimento em bytes da codificação UTF-8, que é igual à contagem de caracteres apenas para texto puramente ASCII; qualquer coisa fora do ASCII precisa dechars().count(), e mesmo isso não é a contagem de caracteres percebida pelo usuário. - "
s[i]deveria funcionar como para arrays" - A indexação de array é rápida porque cada elemento tem o mesmo tamanho; caracteres UTF-8 têm de 1 a 4 bytes cada, então não há maneira O(1) de encontrar "o enésimo caractere", e Rust se recusa a oferecer uma API que finja o contrário. - "Um
Stringe as fatias&strretiradas dele são cópias separadas" - Um&stremprestado de umStringaponta diretamente para o buffer existente no heap desseString; nenhuma cópia ocorre a menos que você explicitamente chame.to_string()ou.to_owned(). - "Qualquer sequência de bytes pode ser tratada como uma string se você apenas a converter" -
String/strgarantem UTF-8 válido como um invariante imposto pelo sistema de tipos; transformar bytes arbitrários em um deles ou os valida (String::from_utf8, que pode falhar) ou requer códigounsafeafirmando uma garantia que você deve realmente cumprir.
FAQs
Por que Rust tem dois tipos de string em vez de um?
Porque a propriedade importa para texto da mesma forma que importa para qualquer outra coleção: String possui um buffer expansível no heap, enquanto &str empresta uma visualização de bytes de propriedade em outro lugar (um String, um literal de string ou outro buffer). Isso espelha a divisão Vec<T>/[T] e permite que APIs aceitem texto emprestado sem forçar uma alocação.
O que "garantia UTF-8" realmente significa para `str`?
Significa que o sistema de tipos trata "ser um str válido" e "conter bytes UTF-8 bem formados" como inseparáveis - não há maneira segura de construir um str ou String contendo UTF-8 inválido. Bytes de uma fonte não confiável devem ser validados (String::from_utf8, que retorna um Result) antes que possam se tornar um str real.
Por que `s[i]` não funciona em um `str`?
Porque UTF-8 tem largura variável - um caractere pode ter de 1 a 4 bytes - então não há aritmética de tamanho fixo que encontre "o enésimo caractere" da maneira que a indexação de array encontra "o enésimo elemento". Rust simplesmente não implementa esse operador de indexação para str, forçando você a buscar um método explícito e com preço honesto em vez disso.
O que `.len()` em um `&str` realmente mede?
O comprimento em bytes de sua codificação UTF-8, não uma contagem de caracteres. Para texto apenas ASCII, os dois coincidem por acaso, que é por que a distinção surpreende as pessoas até que testem com entrada não ASCII como emojis ou caracteres acentuados.
O que é um "limite de caractere" e por que o fatiamento às vezes entra em pânico?
Um limite de caractere é um deslocamento de byte que cai no início de um caractere codificado em vez de no meio de um. Fatiar &s[a..b] onde a ou b cai no meio de um caractere produziria bytes que não são UTF-8 válidos por si só, o que o invariante do tipo str proíbe, então Rust entra em pânico em vez de retornar um valor corrompido.
Como obtenho o número de "caracteres" em uma string, então?
Depende de qual noção de "caractere" você quer dizer: .chars().count() fornece o número de valores escalares Unicode (uma caminhada O(n)), enquanto o número de caracteres percebidos pelo usuário (clusters de grafemas, correspondendo ao que alguém lendo o texto contaria) precisa do crate unicode-segmentation, já que a biblioteca padrão não implementa essa lógica.
Converter um `String` para `&str` é uma operação cara?
Não - é efetivamente gratuito. Um &str emprestado de um String é apenas uma visualização de ponteiro e comprimento no buffer existente do String; nenhum byte é copiado, que é também por que funções que apenas precisam ler texto devem aceitar &str em vez de String.
Por que algumas APIs aceitam `String` em vez de `&str`?
Quando uma função precisa possuir o texto - para armazená-lo em um campo de struct, adicionar mais conteúdo a ele, ou passá-lo para algo que sobrevive ao empréstimo do chamador - ela precisa de String, não de uma visualização emprestada. &str como parâmetro é o padrão para qualquer coisa que apenas leia o texto.
Por que o texto de emoji às vezes tem um `chars().count()` que não corresponde ao que parece ser um emoji?
Alguns emojis (um emoji de família unido com zero-width joiners, ou um emoji base mais um modificador de tom de pele) são codificados como múltiplos valores escalares Unicode que renderizam como um glifo. chars() conta valores escalares, não glifos renderizados, então ele pode relatar mais "caracteres" do que uma pessoa contaria visualmente - essa lacuna é exatamente o que a segmentação de grafemas existe para fechar.
Por que caminhos de arquivo não são apenas `String` em Rust?
Porque sistemas operacionais não garantem que seus caminhos sejam UTF-8 válido - caminhos do Windows são efetivamente baseados em UTF-16 e caminhos Unix são sequências de bytes arbitrárias que não são obrigadas a ser de nenhuma codificação específica. std::path::Path e std::ffi::OsString existem especificamente porque o invariante UTF-8 que String/str dependem não é algo que o sistema operacional realmente promete.
É seguro truncar uma string por comprimento de byte alguma vez?
Apenas se você souber que o conteúdo é apenas ASCII, ou se encontrar um limite de caractere válido primeiro (como is_char_boundary permite verificar) - truncar em um deslocamento de byte arbitrário pode cortar um caractere multibyte e ou entrar em pânico (API segura) ou produzir texto corrompido (API unsafe usada incorretamente).
Qual a diferença entre `String::from_utf8` e `String::from_utf8_lossy`?
from_utf8 valida os bytes de entrada estritamente e retorna um Result, falhando se qualquer sequência de bytes não for UTF-8 válida. from_utf8_lossy sempre tem sucesso substituindo sequências inválidas pelo caractere de substituição Unicode, o que é conveniente para exibição, mas descarta silenciosamente informações sobre o que os bytes originais realmente eram.
Relacionados
- Noções Básicas de Strings - sintaxe prática de
String/&str,push_streformat!. - UTF-8 e Indexação - fatiamento seguro,
char_indiceseis_char_boundaryna prática. - Chars, Bytes e Grafemas - as três maneiras de medir o comprimento do texto, incluindo clusters de grafemas.
- Manipulação de Strings - divisão, remoção de espaços em branco, substituição e construção de strings.
- OsString, Path e CStr - tipos de texto que deliberadamente não carregam a garantia UTF-8.
Versões do Stack: Esta página é conceitual e não está vinculada a uma versão específica do stack.