Introdução
Bem-vindo(a) a Armazenando Listas de Valores com Vetores. Este laboratório faz parte do Livro do Rust. Você pode praticar suas habilidades em Rust no LabEx.
"Neste laboratório, exploraremos o tipo de coleção Vec<T>, também conhecido como vetor, que permite armazenar listas de valores do mesmo tipo em uma única estrutura de dados."
Armazenando Listas de Valores com Vetores
O primeiro tipo de coleção que analisaremos é Vec<T>, também conhecido como vetor. Vetores permitem que você armazene mais de um valor em uma única estrutura de dados que coloca todos os valores lado a lado na memória. Vetores só podem armazenar valores do mesmo tipo. Eles são úteis quando você tem uma lista de itens, como as linhas de texto em um arquivo ou os preços de itens em um carrinho de compras.
Criando um Novo Vetor
Para criar um novo vetor vazio, chamamos a função Vec::new, como mostrado na Listagem 8-1.
let v: Vec<i32> = Vec::new();
Listagem 8-1: Criando um novo vetor vazio para armazenar valores do tipo i32
Observe que adicionamos uma anotação de tipo aqui. Como não estamos inserindo nenhum valor neste vetor, o Rust não sabe que tipo de elementos pretendemos armazenar. Este é um ponto importante. Vetores são implementados usando genéricos; abordaremos como usar genéricos com seus próprios tipos no Capítulo 10. Por enquanto, saiba que o tipo Vec<T> fornecido pela biblioteca padrão pode conter qualquer tipo. Quando criamos um vetor para conter um tipo específico, podemos especificar o tipo dentro de colchetes angulares. Na Listagem 8-1, dissemos ao Rust que o Vec<T> em v conterá elementos do tipo i32.
Mais frequentemente, você criará um Vec<T> com valores iniciais e o Rust inferirá o tipo de valor que você deseja armazenar, então você raramente precisará fazer essa anotação de tipo. O Rust convenientemente fornece a macro vec!, que criará um novo vetor que contém os valores que você fornecer. A Listagem 8-2 cria um novo Vec<i32> que contém os valores 1, 2 e 3. O tipo inteiro é i32 porque esse é o tipo inteiro padrão, como discutimos em "Tipos de Dados".
let v = vec![1, 2, 3];
Listagem 8-2: Criando um novo vetor contendo valores
Como fornecemos valores i32 iniciais, o Rust pode inferir que o tipo de v é Vec<i32>, e a anotação de tipo não é necessária. Em seguida, veremos como modificar um vetor.
Atualizando um Vetor
Para criar um vetor e, em seguida, adicionar elementos a ele, podemos usar o método push, como mostrado na Listagem 8-3.
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Listagem 8-3: Usando o método push para adicionar valores a um vetor
Como com qualquer variável, se quisermos ser capazes de alterar seu valor, precisamos torná-la mutável usando a palavra-chave mut, como discutido no Capítulo 3. Os números que colocamos dentro são todos do tipo i32, e o Rust infere isso dos dados, então não precisamos da anotação Vec<i32>.
Lendo Elementos de Vetores
Existem duas maneiras de referenciar um valor armazenado em um vetor: por indexação ou usando o método get. Nos exemplos a seguir, anotamos os tipos dos valores que são retornados dessas funções para maior clareza.
A Listagem 8-4 mostra ambos os métodos de acesso a um valor em um vetor, com sintaxe de indexação e o método get.
let v = vec![1, 2, 3, 4, 5];
1 let third: &i32 = &v[2];
println!("The third element is {third}");
2 let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
Listagem 8-4: Usando a sintaxe de indexação e o método get para acessar um item em um vetor
Observe alguns detalhes aqui. Usamos o valor do índice 2 para obter o terceiro elemento [1] porque os vetores são indexados por número, começando em zero. Usar & e [] nos dá uma referência ao elemento no valor do índice. Quando usamos o método get com o índice passado como um argumento [2], obtemos um Option<&T> que podemos usar com match.
O Rust fornece essas duas maneiras de referenciar um elemento para que você possa escolher como o programa se comporta quando tenta usar um valor de índice fora do intervalo de elementos existentes. Como exemplo, vamos ver o que acontece quando temos um vetor de cinco elementos e, em seguida, tentamos acessar um elemento no índice 100 com cada técnica, como mostrado na Listagem 8-5.
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
Listagem 8-5: Tentando acessar o elemento no índice 100 em um vetor contendo cinco elementos
Quando executamos este código, o primeiro método [] fará com que o programa entre em pânico porque referencia um elemento inexistente. Este método é melhor usado quando você deseja que seu programa trave se houver uma tentativa de acessar um elemento além do final do vetor.
Quando o método get recebe um índice que está fora do vetor, ele retorna None sem entrar em pânico. Você usaria este método se o acesso a um elemento além do intervalo do vetor pudesse acontecer ocasionalmente em circunstâncias normais. Seu código terá então lógica para lidar com Some(&element) ou None, como discutido no Capítulo 6. Por exemplo, o índice pode vir de uma pessoa digitando um número. Se eles digitarem acidentalmente um número muito grande e o programa receber um valor None, você poderá dizer ao usuário quantos itens estão no vetor atual e dar a eles outra chance de inserir um valor válido. Isso seria mais amigável do que travar o programa devido a um erro de digitação!
Quando o programa tem uma referência válida, o verificador de empréstimo (borrow checker) impõe as regras de propriedade e empréstimo (cobertas no Capítulo 4) para garantir que essa referência e quaisquer outras referências ao conteúdo do vetor permaneçam válidas. Lembre-se da regra que afirma que você não pode ter referências mutáveis e imutáveis no mesmo escopo. Essa regra se aplica na Listagem 8-6, onde mantemos uma referência imutável ao primeiro elemento em um vetor e tentamos adicionar um elemento ao final. Este programa não funcionará se também tentarmos nos referir a esse elemento mais tarde na função.
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
Listagem 8-6: Tentando adicionar um elemento a um vetor enquanto mantém uma referência a um item
A compilação deste código resultará neste erro:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ----- immutable borrow later used here
O código na Listagem 8-6 pode parecer que deveria funcionar: por que uma referência ao primeiro elemento se importaria com as alterações no final do vetor? Este erro se deve à forma como os vetores funcionam: como os vetores colocam os valores próximos uns dos outros na memória, adicionar um novo elemento ao final do vetor pode exigir a alocação de nova memória e a cópia dos elementos antigos para o novo espaço, se não houver espaço suficiente para colocar todos os elementos próximos uns dos outros onde o vetor está atualmente armazenado. Nesse caso, a referência ao primeiro elemento estaria apontando para a memória desalocada. As regras de empréstimo impedem que os programas acabem nessa situação.
Nota: Para obter mais informações sobre os detalhes de implementação do tipo
Vec<T>, consulte "The Rustonomicon" em https://doc.rust-lang.org/nomicon/vec/vec.html.
Iterando Sobre os Valores em um Vetor
Para acessar cada elemento em um vetor por sua vez, iteraríamos por todos os elementos em vez de usar índices para acessar um de cada vez. A Listagem 8-7 mostra como usar um loop for para obter referências imutáveis a cada elemento em um vetor de valores i32 e imprimi-los.
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
Listagem 8-7: Imprimindo cada elemento em um vetor iterando sobre os elementos usando um loop for
Também podemos iterar sobre referências mutáveis a cada elemento em um vetor mutável para fazer alterações em todos os elementos. O loop for na Listagem 8-8 adicionará 50 a cada elemento.
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
Listagem 8-8: Iterando sobre referências mutáveis a elementos em um vetor
Para alterar o valor ao qual a referência mutável se refere, temos que usar o operador de desreferência * para chegar ao valor em i antes de podermos usar o operador +=. Falaremos mais sobre o operador de desreferência em "Seguindo o Ponteiro para o Valor".
Iterar sobre um vetor, seja imutável ou mutavelmente, é seguro por causa das regras do verificador de empréstimo (borrow checker). Se tentássemos inserir ou remover itens nos corpos do loop for nas Listagens 8-7 e 8-8, obteríamos um erro do compilador semelhante ao que obtivemos com o código na Listagem 8-6. A referência ao vetor que o loop for mantém impede a modificação simultânea de todo o vetor.
Usando um Enum para Armazenar Múltiplos Tipos
Vetores só podem armazenar valores que são do mesmo tipo. Isso pode ser inconveniente; definitivamente existem casos de uso para a necessidade de armazenar uma lista de itens de tipos diferentes. Felizmente, as variantes de um enum são definidas sob o mesmo tipo enum, então, quando precisamos de um tipo para representar elementos de tipos diferentes, podemos definir e usar um enum!
Por exemplo, digamos que queremos obter valores de uma linha em uma planilha na qual algumas das colunas na linha contêm inteiros, alguns números de ponto flutuante e algumas strings. Podemos definir um enum cujas variantes conterão os diferentes tipos de valor, e todas as variantes do enum serão consideradas do mesmo tipo: o do enum. Então, podemos criar um vetor para conter esse enum e, assim, finalmente, conter diferentes tipos. Demonstramos isso na Listagem 8-9.
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
Listagem 8-9: Definindo um enum para armazenar valores de diferentes tipos em um vetor
O Rust precisa saber quais tipos estarão no vetor no tempo de compilação para que saiba exatamente quanta memória no heap será necessária para armazenar cada elemento. Também devemos ser explícitos sobre quais tipos são permitidos neste vetor. Se o Rust permitisse que um vetor contivesse qualquer tipo, haveria uma chance de que um ou mais dos tipos causasse erros com as operações realizadas nos elementos do vetor. Usar um enum mais uma expressão match significa que o Rust garantirá no tempo de compilação que todos os casos possíveis sejam tratados, como discutido no Capítulo 6.
Se você não souber o conjunto exaustivo de tipos que um programa obterá em tempo de execução para armazenar em um vetor, a técnica enum não funcionará. Em vez disso, você pode usar um objeto trait, que abordaremos no Capítulo 17.
Agora que discutimos algumas das maneiras mais comuns de usar vetores, certifique-se de revisar a documentação da API para todos os muitos métodos úteis definidos em Vec<T> pela biblioteca padrão. Por exemplo, além de push, um método pop remove e retorna o último elemento.
Descartando um Vetor Descarta Seus Elementos
Como qualquer outra struct, um vetor é liberado quando sai do escopo, conforme anotado na Listagem 8-10.
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
Listagem 8-10: Mostrando onde o vetor e seus elementos são descartados
Quando o vetor é descartado, todo o seu conteúdo também é descartado, o que significa que os inteiros que ele contém serão limpos. O verificador de empréstimo (borrow checker) garante que quaisquer referências ao conteúdo de um vetor sejam usadas apenas enquanto o próprio vetor for válido.
Vamos passar para o próximo tipo de coleção: String!
Resumo
Parabéns! Você concluiu o laboratório Armazenando Listas de Valores com Vetores. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.