Bem-vindo à décima segunda aula do nosso curso de Rust! Até agora, você já conheceu os conceitos de ownership (posse), borrowing (empréstimo) e referências. Nesta aula, vamos mergulhar em um tipo especial de referência que é fundamental em Rust: os slices. Slices permitem que você acesse uma sequência contígua de elementos em uma coleção sem tomar posse dela, oferecendo uma visão parcial segura e eficiente.

Veremos como os slices se aplicam a strings e arrays, por que eles são tão importantes no design da linguagem e quais são as armadilhas mais comuns que você pode encontrar ao usá-los. Ao final, você terá uma compreensão sólida para usar slices em seus próprios programas.

String slices (&str)

Em Rust, uma string slice é uma referência a uma parte de uma String. O tipo é escrito como &str (pronuncia-se 'string slice'). Ao contrário de String, que é uma estrutura que possui seu conteúdo, um &str é apenas uma visão de uma sequência de caracteres que pertence a outra variável. Isso significa que um string slice não pode ser modificado e não tem capacidade de crescimento; ele é essencialmente um par de ponteiro e comprimento (fat pointer) para uma região de memória.

Para criar um string slice, você pode usar a sintaxe de fatiamento (slicing) com índices de bytes. Por exemplo:

let s = String::from("hello world");
let hello = &s[0..5];   // "hello"
let world = &s[6..11];  // "world"

Aqui, &s[0..5] cria uma referência à parte da String que vai do byte 0 (inclusive) até o byte 5 (exclusive). Note que os índices são em bytes, não em caracteres. Isso é crucial porque strings em Rust são codificadas em UTF-8, e caracteres podem ocupar mais de um byte. Se você tentar fatiar no meio de um caractere multi-byte, o programa irá entrar em pânico em tempo de execução. Vamos ver isso em detalhes na seção de pegadinhas.

Além de fatiar String, você pode criar um &str a partir de um literal de string. Literais de string, como "hello", têm o tipo &str diretamente, porque são armazenados no binário do programa. Na verdade, quando você escreve let s = "hello";, s é um &str que aponta para a memória estática. Isso é uma diferença importante: String é alocada no heap e é mutável, enquanto &str é imutável e pode apontar para dados estáticos ou para o heap.

Uma operação comum é converter entre String e &str. Você pode obter um &str de uma String usando &s[..] ou simplesmente &s (devido à coerção de deref). Para criar uma String a partir de um &str, use to_string() ou String::from(). Por exemplo:

let s = String::from("hello");
let slice: &str = &s;  // coerção automática
let nova: String = slice.to_string();

Um ponto importante é que &str é um tipo sem dono (unsized) que sempre aparece atrás de uma referência. Você nunca vê um str sozinho; sempre &str ou Box<str> etc. Isso porque o compilador precisa saber o tamanho em tempo de compilação para alocar, e uma string slice não tem tamanho fixo.

Slices de arrays

O conceito de slice não se limita a strings; ele se aplica a qualquer array ou vetor. Um slice de array é uma referência a uma parte de um array ou Vec, e seu tipo é escrito como &[T], onde T é o tipo dos elementos. Assim como &str, um slice de array é um ponteiro gordo (fat pointer) que contém o endereço do primeiro elemento e o comprimento em número de elementos.

Para criar um slice de um array, você usa a mesma sintaxe de índices:

let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3];  // elementos 2 e 3 (índices 1 e 2)
println!("{:?}", slice);  // [2, 3]

Você também pode criar slices de Vec:

let vec = vec![10, 20, 30, 40, 50];
let slice: &[i32] = &vec[2..4];  // [30, 40]

Note que o tipo do slice é &[i32], que é uma referência a uma fatia de inteiros. É importante distinguir entre &Vec<T> e &[T]: o primeiro é uma referência ao vetor inteiro (com capacidade e comprimento), enquanto o segundo é uma referência apenas aos elementos. Em APIs, é comum aceitar &[T] em vez de &Vec<T> porque é mais flexível: você pode passar um slice de qualquer array, vetor ou até mesmo de outra slice.

Os slices são imutáveis por padrão. Se você precisar de uma versão mutável, pode usar &mut [T], que permite modificar os elementos do slice. Por exemplo:

let mut arr = [1, 2, 3, 4, 5];
let slice = &mut arr[1..3];
slice[0] = 99;  // arr agora é [1, 99, 3, 4, 5]
println!("{:?}", arr);

Nesse caso, o compilador garante que não há outras referências ativas à região modificada enquanto o slice mutável existir, mantendo a segurança de memória.

Por que slices existem

Os slices existem para resolver um problema fundamental: permitir que você acesse e manipule partes de uma coleção sem precisar copiar os dados ou tomar posse dela. Imagine que você queira escrever uma função que encontra a primeira palavra de uma string. Sem slices, você teria que retornar uma String nova, copiando os caracteres, o que é ineficiente para strings grandes. Com slices, você pode retornar uma referência para a parte original, sem cópia.

Além disso, slices fornecem uma abstração uniforme para diferentes tipos de coleções. Por exemplo, uma função que opera em &[T] pode ser usada com arrays, vetores e até mesmo com slices de slices. Isso promove a reutilização de código e torna as APIs mais genéricas.

Outro aspecto importante é a segurança. Como os slices são referências, eles são verificados pelo compilador para garantir que não haja referências pendentes (dangling references) e que as regras de ownership sejam respeitadas. Se você tentar usar um slice após a coleção original ter sido movida ou destruída, o compilador emitirá um erro. Por exemplo:

let s = String::from("hello");
let slice = &s[..];
drop(s);  // erro: não é possível usar slice depois que s foi movido/destruído
println!("{}", slice);

Isso evita uma classe inteira de bugs comuns em linguagens como C e C++.

Um exemplo clássico é a função first_word que retorna a primeira palavra de uma string. Sem slices, você teria que retornar o índice do fim da palavra, mas isso é frágil. Com slices, você retorna a própria palavra:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s);
    println!("{}", word);  // "hello"
}

Observe que a função aceita &str, que pode ser uma referência a uma String ou a um literal. Isso a torna flexível.

Pegadinhas

Ao usar slices, você pode encontrar algumas armadilhas. Vamos discutir as mais comuns.

1. Índices de bytes vs. caracteres: Como mencionado, strings são UTF-8, então os índices são em bytes. Se você tentar fatiar uma string que contém caracteres multi-byte (como acentos ou emojis) no meio de um caractere, o Rust irá entrar em pânico. Por exemplo:

let s = String::from("Olá");
let slice = &s[0..2];  // isso vai falhar porque 'á' ocupa 2 bytes em UTF-8, e o byte 2 está no meio do caractere.

Para evitar isso, você deve usar métodos que operam em caracteres, como char_indices() ou split_at com cuidado. Na prática, para obter uma substring segura, é melhor usar s.get(0..2) que retorna Option<&str>.

2. Slices de arrays mutáveis e imutáveis conflitantes: As regras de ownership se aplicam a slices. Você não pode ter um slice mutável e um imutável da mesma região ao mesmo tempo. Por exemplo, isso é um erro:

let mut arr = [1, 2, 3];
let s1 = &arr[..];
let s2 = &mut arr[..];  // erro: não é possível emprestar arr como mutável porque também está emprestado como imutável

Isso é uma proteção contra corridas de dados.

3. Slices não possuem ownership: Lembre-se de que um slice é uma referência, então ele não pode sobreviver à coleção original. Se você tentar retornar um slice de uma função que cria uma String local, o compilador rejeitará porque a String será destruída ao final da função e o slice ficaria pendente. Por exemplo:

fn cria_slice() -> &str {
    let s = String::from("hello");
    &s[..]  // erro: s não vive o suficiente
}

Nesse caso, você precisa retornar a String ou usar um tipo que possua os dados, como String.

4. Range exclusivo: Lembre-se de que o range a..b exclui o índice b. Isso é fácil de esquecer. Para incluir o último elemento, use ..= (range inclusivo) ou .. para até o final.

5. Slices de arrays com tamanho fixo vs. slices dinâmicos: Quando você cria um slice de um array como &arr[..], o tipo resultante é &[i32], que é um slice de comprimento dinâmico, não um array de tamanho fixo. Isso pode causar confusão, mas é intencional: você perde a informação do tamanho em tempo de compilação, mas ganha flexibilidade.

6. Usar as_bytes() para iterar sobre bytes: Ao fazer fatiamento de strings, é comum converter para bytes para iterar, mas lembre-se de que as_bytes() retorna um slice de bytes, não de caracteres. Se você precisar de caracteres, use chars().

Para ilustrar a pegadinha de índices, vejamos um exemplo que usa get para evitar pânico:

fn main() {
    let s = String::from("Olá mundo");
    match s.get(0..2) {
        Some(sub) => println!("Substring: {}", sub),
        None => println!("Falhou - índice inválido"),
    }
}

Aqui, s.get(0..2) retorna None porque o byte 2 está no meio de 'O'? Na verdade, 'O' é ASCII e ocupa 1 byte, 'l' ocupa 1 byte, então 0..2 é válido e retorna "Ol". Mas se tentarmos 0..3, pegaria 'O' (1 byte) + 'l' (1 byte) + o primeiro byte de 'á', o que é inválido e retornaria None.

Boas práticas:

  • Use &str em parâmetros de função quando você só precisa ler a string, em vez de &String. Isso torna sua função mais genérica.
  • Prefira &[T] em vez de &Vec<T> em APIs para aceitar qualquer tipo de contêiner.
  • Quando for fatiar strings, sempre considere a codificação UTF-8. Se não tiver certeza, use métodos que lidam com caracteres.
  • Evite manter slices por muito tempo se a coleção subjacente pode ser modificada; o borrow checker vai ajudar, mas é bom ter isso em mente.

Referências

Exercícios

  1. Escreva uma função primeira_palavra que recebe uma string slice e retorna a primeira palavra (até o primeiro espaço). Se não houver espaço, retorna a string inteira. Teste com "hello world" e "rust".

✓ Resposta:
fn primeira_palavra(s: &str) -> &str {
    match s.find(' ') {
        Some(pos) => &s[..pos],
        None => s,
    }
}

fn main() {
    let s1 = "hello world";
    let s2 = "rust";
    println!("{}", primeira_palavra(s1)); // hello
    println!("{}", primeira_palavra(s2)); // rust
}
  • Dado um array de inteiros, escreva uma função que retorna o maior valor em um slice específico (por exemplo, do índice 2 ao 5). A função deve receber um slice e retornar o maior elemento.
  • ✓ Resposta:
    fn maior_no_slice(slice: &[i32]) -> i32 {
        let mut maior = slice[0];
        for &item in slice.iter().skip(1) {
            if item > maior {
                maior = item;
            }
        }
        maior
    }
    
    fn main() {
        let arr = [1, 5, 3, 9, 2, 7];
        let slice = &arr[2..5]; // [3, 9, 2]
        println!("{}", maior_no_slice(slice)); // 9
    }
  • Explique o que acontece quando você tenta compilar o seguinte código e por quê:
    fn main() {
        let s = String::from("hello");
        let r = &s;
        let x = &mut s;
    }
  • ✓ Resposta: O código não compila porque viola as regras de borrowing: não é possível ter uma referência imutável (&s) e uma referência mutável (&mut s) para o mesmo valor ao mesmo tempo. O compilador emite o erro E0502: "cannot borrow `s` as mutable because it is also borrowed as immutable". Isso previne corridas de dados.
  • Escreva uma função que recebe uma string e retorna o número de palavras (separadas por espaços). Use slices para iterar sobre as palavras. Dica: use o método split_whitespace().
  • ✓ Resposta:
    fn contar_palavras(s: &str) -> usize {
        s.split_whitespace().count()
    }
    
    fn main() {
        let texto = "Rust é uma linguagem incrível";
        println!("Número de palavras: {}", contar_palavras(texto)); // 5
    }
  • Dado o seguinte código, qual é o tipo de slice e qual é o valor impresso?
    fn main() {
        let v = vec![1, 2, 3, 4, 5];
        let slice = &v[1..4];
        println!("{:?}", slice);
    }
  • ✓ Resposta: O tipo de slice é &[i32] (um slice de inteiros). O valor impresso é [2, 3, 4] porque o range 1..4 inclui os índices 1, 2 e 3 (exclui o 4).