Strings em profundidade
Esta aula explora em profundidade o sistema de strings em Rust, diferenciando String e &str, abordando codificação UTF-8 e indexação, métodos comuns e conversões entre tipos. Inclui exemplos práticos e exercícios para consolidar o aprendizado.
Strings são fundamentais em qualquer linguagem de programação, e Rust oferece um sistema de strings que é ao mesmo tempo poderoso e seguro, mas que pode ser desafiador para iniciantes. Diferentemente de muitas linguagens, Rust possui dois tipos principais de strings: String (heap-allocated, mutável) e &str (fat pointer para uma sequência de bytes UTF-8, imutável). Nesta aula, vamos mergulhar fundo nesses tipos, entender como o UTF-8 afeta a indexação, explorar os métodos mais úteis e aprender a converter entre diferentes representações.
O objetivo é que ao final você se sinta confortável para manipular strings em Rust de forma eficiente e idiomática, evitando armadilhas comuns como indexação incorreta ou alocações desnecessárias.
String vs &str
Em Rust, String é um tipo owned (possui os dados) e mutável, alocado no heap. Já &str é uma referência imutável para uma sequência de bytes UTF-8, geralmente armazenada em algum lugar da memória (como no segmento de dados do binário ou em um String). A principal diferença é que String pode ser modificada (crescer, shrink, etc.), enquanto &str é uma fatia (slice) imutável.
Exemplo de criação:
let s1: &str = "hello"; // string literal, imutável, estática
let s2: String = String::from("hello"); // String alocada no heap
let s3: String = "hello".to_string(); // também cria uma String
let s4: &str = &s2[..]; // referência a uma fatia de s2
Um erro comum é tentar usar &str onde se espera String e vice-versa. Funções geralmente aceitam &str como parâmetro para evitar alocações desnecessárias, pois &str pode ser emprestado de um String ou de um literal. Mas se a função precisa armazenar ou modificar a string, ela deve receber ou clonar para String.
UTF-8 e indexação
Rust armazena strings internamente como bytes UTF-8. Isso significa que cada caractere pode ocupar de 1 a 4 bytes. Consequentemente, a indexação por posição de byte não é segura, pois um índice pode cair no meio de um caractere. Rust previne isso não permitindo indexação direta com []. Em vez disso, você deve usar fatias (slices) que garantem limites de caractere.
Exemplo de fatia segura:
let s = "Olá, mundo!"; // string com caracteres UTF-8
// Obtendo os primeiros 5 caracteres (cuidado: 'O','l','á' são 4 bytes? 'á' tem 2 bytes)
let slice = &s[0..4]; // bytes 0 a 3 (inclusive) = "Olá"? Depende da codificação
// Melhor: usar chars() para iterar sobre caracteres
let first_five: String = s.chars().take(5).collect();
println!("{}", first_five); // "Olá, "
Para percorrer caracteres individualmente, use .chars(). Para percorrer bytes, use .bytes(). Acessar um caractere por índice não é direto; você precisa iterar. Rust também oferece métodos como .get() para acesso seguro a fatias.
let s = "Hello";
// s[0] não compila! Use:
if let Some(c) = s.chars().nth(0) {
println!("{}", c); // 'H'
}
Esse design evita erros de indexação que são comuns em linguagens como C ou Python (que tratam strings como arrays de caracteres de largura fixa).
Métodos comuns
Rust fornece uma rica API para manipulação de strings. Aqui estão alguns dos métodos mais úteis:
- len() e is_empty(): retornam o número de bytes e se a string está vazia, respectivamente.
- push() e push_str(): adicionam um único caractere ou uma fatia ao final de uma String.
- pop(): remove e retorna o último caractere de uma String (Option
). - contains(): verifica se a string contém uma substring.
- replace(): substitui todas as ocorrências de um padrão por outro.
- trim(): remove espaços em branco do início e fim.
- split(): divide a string em um iterador de substrings baseado em um delimitador.
- to_lowercase() / to_uppercase(): converte para minúsculas/maiúsculas (respeitando Unicode).
Exemplo prático:
let mut s = String::from(" Rust é legal! ");
s = s.trim().to_string();
println!("{}", s); // "Rust é legal!"
let partes: Vec<&str> = s.split(' ').collect();
println!("{:?}", partes); // ["Rust", "é", "legal!"]
let substituida = s.replace("legal", "poderoso");
println!("{}", substituida); // "Rust é poderoso!"
Conversões
Converter entre String, &str, números e outros tipos é comum. Aqui estão as principais conversões:
- &str para String: use
.to_string()ouString::from(). - String para &str: use
&s[..]ous.as_str(). - Número para String: use
.to_string()(ex:42.to_string()). - String para número: use
.parse::(ex:() "42".parse::).().unwrap() - char para String:
c.to_string()ouString::from(c). - String para char (primeiro caractere):
s.chars().next().unwrap().
Exemplo de conversão de String para número com tratamento de erro:
let num_str = "42";
let numero: i32 = num_str.parse().expect("Falha ao converter");
println!("{}", numero + 1); // 43
Também é possível converter entre String e Vec (bytes) usando .into_bytes() e String::from_utf8().
Boas práticas
- Prefira
&strem parâmetros de funções sempre que possível, para evitar alocações desnecessárias. - Ao construir strings, use
format!em vez de concatenações repetidas com+, que podem ser ineficientes. - Evite indexar strings diretamente; use iteradores ou fatias baseadas em caracteres.
- Lembre-se que
StringimplementaDeref, então você pode passar uma&Stringonde se espera&str.
Referências
- The Rust Book: Strings
- Documentação oficial de String
- Documentação oficial de str
- Conversão de bytes para String
- Parse de string para número
- Iteração sobre strings
Exercícios
Crie uma função que receba uma
&stre retorne o número de palavras (separadas por espaços) na string. Use o métodosplit_whitespace().✓ Resposta:fn count_words(s: &str) -> usize { s.split_whitespace().count() } fn main() { println!("{}", count_words("Rust é incrível")); // 3 }Escreva uma função que receba uma
Stringe retorne a mesma string com todas as vogais minúsculas removidas. Usechars()efilter().✓ Resposta:fn remove_vowels(s: String) -> String { s.chars() .filter(|&c| !"aeiou".contains(c)) .collect() } fn main() { let result = remove_vowels("hello".to_string()); println!("{}", result); // hll }Crie uma função que receba uma
&stre retorne umOptioncom o primeiro caractere não-ASCII (maior que 127) da string, se houver. Usechars()efind().✓ Resposta:fn first_non_ascii(s: &str) -> Option{ s.chars().find(|&c| c as u32 > 127) } fn main() { let s = "Hello, 世界!"; println!("{:?}", first_non_ascii(s)); // Some('世') } Implemente uma função que concatene um vetor de
&strem uma únicaString, separando cada elemento por vírgula. Usejoin().✓ Resposta:fn join_strings(v: &[&str]) -> String { v.join(",") } fn main() { let words = vec!["Rust", "é", "show"]; println!("{}", join_strings(&words)); // Rust,é,show }Escreva uma função que receba uma
Stringe retorne umResultque tenta converter a string para um número inteiro. Se falhar, retorne um erro com a mensagem "Erro: conversão inválida".✓ Resposta:fn parse_int(s: String) -> Result{ s.parse:: ().map_err(|_| "Erro: conversão inválida".to_string()) } fn main() { match parse_int("42".to_string()) { Ok(n) => println!("{}", n), Err(e) => println!("{}", e), } match parse_int("abc".to_string()) { Ok(n) => println!("{}", n), Err(e) => println!("{}", e), } }