Vetores
O primeiro tipo que iremos ver é Vec<T>
, também conhecido como vetor. Vetores
permitem guardar mais de um valor na mesma estrutura de dados que coloca todos
os valores um ao lado do outro na memória. Vetores só podem guardar valores do
mesmo tipo. Eles são úteis em situações onde há uma lista de itens, como
as linha de texto em um arquivo ou 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
:
#![allow(unused)] fn main() { let v: Vec<i32> = Vec::new(); }
Note que adicionamos uma anotação de tipo aqui. Como não estamos inserindo nenhum valor
no vetor, Rust não sabe o tipo de elementos que irá guardar.
Isto é um ponto importante. Vetores são homogêneos: eles podem guardar muitos
valores, mas todos esses valores devem ser do mesmo tipo. Vetores são implementados
usando genéricos, onde o capítulo 10 irá cobrir como usar em seus tipos. Por
agora, tudo o que precisa saber é que o tipo Vec
fornecido pela biblioteca
padrão pode conter qualquer tipo, e quando um Vec
específico possui um tipo específico, o
tipo vai dentro de < >
. Falamos para Rust que Vec
em v
guardará
elementos do tipo i32
.
No código real, a Rust pode inferir o tipo de valor que queremos armazenar uma vez que inserimos
valores, então você raramente precisa fazer essa anotação de tipo. É mais comum
criar um Vec
que possui valores iniciais, e o Rust fornece a macro vec!
por
conveniência. A macro criará um novo Vec
que contém os valores que damos
. Isso criará um novo Vec <i32>
que contém os valores 1
, 2
e 3
:
#![allow(unused)] fn main() { let v = vec![1, 2, 3]; }
Como nós damos valores iniciais i32
, Rust pode inferir que o tipo de v
é Vec <i32>
, e a anotação de tipo não é necessária. Vejamos a seguir como
modificar um vetor.
Modificando um Vetor
Para criar um vetor e adicionar elementos a ele, podemos usar o método push
:
#![allow(unused)] fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
Como qualquer variável que discutimos no Capítulo 3, se quisermos poder
alterar seu valor, precisamos fazê-lo mutável com a palavra-chave mut
. Os
números que inserimos são todos do tipo i32
, e Rust infere isso dos
dados, por isso não precisamos da anotação Vec <i32>
.
Descartar um Vetor Descarta seus Elementos
Como qualquer outro struct
, um vetor será liberado quando ele sair do escopo:
#![allow(unused)] fn main() { { let v = vec![1, 2, 3, 4]; // use as informações em v } // <- v sai do escopo e é liberado aqui }
Quando o vetor é descartado, todos os seus conteúdos também será descartado, o que significa esses inteiros que ele contém serão limpos. Isso pode parecer um ponto direto, mas pode ficar um pouco mais complicado quando começamos a introduzir referências aos elementos do vetor. Vamos abordar isso em seguida!
Lendo Elementos do Vetor
Agora que você sabe como criar, atualizar e destruir vetores, saber ler o seu conteúdo é um bom passo seguinte. Existem duas maneiras de fazer referência a valores armazenados em um vetor. Nos exemplos, anotamos os tipos de valores que são retornados dessas funções para maior clareza.
Este exemplo mostra os dois métodos de acesso a um valor em um vetor com
sintaxe de indexação ou o método get
:
#![allow(unused)] fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; let third: Option<&i32> = v.get(2); }
Há algumas coisas a serem observadas aqui. Primeiro, que usamos o valor do índice de 2
para obter o terceiro elemento: os vetores são indexados por número, começando em zero.
Em segundo lugar, as duas maneiras diferentes de obter o terceiro elemento são: usando &
e
[]
, que nos dá uma referência, ou usando o método get
com o índice
passado como um argumento, o que nos dá uma Option<&T>
.
A razão pela qual Rust tem duas maneiras de fazer referência a um elemento é para que você possa escolher como o programa se comporta quando você tenta usar um valor de índice para o qual o vetor não tem um elemento correspondente. Por exemplo, o que um programa deve fazer se tiver um vetor que contém cinco elementos, então tenta acessar um elemento no índice 100 dessa maneira:
#![allow(unused)] fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); }
Quando você executar isso, você verá que com o primeiro método []
, Rust irá
causar um panic!
quando um elemento inexistente é referenciado. Este método seria
preferível se você quiser que seu programa considere uma tentativa de acessar um
elemento, passado o fim do vetor, para ser um erro fatal que deve finalizar o
programa.
Quando é passado um índice que está fora da matriz para o método get
, ele
retorna None
sem entrar em pânico. Você usaria isso se acessar um elemento
além do alcance do vetor ocorrerá ocasionalmente sob
circunstâncias normais. Seu código pode então ter lógica para lidar tanto com
Some(&element)
ou None
, como discutimos no Capítulo 6. Por exemplo, o
O índice pode ser proveniente de uma pessoa que digite um número. Se eles acidentalmente
insira um número que é muito grande e seu programa recebe um valor None
, você poderia
dizer ao usuário quantos itens estão no atual Vec
e dar uma nova
chance de inserir um valor válido. Isso seria mais amigável do que quebrar o
programa por um erro de digitação!
Referências Inválidas
Uma vez que o programa tenha uma referência válida, o verificador de empréstimo (borrow checker) faz valer as regras de propriedade e empréstimo abrangidas no Capítulo 4 para garantir que essa referência e quaisquer outras referências aos conteúdos do vetor permaneçam válidas. Lembre-se da regra que diz que não podemos ter referências mutáveis e imutáveis no mesmo escopo. Essa regra se aplica neste exemplo, onde mantemos uma referência imutável ao primeiro elemento em um vetor e tentamos adicionar um elemento ao final:
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
Compilar isso nos dará esse erro:
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^ mutable borrow occurs here
7 | }
| - immutable borrow ends here
Este código pode parecer que deveria funcionar: por que uma referência ao primeiro elemento deveria se preocupar com o que muda sobre o final do vetor? A razão porque este código não é permitido é devido à forma como os vetores funcionam. Adicionando um novo elemento no final do vetor pode exigir a atribuição de nova alocação de memória e copiar os elementos antigos para o novo espaço, na circunstância de não haver espaço suficiente para colocar todos os elementos próximos um do outro onde o vetor estava. Nesse caso, a referência ao primeiro elemento apontaria para memória não alocada. As regras de empréstimo impedem que os programas acabem nessa situação.
Nota: para mais informações, veja o Rustonomicon em https://doc.rust-lang.org/stable/nomicon/vec.html.
Usando um Enum para Armazenar Vários Tipos
No início deste capítulo, dissemos que os vetores só podem armazenar valores que são todos do mesmo tipo. Isso pode ser inconveniente; definitivamente há casos de uso para a necessidade de armazenar uma lista de coisas de diferentes tipos. Felizmente, as variantes de um enum são todas definidas sob o mesmo tipo de enum, então, quando precisamos armazenar elementos de um tipo diferente em um vetor, podemos definir e usar um enum!
Por exemplo, digamos que queremos obter valores de uma linha em uma planilha, onde algumas das colunas da linha contêm números inteiros, alguns números de ponto flutuante, e algumas strings. Podemos definir um enum cujas variantes guardarão os diferentes tipos de valor, e então todas as variantes de enum serão consideradas do mesmos tipo, o do enum. Então, podemos criar um vetor que contenha esse enum e então, em última instância, possui diferentes tipos:
#![allow(unused)] fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; }
A razão pela qual Rust precisa saber exatamente quais tipos estarão no vetor em
tempo de compilação é para que ele saiba exatamente a quantidade de memória no heap que será
necessária para armazenar cada elemento. Uma vantagem secundária para isso é que podemos ser
explícitos sobre quais tipos são permitidos neste vetor. Se Rust permitisse um vetor
guardar qualquer tipo, haveria uma chance de que um ou mais dos tipos
causar erros com as operações realizadas nos elementos do vetor. Usando
um enum mais um match
significa que a Rust garantirá no tempo de compilação que nós
sempre lidaremos com todos os casos possíveis, como discutimos no Capítulo 6.
Se você não sabe no momento em que você está escrevendo um programa, o conjunto exaustivo dos tipos que o programa irá precisar no tempo de execução para armazenar em um vetor, a técnica de usar o enum não funcionará. Em vez disso, você pode usar um objeto trait, que abordaremos no Capítulo 17.
Agora que examinamos algumas das maneiras mais comuns de usar vetores, certifique-se
para dar uma olhada na documentação da API para todos os muitos métodos úteis
definidos no Vec
pela biblioteca padrão. Por exemplo, além de push
existe um método pop
que irá remover e retornar o último elemento. Vamos mover
para o próximo tipo de coleção: String
!