Strings em profundidade
Esta aula aprofunda o conhecimento sobre strings em Rust, abordando as diferenças entre String e &str, o suporte a UTF-8, os principais métodos e as conversões entre tipos. O objetivo é capacitar o aluno a manipular textos de forma eficiente e segura.
Strings são essenciais em qualquer linguagem de programação, e em Rust elas são tratadas com cuidado especial para garantir segurança de memória e suporte a textos Unicode. Nesta aula, vamos explorar os dois tipos de strings do Rust: String e &str, entender como a codificação UTF-8 afeta a indexação, conhecer os métodos mais usados e aprender a converter entre diferentes tipos de strings e outros tipos de dados.
Dominar strings em Rust é fundamental para escrever código robusto, especialmente ao lidar com entrada de usuário, arquivos de texto ou comunicação em rede. Vamos começar!
String vs &str
Em Rust, existem dois tipos principais para representar texto: String e &str. O &str (pronuncia-se "string slice") é uma referência imutável a uma sequência de bytes UTF-8 armazenada em algum lugar (como no código-fonte ou em um buffer alocado). Já String é um tipo proprietário (owned) que aloca memória no heap e pode ser modificada.
A principal diferença é que String possui ownership e pode ser alterada (crescer, encolher, etc.), enquanto &str é apenas uma visualização de dados que pertencem a outra variável. Por exemplo, literais de string como "Olá" são do tipo &'static str, ou seja, armazenados no binário do programa. Já uma String pode ser criada a partir de um literal ou de outras fontes.
fn main() {
let literal: &str = "Olá, mundo!"; // &str
let mut string_owned: String = String::from("Olá, mundo!"); // String
// &str pode ser criado a partir de String
let slice: &str = &string_owned[..5];
println!("{}", slice); // "Olá,"
// String pode ser criada a partir de &str
let nova_string: String = literal.to_string();
}Na prática, use &str para parâmetros de funções que só precisam ler o texto (mais eficiente) e String quando você precisar ser dono dos dados ou modificá-los.
UTF-8 e indexação
Rust armazena strings internamente como bytes em UTF-8. Isso significa que cada caractere Unicode pode ocupar de 1 a 4 bytes. Por isso, não é possível indexar uma string diretamente com um número de índice esperando obter um caractere; a indexação em Rust retorna bytes, e acessar um índice que não seja um limite de caractere causa pânico.
fn main() {
let s = String::from("Olá");
// let c = s[0]; // Erro! String não suporta indexação por caractere
// Para acessar bytes:
let bytes: &[u8] = s.as_bytes();
println!("{:?}", bytes); // [79, 108, 195, 161] (O, l, á em UTF-8)
// Para acessar caracteres, use chars():
for c in s.chars() {
println!("{}", c);
}
// Para pegar um caractere específico:
if let Some(c) = s.chars().nth(2) {
println!("{}", c); // 'á'
}
}Para trabalhar com índices baseados em caracteres, você pode usar o método chars() que itera sobre os caracteres Unicode, ou char_indices() que retorna também a posição em bytes. Lembre-se: a indexação por byte é rápida, mas insegura se você não souber que está em um limite de caractere. Use get() com intervalos seguros.
Métodos comuns
Rust oferece uma vasta gama de métodos para manipular strings. Vamos destacar os mais usados:
- push_str e push: adicionam uma string slice ou um caractere ao final de uma String.
- len e is_empty: retornam o tamanho em bytes e se a string está vazia.
- contains e find: buscam por substrings.
- replace e replacen: substituem ocorrências de um padrão.
- split e split_whitespace: dividem a string em partes.
- trim e to_lowercase/to_uppercase: ajustam capitalização e removem espaços.
fn main() {
let mut s = String::from("Olá");
s.push_str(", mundo!");
s.push('?');
println!("{}", s); // "Olá, mundo!?"
let palavras = s.split_whitespace().collect::<Vec<&str>>();
println!("{:?}", palavras); // ["Olá,", "mundo!?"]
let substituido = s.replace("mundo", "terra");
println!("{}", substituido); // "Olá, terra!?"
if s.contains("mundo") {
println!("Contém 'mundo'");
}
}Explore a documentação oficial para uma lista completa: String e str.
Conversões
Converter entre String, &str e outros tipos é comum. Para converter um &str em String, use .to_string() ou String::from(). Para o inverso, faça um borrow: &s[..] ou s.as_str().
fn main() {
let s: &str = "Olá";
let owned: String = s.to_string(); // ou String::from(s)
let owned2: String = String::from("Mundo");
let slice: &str = &owned2[..]; // ou owned2.as_str()
// Para números:
let numero = 42;
let num_str: String = numero.to_string();
let parsed: i32 = "42".parse().unwrap(); // cuidado com unwrap!
// Para booleanos:
let b = true;
let b_str: String = b.to_string();
let parsed_b: bool = "true".parse().unwrap();
}Para conversões de tipos mais complexos, use format! que funciona como println! mas retorna uma String. Lembre-se de tratar erros ao fazer parsing com parse(); prefira usar match ou unwrap_or.
Boas práticas
Ao trabalhar com strings em Rust, lembre-se:
- Prefira
&strem parâmetros de funções para evitar ownership desnecessário. - Use
Stringquando precisar modificar ou armazenar dados. - Evite indexação direta; use iteração ou slices seguros.
- Para concatenação, use
push_strouformat!em vez de+para evitar alocações extras.
Referências
- The Rust Programming Language - Strings
- Documentação oficial de String
- Documentação oficial de str
- Rust by Example - Strings
- Referência da linguagem - Tipos textuais
- The Rustonomicon - Strings
Exercícios
Crie uma função que receba uma string slice e retorne o número de palavras (separadas por espaços). Dica: use
split_whitespace.✓ Resposta:fn conta_palavras(s: &str) -> usize { s.split_whitespace().count() } fn main() { println!("{}", conta_palavras("Olá mundo!")); // 2 }Escreva um programa que leia uma linha do usuário e a imprima em letras maiúsculas. Use
to_uppercase().✓ Resposta:use std::io; fn main() { let mut input = String::new(); io::stdin().read_line(&mut input).expect("Falha ao ler linha"); print!("{}", input.to_uppercase()); }Implemente uma função que receba uma string e retorne uma nova string com todos os caracteres 'a' substituídos por 'o'. Use
replace.✓ Resposta:fn troca_a_por_o(s: &str) -> String { s.replace('a', "o") } fn main() { println!("{}", troca_a_por_o("casa")); // "coso" }Dada uma string, verifique se ela contém a substring "rust". Use
contains.✓ Resposta:fn contem_rust(s: &str) -> bool { s.contains("rust") } fn main() { println!("{}", contem_rust("Amo rust")); // true }Converta um número inteiro (ex: 123) para string e depois de volta para inteiro. Imprima ambos. Trate o erro de parsing adequadamente.
✓ Resposta:fn main() { let num = 123; let num_str = num.to_string(); println!("String: {}", num_str); match num_str.parse::<i32>() { Ok(valor) => println!("Número: {}", valor), Err(e) => println!("Erro ao parsear: {}", e), } }