Pular para o conteúdo

Slices em Rust

Um slice (fatia) permite pegar emprestada uma parte de um array ou vetor, uma espécie de “janela” para um trecho dele, sem copiar os dados e sem tomar o ownership. Se você já usou borrowing com um único valor, o slice é a mesma ideia aplicada a uma sequência de elementos.

fn main() {
let numbers = [10, 20, 30, 40, 50];
let middle = &numbers[1..4]; // elementos nos índices 1, 2 e 3
println!("{:?}", middle);
}

Saída:

[20, 30, 40]
▶ Experimente você mesmo

1..4 é o mesmo tipo de intervalo que você viu nos laços for: começa no índice 1 e termina antes do índice 4. Ou seja, você recebe os índices 1, 2 e 3.

Alguns atalhos que aparecem bastante:

fn main() {
let numbers = [10, 20, 30, 40, 50];
println!("{:?}", &numbers[..3]); // do início até o índice 3 -> [10, 20, 30]
println!("{:?}", &numbers[2..]); // do índice 2 até o fim -> [30, 40, 50]
println!("{:?}", &numbers[..]); // tudo -> [10, 20, 30, 40, 50]
}
▶ Experimente você mesmo

Na verdade, você já estava usando slices: &str é um slice de uma String (ou de outro texto)! É por isso que os dois tipos combinam tão bem:

fn main() {
let sentence = String::from("The quick brown fox");
let first_word = &sentence[0..3];
println!("{first_word}"); // "The"
}
▶ Experimente você mesmo

Deixamos a frase em inglês de propósito. Em uma string, os índices de [0..3] contam bytes, não letras, e letras acentuadas como “ã” ou “é” ocupam 2 bytes cada. Com um texto em português, o corte poderia cair no meio de uma letra, e o programa daria panic.

Dá para imaginar resolver isso copiando o trecho que você quer para um novo Vec ou uma nova String. Os slices evitam isso completamente: eles só apontam para uma parte dos dados que já existem, do mesmo jeito que uma referência (&) aponta para um único valor. Isso significa nenhuma alocação extra, nenhuma cópia, e as regras de borrowing que você já conhece da página de Borrowing continuam valendo.

Slices são especialmente úteis como parâmetros de função, porque aceitam tanto arrays quanto vetores:

fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for n in numbers {
total += n;
}
total
}
fn main() {
let array = [1, 2, 3];
let vector = vec![4, 5, 6];
println!("{}", sum(&array));
println!("{}", sum(&vector));
}
▶ Experimente você mesmo

&[i32] aceita um slice vindo tanto de um array quanto de um Vec: uma única função, e as duas chamadas funcionam.

Até aqui, nossos slices só leram dados. Um slice mutável, &mut [T], permite que uma função altere os itens que recebeu, mas só esses itens:

fn double_all(numbers: &mut [i32]) {
for i in 0..numbers.len() {
numbers[i] *= 2;
}
}
fn main() {
let mut scores = vec![1, 2, 3, 4, 5];
double_all(&mut scores[1..3]); // só os índices 1 e 2
println!("{:?}", scores);
double_all(&mut scores); // o vetor inteiro
println!("{:?}", scores);
}

Saída:

[1, 4, 6, 4, 5]
[2, 8, 12, 8, 10]
▶ Experimente você mesmo

A primeira chamada passa &mut scores[1..3], então double_all só enxerga os índices 1 e 2. Tudo o que está fora desse intervalo fica protegido dela. Dentro da função, o slice volta a começar no índice 0: para double_all, é só uma lista de dois números.

Isso é seguro por causa de uma regra que você vai conhecer direito na página de Borrowing: enquanto a função segura um slice &mut, nada mais pode usar esses itens, então duas partes do seu código nunca conseguem alterá-los ao mesmo tempo. O vetor também precisa ser declarado com mut, senão o Rust não deixa você pegá-lo emprestado de forma mutável.

Crie o vetor vec![3, 6, 9, 12, 15, 18]. Imprima a primeira metade e a segunda metade dele como dois slices separados. Dica: calcule let mid = numbers.len() / 2; e use ..mid e mid...

Ver solução
fn main() {
let numbers = vec![3, 6, 9, 12, 15, 18];
let mid = numbers.len() / 2;
let first_half = &numbers[..mid];
let second_half = &numbers[mid..];
println!("Primeira metade: {:?}", first_half);
println!("Segunda metade: {:?}", second_half);
}

Saída:

Primeira metade: [3, 6, 9]
Segunda metade: [12, 15, 18]
▶ Experimente você mesmo

Como mid é calculado a partir de len(), o código continua funcionando se você adicionar ou remover números. Com um tamanho ímpar, o item que sobra fica na segunda metade.

Escreva uma função largest(numbers: &[i32]) -> i32 que devolve o maior número. Chame-a com um array, com um vetor e com um slice só do primeiro item do vetor. Dica: comece com numbers[0] e percorra o resto, como você faria com um array.

Ver solução
fn largest(numbers: &[i32]) -> i32 {
let mut biggest = numbers[0];
for n in numbers {
if *n > biggest {
biggest = *n;
}
}
biggest
}
fn main() {
let array = [7, 2, 9, 4];
let vector = vec![10, 35, 21];
println!("{}", largest(&array));
println!("{}", largest(&vector));
println!("{}", largest(&vector[..1]));
}

Saída:

9
35
10
▶ Experimente você mesmo

Percorrer um slice entrega uma referência para cada item (&i32), e não o número em si. *n quer dizer “o valor para o qual n aponta”, então *n > biggest compara dois números comuns. Esta versão dá panic com um slice vazio, porque numbers[0] não existe. Mais adiante você vai aprender a devolver um Option nesse caso.

  • Um slice (&[T]) pega emprestada uma parte de um array ou vetor sem copiá-la.
  • 1..4 seleciona os índices de 1 a 3; omita um dos lados do .. para ir desde o início ou até o fim.
  • &str é, ele mesmo, um slice de texto. Por isso strings e slices se comportam de forma tão parecida.
  • Prefira &[T] como tipo de parâmetro quando quiser aceitar tanto arrays quanto vetores.
  • Um slice mutável (&mut [T]) permite que uma função altere só os itens que recebeu.

Teste o que aprendeu

1. O que um slice permite fazer com uma parte de um array ou vetor?

2. Quais índices &numbers[1..4] inclui?

3. Por que &[i32] é uma boa escolha de tipo para um parâmetro de função?

Pontuação: 0 / 3