Curso gratuito de Rust: A linguagem mais amada
Trabalhando com Slices: Referências Parciais
Aprenda sobre trabalhando com slices: referências parciais
🚀 Trabalhando com Slices: Referências Parciais em Rust
Olá, futuro mestre de Rust! 👋 Nesta aula prática, vamos mergulhar em um conceito super poderoso e seguro da linguagem: os slices. Slices são uma forma de referenciar uma parte de uma coleção, ao invés da coleção inteira, sem copiar os dados. Isso nos permite escrever código mais eficiente, flexível e, como sempre em Rust, seguro!
🎯 1. Introdução Clara: O que são Slices?
Imagine que você tem uma frase longa e quer apenas a primeira palavra. Ou um array de números e precisa trabalhar apenas com os últimos três. É exatamente para isso que os slices servem!
Um slice é uma referência a uma sequência contígua de elementos dentro de uma coleção (como uma String ou um Vec). Ele não possui os dados, apenas "empresta" uma visão para uma parte deles. Isso significa que:
- Não há cópia de dados: Slices são extremamente eficientes em termos de memória.
- Segurança garantida: O compilador Rust garante que as referências de slice sejam válidas e não causem "out-of-bounds" (acessos fora dos limites).
- Flexibilidade: Você pode escrever funções que aceitam diferentes tipos de coleções (como
Stringe&str, ouVec<T>e&[T]) de forma genérica.
Os slices são um tipo especial de empréstimo (borrowing), o que os conecta diretamente com o sistema de ownership que já estudamos. Eles nos permitem ter múltiplas referências imutáveis (&) ou uma única referência mutável (&mut) para uma parte dos dados.
Vamos começar a praticar!
📚 2. Explicação Detalhada com Exemplos
2.1. String Slices (&str)
O tipo mais comum de slice que você encontrará é o string slice, denotado como &str. Um &str é uma referência a uma parte de uma String ou a um literal de string.
A sintaxe para criar um slice é [starting_index..ending_index]. O starting_index é o ponto de partida do slice (inclusivo), e o ending_index é o ponto final (exclusivo).
fn main() {
let s = String::from("Olá, mundo!");
// Um slice da primeira palavra
let ola = &s[0..3]; // Inclui o índice 0, 1, 2. Exclui o 3.
println!("Primeira parte: {}", ola); // Saída: Olá
// Um slice da segunda palavra (considerando o espaço)
let mundo = &s[5..10]; // Inclui 5, 6, 7, 8, 9. Exclui o 10.
println!("Segunda parte: {}", mundo); // Saída: mundo
// Sintaxe de atalho: do início até um índice
let ola_completo = &s[..3]; // Equivalente a &s[0..3]
println!("Slice do início: {}", ola_completo); // Saída: Olá
// Sintaxe de atalho: de um índice até o final
let mundo_completo = &s[5..]; // Equivalente a &s[5..s.len()]
println!("Slice até o final: {}", mundo_completo); // Saída: mundo!
// Sintaxe de atalho: a string inteira como slice
let string_completa_slice = &s[..]; // Equivalente a &s[0..s.len()]
println!("Slice completo: {}", string_completa_slice); // Saída: Olá, mundo!
// Literais de string já são &str!
let literal_slice = "Este é um literal de string.";
println!("Literal: {}", literal_slice); // Saída: Este é um literal de string.
}Importante: Os índices de slice devem estar em limites de caracteres UTF-8 válidos. Se você tentar criar um slice no meio de um caractere multi-byte (como á, é, ç), o Rust entrará em pânico em tempo de execução. Para manipulação de strings em nível de caractere, é melhor usar métodos como .chars().
2.2. Funções com String Slices: O Exemplo first_word
Vamos ver um exemplo clássico do livro oficial do Rust, a função first_word. Inicialmente, ela pode ser escrita para retornar um índice:
// Código oficial do livro "The Rust Programming Language"
// Capítulo 4.3: The Slice Type
fn first_word_v1(s: &String) -> usize {
let bytes = s.as_bytes(); // Converte a string para um array de bytes
for (i, &item) in bytes.iter().enumerate() { // Itera sobre os bytes com seus índices
if item == b' ' { // Se encontrar um espaço...
return i; // ...retorna o índice do espaço
}
}
s.len() // Se não encontrar espaço, a palavra é a string inteira
}
fn main() {
let mut s = String::from("hello world");
let word_end_index = first_word_v1(&s); // word_end_index será 5
println!("O fim da primeira palavra está no índice: {}", word_end_index);
// Problema: E se 's' for modificada depois?
s.clear(); // Esvazia a String, tornando-a ""
// word_end_index ainda é 5, mas não faz mais sentido para a string vazia!
// println!("A primeira palavra é: {}", &s[0..word_end_index]); // Isso causaria um erro em tempo de execução!
}O problema com first_word_v1 é que ela retorna um usize que não tem nenhuma conexão com o estado da String original. Se a String for modificada, o índice se torna inválido.
É aqui que os slices brilham! Podemos reescrever first_word para retornar um &str:
// Código oficial do livro "The Rust Programming Language"
// Capítulo 4.3: The Slice Type (versão com slices)
fn first_word_v2(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i]; // Retorna um slice da primeira palavra
}
}
&s[..] // Retorna um slice da string inteira se não houver espaços
}
fn main() {
let mut s = String::from("hello world");
let word = first_word_v2(&s);
println!("A primeira palavra é: {}", word); // Saída: hello
// s.clear(); // Se tentarmos esvaziar 's' aqui, teremos um erro de compilação!
// Isso acontece porque 'word' tem um empréstimo imutável de 's'.
// Não podemos ter um empréstimo imutável e um empréstimo mutável (s.clear()) ao mesmo tempo.
// O compilador nos protege!
// Podemos usar 's' novamente depois que 'word' sair do escopo ou não for mais usado.
// Mas enquanto 'word' existir, 's' não pode ser modificada.
}Ainda melhor, podemos tornar first_word ainda mais genérica, aceitando um &str como parâmetro. Isso permite que ela funcione tanto com Strings (através de &s[..]) quanto com literais de string!
// Código oficial do livro "The Rust Programming Language"
// Capítulo 4.3: The Slice Type (versão mais genérica)
fn first_word_generic(s: &str) -> &str { // Aceita &str!
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word_generic funciona com um slice completo de String
let word_from_string = first_word_generic(&my_string[..]);
println!("Word from String: {}", word_from_string);
// first_word_generic funciona com um literal de string diretamente
let my_literal = "another example";
let word_from_literal = first_word_generic(my_literal);
println!("Word from Literal: {}", word_from_literal);
// first_word_generic também funciona com um slice parcial de String
let part_of_string = &my_string[6..]; // "world"
let word_from_part = first_word_generic(part_of_string);
println!("Word from part of String: {}", word_from_part);
}2.3. Outros Slices: Slices de Arrays (&[T]) e Vetores (&[T])
Slices não são exclusivos de strings! Eles podem ser usados com qualquer coleção contígua, como arrays e Vecs. O tipo de um slice genérico é &[T], onde T é o tipo dos elementos.
fn main() {
let a = [1, 2, 3, 4, 5];
// Slice de um array
let slice_array = &a[1..3]; // Contém [2, 3]
println!("Slice de array: {:?}", slice_array); // Saída: [2, 3]
// Slice de um vetor (Vec)
let v = vec![10, 20, 30, 40, 50];
let slice_vec = &v[2..4]; // Contém [30, 40]
println!("Slice de vetor: {:?}", slice_vec); // Saída: [30, 40]
// Função que aceita um slice genérico
fn process_slice(data: &[i32]) {
println!("Processando slice: {:?}", data);
println!("Primeiro elemento: {}", data[0]);
// data[0] = 99; // ERRO! Slices imutáveis não podem ser modificados.
}
process_slice(slice_array);
process_slice(slice_vec);
process_slice(&a[..]); // Passando o array inteiro como slice
process_slice(&v[..]); // Passando o vetor inteiro como slice
}2.4. Slices Mutáveis (&mut [T], &mut str)
Assim como referências comuns, você pode ter slices mutáveis. Um slice mutável permite que você modifique os dados subjacentes através do slice.
Regra de ouro do borrowing: Você só pode ter um slice mutável para uma coleção em um determinado escopo, OU vários slices imutáveis. Nunca ambos ao mesmo tempo!
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
// Criando um slice mutável
let slice_mut = &mut numbers[1..4]; // Contém [2, 3, 4]
println!("Slice mutável original: {:?}", slice_mut); // Saída: [2, 3, 4]
// Modificando elementos através do slice mutável
slice_mut[0] = 20; // O 2 original agora é 20
slice_mut[2] = 40; // O 4 original agora é 40
println!("Slice mutável modificado: {:?}", slice_mut); // Saída: [20, 3, 40]
println!("Vetor original após modificação: {:?}", numbers); // Saída: [1, 20, 3, 40, 5]
// Exemplo de função que aceita slice mutável
fn double_elements(data: &mut [i32]) {
for item in data.iter_mut() {
*item *= 2; // Desreferencia e multiplica
}
}
let mut another_vec = vec![10, 20, 30];
println!("Antes de dobrar: {:?}", another_vec);
double_elements(&mut another_vec[..]); // Passa um slice mutável do vetor inteiro
println!("Depois de dobrar: {:?}", another_vec); // Saída: [20, 40, 60]
}🌐 3. Código de Exemplo Oficial
Os exemplos que usamos para first_word e a sintaxe de slices são diretamente adaptados do Capítulo 4.3 do livro "The Rust Programming Language". Esta é a fonte mais oficial e recomendada para aprender Rust!
🤝 4. Integração com Múltiplas Tecnologias (N/A)
Este tópico, "Trabalhando com Slices", é um conceito fundamental e intrínseco à linguagem Rust. Ele não envolve a integração de múltiplas tecnologias ou frameworks externos como Express.js ou Better-Auth. O foco aqui é a compreensão aprofundada de como Rust gerencia a memória e as referências de forma segura e eficiente.
🏋️♀️ 5. Exercícios Práticos e Desafios
Chegou a hora de colocar a mão na massa e solidificar seu conhecimento sobre slices!
Exercício 1: Manipulando Slices de String e Array
Crie um novo projeto Rust (cargo new slices_exercicio_1).
Tarefas:
- Crie uma
Stringchamadafrasecom o valor"A linguagem Rust é incrível!". - Crie um slice
&strque contenha apenas a palavra "linguagem". - Crie um slice
&strque contenha a frase "Rust é incrível!". - Crie um array de números inteiros
[10, 20, 30, 40, 50, 60]. - Crie um slice
&[i32]que contenha os três elementos do meio do array (20, 30, 40). - Imprima todos os slices criados.
// Dica: Use println! com o placeholder {:?} para imprimir arrays e slices.
// Exemplo: println!("Meu slice: {:?}", meu_slice);Exercício 2: Encontrando a Segunda Palavra
Crie uma função second_word que recebe um &str e retorna um &str contendo a segunda palavra da string. Se não houver uma segunda palavra, retorne um slice vazio ("").
Tarefas:
- Defina a função
second_word(s: &str) -> &str. - Dentro da função, converta a string para bytes (
s.as_bytes()). - Itere sobre os bytes para encontrar o primeiro espaço.
- Se encontrar o primeiro espaço, continue iterando a partir do índice seguinte para encontrar o segundo espaço.
- Retorne o slice correspondente à segunda palavra.
- Teste sua função com as seguintes entradas no
main:"hello world"(deve retornar "world")"rust programming"(deve retornar "programming")"one"(deve retornar "")"one two three"(deve retornar "two")" leading spaces"(deve retornar "leading")"trailing spaces "(deve retornar "spaces")
// Dica: Você pode precisar de uma variável de estado para saber se já encontrou o primeiro espaço.
// Dica 2: Cuidado com múltiplos espaços consecutivos! Considere-os como um único delimitador.Exercício 3: Invertendo um Slice Mutável
Crie uma função reverse_slice que recebe um slice mutável de inteiros (&mut [i32]) e inverte a ordem dos elementos dentro desse slice.
Tarefas:
- Defina a função
reverse_slice(arr: &mut [i32]). - Implemente a lógica para inverter os elementos do slice (ex: usando dois ponteiros, um no início e outro no final, trocando os elementos).
- No
main, crie umVec<i32>e chamereverse_slicecom um slice parcial e com um slice completo desse vetor. - Imprima o vetor antes e depois da inversão.
// Exemplo de uso no main:
// let mut data = vec![1, 2, 3, 4, 5];
// println!("Original: {:?}", data);
// reverse_slice(&mut data[1..4]); // Inverte apenas [2, 3, 4]
// println!("Parcialmente invertido: {:?}", data); // Deve ser [1, 4, 3, 2, 5]💡 Desafio (Opcional): Palíndromo com Slices e Iterators
Crie uma função is_palindrome que recebe um &str e retorna true se a string for um palíndromo (lê-se da mesma forma de trás para frente), ignorando espaços e case-sensitivity (maiúsculas/minúsculas).
Tarefas:
- Defina a função
is_palindrome(s: &str) -> bool. - Converta a string para minúsculas e filtre caracteres não-alfabéticos.
- Use slices ou iterators para comparar a string normal com a string invertida.
- Teste com exemplos como:
"Madam"(true)"A man, a plan, a canal: Panama"(true)"hello"(false)"Racecar"(true)"No lemon, no melon"(true)
// Dicas:
// - Use `s.chars().filter(|c| c.is_alphabetic()).map(|c| c.to_ascii_lowercase())` para pré-processar a string.
// - Colete os caracteres filtrados em um `Vec<char>` para facilitar o acesso por índice ou para criar um slice.
// - Você pode usar `collect::<Vec<char>>()` para transformar um iterator em um vetor.
// - Compare o vetor com seu reverso.📝 6. Resumo e Próximos Passos
Parabéns! 🎉 Você dominou o conceito de slices em Rust.
Pontos chave:
- Slices são referências a partes contíguas de coleções.
- Eles não possuem os dados, apenas os "emprestam", tornando-os eficientes.
- String slices (
&str) são a forma padrão de referenciar partes de strings ou literais. - Slices genéricos (
&[T]) funcionam para arrays eVecs. - Você pode ter slices mutáveis (
&mut [T]) para modificar os dados subjacentes, mas com as regras de borrowing (um mutável OU vários imutáveis). - Slices nos ajudam a escrever código mais seguro, flexível e genérico.
Os slices são fundamentais para escrever código idiomático em Rust e você os verá em todo lugar. Eles são uma manifestação clara de como Rust equilibra poder e segurança.
Próximos Passos:
No próximo módulo, vamos explorar Structs e Enums, que são as ferramentas de Rust para criar tipos de dados personalizados e organizar seu código de forma mais robusta e expressiva. Prepare-se para construir suas próprias estruturas de dados! 🚀