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() ou String::from().
  • String para &str: use &s[..] ou s.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() ou String::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 &str em 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 String implementa Deref, então você pode passar uma &String onde se espera &str.

Referências

Exercícios

  1. Crie uma função que receba uma &str e retorne o número de palavras (separadas por espaços) na string. Use o método split_whitespace().

    ✓ Resposta:
    fn count_words(s: &str) -> usize {
        s.split_whitespace().count()
    }
    
    fn main() {
        println!("{}", count_words("Rust é incrível")); // 3
    }
    
  2. Escreva uma função que receba uma String e retorne a mesma string com todas as vogais minúsculas removidas. Use chars() e filter().

    ✓ 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
    }
    
  3. Crie uma função que receba uma &str e retorne um Option com o primeiro caractere não-ASCII (maior que 127) da string, se houver. Use chars() e find().

    ✓ 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('世')
    }
    
  4. Implemente uma função que concatene um vetor de &str em uma única String, separando cada elemento por vírgula. Use join().

    ✓ Resposta:
    fn join_strings(v: &[&str]) -> String {
        v.join(",")
    }
    
    fn main() {
        let words = vec!["Rust", "é", "show"];
        println!("{}", join_strings(&words)); // Rust,é,show
    }
    
  5. Escreva uma função que receba uma String e retorne um Result que 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),
        }
    }