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 &str em parâmetros de funções para evitar ownership desnecessário.
  • Use String quando precisar modificar ou armazenar dados.
  • Evite indexação direta; use iteração ou slices seguros.
  • Para concatenação, use push_str ou format! em vez de + para evitar alocações extras.

Referências

Exercícios

  1. 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
    }
  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());
    }
  3. 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"
    }
  4. 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
    }
  5. 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),
        }
    }