Slices em Rust
LidoEsta aula explora o conceito de slices em Rust, abordando string slices (&str), slices de arrays e a motivação por trás deles. Discutimos como slices fornecem referências seguras a partes de coleções sem cópia, e destacamos pegadinhas comuns para evitar erros de compilação e de lógica.
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
&strem 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
- The Rust Programming Language - Slices
- Rust Standard Library - slice
- Rust Standard Library - str
- Rust Reference - Slice types
- Rust by Example - Slices
- The Rust Programming Language - Strings
Exercícios
- Escreva uma função
primeira_palavraque 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".
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
}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
}fn main() {
let s = String::from("hello");
let r = &s;
let x = &mut s;
}&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.split_whitespace().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
}slice e qual é o valor impresso? fn main() {
let v = vec![1, 2, 3, 4, 5];
let slice = &v[1..4];
println!("{:?}", slice);
}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).