quarta-feira, 14 de abril de 2021

Erros recuperáveis com Result em Rust

A maioria dos erros não é grave o suficiente para exigir que o programa seja totalmente interrompido. Às vezes, quando uma função falha, é por um motivo que você pode interpretar e responder facilmente. Por exemplo, se você tentar abrir um arquivo e essa operação falhar porque o arquivo não existe, convém criar o arquivo em vez de encerrar o processo.

Lembre-se de "Lidando com a falha potencial com o Result tipo" no Capítulo 2, que o enum Result é definido como tendo duas variantes Ok e Err, da seguinte forma:


#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

Os parâmetros de tipo T e E são genéricos: discutiremos os genéricos com mais detalhes no Capítulo 10. O que você precisa saber agora é que T representa o tipo do valor que será retornado em um caso de sucesso dentro da variante Ok e E representa o tipo do erro que será retornado em um caso de falha na variante Err. Por Result ter esses parâmetros de tipo genérico, podemos usar o tipo Result e as funções que a biblioteca padrão definiu nele em muitas situações diferentes onde o valor de sucesso e o valor de erro que desejamos retornar podem ser diferentes.

Vamos chamar uma função que retorna um valor Result porque a função pode falhar. Na Listagem 9-3, tentamos abrir um arquivo.

Nome do arquivo: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}

Listagem 9-3: Abrindo um arquivo

Como sabemos que File::open retorna um Result? Podemos olhar a documentação da API da biblioteca padrão ou perguntar ao compilador! Se dermos a f uma anotação de tipo que sabemos não ser o tipo de retorno da função e, em seguida, tentarmos compilar o código, o compilador nos dirá que os tipos não correspondem. A mensagem de erro, então, dizer-nos o que o tipo de f é. Vamos tentar! Sabemos que o tipo de retorno de File::open não é do tipo u32, então vamos mudar a instrução let f para isto:

Esse código não compila Esse código não compila.

use std::fs::File;

fn main() {
    let f: u32 = File::open("hello.txt");
}

A tentativa de compilar agora nos dá a seguinte saída:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |            ---   ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `std::result::Result`
  |            |
  |            expected due to this
  |
  = note: expected type `u32`
             found enum `std::result::Result<File, std::io::Error>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `error-handling`

To learn more, run the command again with --verbose.

Isso nos diz que o tipo de retorno da função File::open é Result<T, E>. O parâmetro genérico T foi preenchido aqui com o tipo do valor de sucesso std::fs::File, que é um identificador de arquivo. O tipo de E usado no valor de erro é std::io::Error.

Este tipo de retorno significa que a chamada para File::open pode ser bem-sucedida e retornar um identificador de arquivo do qual podemos ler ou gravar. A chamada de função também pode falhar: por exemplo, o arquivo pode não existir ou podemos não ter permissão para acessar o arquivo. A função File::open precisa ter uma maneira de nos dizer se foi bem-sucedida ou falhou e, ao mesmo tempo, nos fornecer o identificador de arquivo ou as informações de erro. Essa informação é exatamente o que o enum Result transmite.

Caso File::open seja bem-sucedido, o valor na variável f será uma instância de Ok que contém um identificador de arquivo. No caso de falha, o valor em f será uma instância de Err que contém mais informações sobre o tipo de erro ocorrido.

Precisamos adicionar ao código na Listagem 9-3 para realizar ações diferentes dependendo do valor File::openretornado. A Listagem 9-4 mostra uma maneira de lidar com o Resultuso de uma ferramenta básica, a matchexpressão que discutimos no Capítulo 6.

Nome do arquivo: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problema ao abrir o arquivo: {:?}", error),
    };
}

Listagem 9-4: Usando uma expressão match para lidar com as variantes de Result que podem ser retornadas

Observe que, como o enum Option, o enum Result e suas variantes foram trazidos ao escopo pelo prelúdio, portanto, não precisamos especificar Result:: antes das variantes Ok e Err nas ramificações de match.

Aqui, dizemos a Rust que, quando o resultado é Ok, deve ser retornado o valor file interno da variante Ok e, em seguida, atribuímos esse valor de identificador de arquivo à variável f. Após o match, podemos usar o identificador de arquivo para leitura ou gravação.

A outra ramificação do match trata o caso de onde obtemos um valor Err do File::open. Neste exemplo, optamos por chamar a macro panic!. Se não houver um arquivo chamado hello.txt em nosso diretório atual e executarmos esse código, veremos a seguinte saída da macro panic!:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`
thread 'main' panicked at 'Problema ao abrir o arquivo: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Como de costume, essa saída nos diz exatamente o que deu errado.

Correspondência em erros diferentes

O código na Listagem 9-4 panic! não importa o motivo da falha File::open. O que queremos fazer, em vez disso, é executar ações diferentes por motivos de falha diferentes: se File::open falhar porque o arquivo não existe, queremos criar o arquivo e retornar o identificador para o novo arquivo. Se File::open falhou por qualquer outro motivo - por exemplo, porque não tínhamos permissão para abrir o arquivo - ainda queremos que o código entre em panic! da mesma forma como fez na Listagem 9-4. Observe a Listagem 9-5, que adiciona uma expressão match interna .

Nome do arquivo: src/main.rs

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problema ao criar o arquivo: {:?}", e),
            },
            other_error => {
                panic!("Problema ao abrir o arquivo: {:?}", other_error)
            }
        },
    };
}

Listagem 9-5: Lidando com diferentes tipos de erros de maneiras diferentes

O tipo de valor que File::open retorna dentro da variante Err é io::Error, que é uma estrutura fornecida pela biblioteca padrão. Essa estrutura tem um método kind que podemos chamar para obter um valor io::ErrorKind. O enum io::ErrorKind é fornecido pela biblioteca padrão e tem variantes que representam os diferentes tipos de erros que podem resultar de uma operação io. A variante que queremos usar é ErrorKind::NotFound, que indica que o arquivo que estamos tentando abrir ainda não existe. Então nós combinamos f, mas também temos uma correspondência interna error.kind().

A condição que queremos verificar na correspondência interna é se o valor retornado por error.kind() é a variante NotFound do enum ErrorKind. Se for, tentamos criar o arquivo com File::create. No entanto, porque File::create também pode falhar, precisamos de uma segunda ramificação na expressão match interna. Quando o arquivo não pode ser criado, uma mensagem de erro diferente é impressa. A segunda ramificação do match externo permanece o mesmo, então o programa entra em pânico com qualquer erro além do erro de arquivo ausente.

Isso é muito match! A expressão match é muito útil, mas também muito primitiva. No Capítulo 13, você aprenderá sobre fechamentos; o tipo Result<T, E> tem muitos métodos que aceitam um encerramento e são implementados usando expressões match. Usar esses métodos tornará seu código mais conciso. Um Rustáceo mais experiente pode escrever este código em vez da Listagem 9-5:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problema ao criar o arquivo: {:?}", error);
            })
        } else {
            panic!("Problema ao abrir o arquivo: {:?}", error);
        }
    });
}

Embora esse código tenha o mesmo comportamento da Listagem 9-5, ele não contém nenhuma expressão match e é mais limpo de ler. Volte a este exemplo depois de ler o Capítulo 13 e procure o método unwrap_or_else na documentação da biblioteca padrão. Muitos mais desses métodos podem limpar grandes expressões match aninhadas quando você está lidando com erros.

Atalhos para Pânico em caso de erro: unwrap e expect

Usar match funciona bem, mas pode ser um pouco prolixo e nem sempre comunica bem a intenção. O tipo Result<T, E> possui muitos métodos auxiliares definidos para realizar várias tarefas. Um desses métodos, chamado unwrap, é um método de atalho implementado exatamente como a expressão match que escrevemos na Listagem 9-4. Se o valor Result for a variante Ok, unwrap retornará o valor dentro de Ok. Se Result for a variante Err, unwrap chamará a macro panic! para nós. Aqui está um exemplo de ação unwrap:

Nome do arquivo: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

Se executarmos este código sem um arquivo hello.txt, veremos uma mensagem de erro da chamada de panic! que o método unwrap faz:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

Outro método, expect que é semelhante a unwrap, também nos permite escolher a mensagem panic! de erro. Usar expect em vez de unwrap e fornecer boas mensagens de erro pode transmitir sua intenção e facilitar o rastreamento da origem de um pânico. A sintaxe de expect é semelhante a esta:

Nome do arquivo: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Falha ao abrir o arquivo hello.txt");
}

Usamos expect da mesma maneira que unwrap: para retornar o identificador de arquivo ou chamar a macro panic!. A mensagem de erro usada por expect em sua chamada a panic! será o parâmetro que passamos para expect, ao invés da mensagem panic! padrão que unwrap usa. Esta é a aparência:

thread 'main' panicked at 'Falha ao abrir o arquivo hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4

Como essa mensagem de erro começa com o texto que especificamos, Falha ao abrir o arquivo hello.txt será mais fácil descobrir de onde no código essa mensagem de erro está vindo. Se usarmos unwrap em vários lugares, pode levar mais tempo para descobrir exatamente o que unwrap está causando o pânico, porque todas as chamadas unwrap que causam pânico imprimem a mesma mensagem.

Erros de propagação

Ao escrever uma função cuja implementação chama algo que pode falhar, em vez de tratar o erro dentro dessa função, você pode retornar o erro ao código de chamada para que ele decida o que fazer. Isso é conhecido como propagação do erro e fornece mais controle ao código de chamada, onde pode haver mais informações ou lógica que ditam como o erro deve ser tratado do que o que você tem disponível no contexto do seu código.

Por exemplo, a Listagem 9-6 mostra uma função que lê um nome de usuário de um arquivo. Se o arquivo não existe ou não pode ser lido, esta função retornará esses erros para o código que chamou esta função.

Nome do arquivo: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn ler_nome_de_usuario_do_arquivo() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}
}

Listagem 9-6: uma função que retorna erros para o código de chamada usando match

Essa função pode ser escrita de uma maneira muito mais curta, mas vamos começar fazendo muito isso manualmente para explorar o tratamento de erros; no final, mostraremos o caminho mais curto. Vamos olhar o tipo de retorno da função first: Result<String, io::Error>. Isso significa que a função está retornando um valor do tipo Result<T, E> onde o parâmetro genérico T foi preenchido com o tipo concreto String e o tipo genérico E foi preenchido com o tipo concreto io::Error. Se essa função for bem-sucedida sem problemas, o código que a chama receberá um valor Ok que contém uma String - o nome de usuário que essa função leu do arquivo. Se esta função encontrar algum problema, o código que a chama receberá um valor Err que contém uma instância de io::Error que contém mais informações sobre quais foram os problemas. Escolhemos io::Error como o tipo de retorno desta função porque esse é o tipo do valor de erro retornado de ambas as operações que estamos chamando no corpo desta função que podem falhar: a função File::open e o método read_to_string.

O corpo da função começa chamando a função File::open. Em seguida, lidamos com o valor Result retornado com um match semelhante ao match da Listagem 9-4, mas em vez de chamar panic! no caso Err, retornamos antecipadamente desta função e passamos o valor de erro de File::open de volta para o código de chamada como o valor de erro desta função. Se File::open for bem-sucedido, armazenamos o identificador de arquivo na variável f e continuamos.

Em seguida, criamos uma nova variável String em s e chamamos o método read_to_string no identificador de arquivo f para ler o conteúdo do arquivo s. O método read_to_string também retorna um Result porque pode falhar, embora File::open seja bem-sucedido. Portanto, precisamos de outro match para lidar com esse Result: se read_to_string for bem-sucedido, nossa função foi bem-sucedida e retornamos o nome de usuário do arquivo que agora está em s e embrulhado em um Ok. Se read_to_string falhar, retornamos o valor de erro da mesma forma que retornamos o valor de erro no match que tratou o valor de retorno de File::open. No entanto, não precisamos dizer explicitamente return, porque esta é a última expressão na função.

O código que chama esse código tratará a obtenção de um valor Ok que contém um nome de usuário ou um valor Err que contém um io::Error. Não sabemos o que o código de chamada fará com esses valores. Se o código de chamada obtiver um valor Err, ele pode chamar panic! e travar o programa, usar um nome de usuário padrão ou procurar o nome de usuário em algum lugar diferente de um arquivo, por exemplo. Não temos informações suficientes sobre o que o código de chamada está realmente tentando fazer, então propagamos todas as informações de sucesso ou erro para cima para que ele as manipule apropriadamente.

Esse padrão de propagação de erros é tão comum em Rust que Rust fornece o operador de ponto de interrogação ? para tornar isso mais fácil.

Um atalho para a propagação de erros: o operador ?

A Listagem 9-7 mostra uma implementação de ler_nome_de_usuario_do_arquivo que tem a mesma funcionalidade que tinha na Listagem 9-6, mas essa implementação usa o operador ?.

Nome do arquivo: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn ler_nome_de_usuario_do_arquivo() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
}

Listagem 9-7: uma função que retorna erros para o código de chamada usando o operador ?

O ? colocado após um valor Result é definido para funcionar quase da mesma maneira que as expressões match que definimos para lidar com os valores Result na Listagem 9-6. Se o valor de Result for um Ok, o valor dentro de Ok será retornado desta expressão e o programa continuará. Se o valor for um Err, o Err será retornado de toda a função como se tivéssemos usado a palavra-chave return, de forma que o valor do erro seja propagado para o código de chamada.

Há uma diferença entre o que a expressão match da Listagem 9-6 faz e o que o operador ? faz: os valores de erro que têm o operador ? chamado passam pela função from, definida na característica From na biblioteca padrão, que é usada para converter erros de um tipo em outro. Quando o operador ? chama a função from, o tipo de erro recebido é convertido no tipo de erro definido no tipo de retorno da função atual. Isso é útil quando uma função retorna um tipo de erro para representar todas as maneiras pelas quais uma função pode falhar, mesmo se as partes falharem por vários motivos diferentes. Contanto que cada tipo de erro implemente a função from para definir como se converter para o tipo de erro retornado, o operador ? cuida da conversão automaticamente.

No contexto da Listagem 9-7, ? no final da chamada File::open retornará o valor dentro de um Ok para a variável f. Se ocorrer um erro, o operador ? retornará antecipadamente de toda a função e fornecerá qualquer valor Err ao código de chamada. O mesmo se aplica ao ? no final da chamada read_to_string.

O operador ? elimina muitos clichês e torna a implementação dessa função mais simples. Poderíamos até encurtar esse código ainda mais encadeando chamadas de método imediatamente após o ?, conforme mostrado na Listagem 9-8.

Nome do arquivo: src/main.rs


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn ler_nome_de_usuario_do_arquivo() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}
}

Listagem 9-8: Chamando chamadas de método após o operador ?

Nós movemos a criação da nova String em s ao início da função; essa parte não mudou. Em vez de criar uma variável f, encadeamos a chamada a read_to_string diretamente no resultado de File::open("hello.txt")?. Ainda temos um ? no final da chamada read_to_string e ainda retornamos um valor Ok contendo o nome de usuário em s quando File::open e read_to_string for bem-sucedido, em vez de retornar erros. A funcionalidade é novamente a mesma que na Listagem 9-6 e na Listagem 9-7; esta é apenas uma maneira diferente e mais ergonômica de escrever.

Falando de maneiras diferentes de escrever essa função, a Listagem 9-9 mostra que há uma maneira de torná-la ainda mais curta.

Nome do arquivo: src/main.rs


#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn ler_nome_de_usuario_do_arquivo() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

Listagem 9-9: Usando fs::read_to_string em vez de abrir e depois ler o arquivo

Ler um arquivo em uma string é uma operação bastante comum, então Rust fornece a função fs::read_to_string conveniente que abre o arquivo, cria uma nova String, lê o conteúdo do arquivo, coloca o conteúdo String nele e o retorna. Obviamente, o uso de fs::read_to_string não nos dá a oportunidade de explicar todo o tratamento de erros, então o fizemos da maneira mais longa primeiro.

O operador ? pode ser usado em funções que retornam Result

O operador ? pode ser usado em funções que têm um tipo de retorno Result, porque é definido para funcionar da mesma maneira que a expressão match que definimos na Listagem 9-6. A parte de match que requer um tipo de retorno de Result é return Err(e), portanto, o tipo de retorno da função deve ser um Result para ser compatível com esse return.

Vejamos o que acontece se usarmos o operador ? na função main, que você deve lembrar tem um tipo de retorno de ():

Esse código não compila Esse código não compila.

use std::fs::File;

fn main() {
    let f = File::open("hello.txt")?;
}

Quando compilamos este código, obtemos a seguinte mensagem de erro:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `Try`)
 --> src/main.rs:4:13
  |
3 | / fn main() {
4 | |     let f = File::open("hello.txt")?;
  | |             ^^^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
5 | | }
  | |_- this function should return `Result` or `Option` to accept `?`
  |
  = help: the trait `Try` is not implemented for `()`
  = note: required by `from_error`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling`

To learn more, run the command again with --verbose.

Este erro indica que só podemos usar o operador ? em uma função que retorna Result ou Option ou outro tipo que implementa std::ops::Try. Quando você está escrevendo código em uma função que não retorna um desses tipos e deseja usar ? ao chamar outras funções que retornam Result<T, E>, você tem duas opções para corrigir esse problema. Uma técnica é alterar o tipo de retorno de sua função para Result<T, E> para que não haja restrições que impeçam isso. A outra técnica é usar um match ou um dos métodos Result<T, E> para lidar com o Result<T, E> da maneira apropriada.

A função main é especial e há restrições sobre qual deve ser seu tipo de retorno. Um tipo de retorno válido para principal é (), e convenientemente, outro tipo de retorno válido é Result<T, E>, conforme mostrado aqui:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

O tipo Box<dyn Error> é chamado de objeto de característica, sobre o qual falaremos na seção “Usando objetos de característica que permitem valores de tipos diferentes” no Capítulo 17. Por enquanto, você pode ler Box<dyn Error> para significar “qualquer tipo de erro”. O uso de ? em uma função main com este tipo de retorno é permitido.

Agora que discutimos os detalhes de chamar panic! ou retornar Result, vamos voltar ao tópico de como decidir o que é apropriado usar em quais casos.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

Erros irrecuperáveis com panic! em Rust

Às vezes, coisas ruins acontecem em seu código e não há nada que você possa fazer a respeito. Nesses casos, Rust tem a macro panic!. Quando a macro panic! for executada, seu programa imprimirá uma mensagem de falha, desenrolará e limpará a pilha e, em seguida, encerrará. Isso ocorre mais comumente quando um bug de algum tipo foi detectado e não está claro para o programador como lidar com o erro.

Desenrolando a pilha ou abortando em resposta a um pânico

Por padrão, quando ocorre um pânico, o programa começa a se desenrolar, o que significa que Rust volta a subir a pilha e limpa os dados de cada função que encontra. Mas essa caminhada de volta e limpeza dá muito trabalho. A alternativa é abortar imediatamente, o que termina o programa sem limpar. A memória que o programa estava usando precisará ser limpa pelo sistema operacional. Se em seu projeto você precisar tornar o binário resultante o menor possível, você pode mudar de desenrolamento para abortar em caso de pânico adicionando panic = 'abort' nas seções [profile] apropriadas em seu arquivo Cargo.toml. Por exemplo, se você deseja abortar em pânico no modo de liberação, adicione isto:

[profile.release]
panic = 'abort'

Vamos tentar chamar panic! num programa simples:

Nome do arquivo: src/main.rs

fn main() {
    panic!("crash and burn");
}

Ao executar o programa, você verá algo assim:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

A chamada para panic! causa a mensagem de erro contida nas duas últimas linhas. A primeira linha mostra nossa mensagem de pânico e o local em nosso código-fonte onde o pânico ocorreu: src/main.rs: 2: 5 indica que é a segunda linha, quinto caractere de nosso arquivo src/main.rs.

Neste caso, a linha indicada faz parte do nosso código, e se formos para essa linha, veremos a chamada da macro panic!. Em outros casos, a chamada panic! pode estar no código que nosso código chama, e o nome do arquivo e o número da linha relatados pela mensagem de erro serão o código de outra pessoa onde a macro panic! é chamada, não a linha do nosso código que eventualmente levou à chamada panic!. Podemos usar o backtrace das funções de onde panic! foi chamado para descobrir a parte do nosso código que está causando o problema. Discutiremos o que é um backtrace com mais detalhes a seguir.

Usando um Backtrace panic!

Vejamos outro exemplo para ver como é quando uma chamada panic! vem de uma biblioteca por causa de um bug em nosso código, em vez de nosso código chamando a macro diretamente. A Listagem 9-1 contém alguns códigos que tentam acessar um elemento por índice em um vetor.

Nome do arquivo: src/main.rs

Este código entra em pânico Este código entra em pânico!

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

Listagem 9-1: Tentativa de acessar um elemento além do final de um vetor, o que causará uma chamada para panic!

Aqui, estamos tentando acessar o 100º elemento de nosso vetor (que está no índice 99 porque a indexação começa em zero), mas ele tem apenas 3 elementos. Nesta situação, Rust entrará em pânico. O uso de [] deve retornar um elemento, mas se você passar um índice inválido, não há nenhum elemento que Rust pudesse retornar aqui que seria correto.

Em C, tentar ler além do final de uma estrutura de dados é um comportamento indefinido. Você pode obter o que quer que esteja no local da memória que corresponda a esse elemento na estrutura de dados, mesmo que a memória não pertença a essa estrutura. Isso é chamado de buffer overread e pode levar a vulnerabilidades de segurança se um invasor for capaz de manipular o índice de forma a ler dados que não deveriam ter permissão para serem armazenados após a estrutura de dados.

Para proteger seu programa desse tipo de vulnerabilidade, se você tentar ler um elemento em um índice que não existe, o Rust interromperá a execução e se recusará a continuar. Vamos experimentar e ver:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Isso aponta erro em um arquivo que não escrever, libcore/fatia/mod.rs. Essa é a implementação de slice no código-fonte do Rust. O código que é executado quando usamos [] em nosso vetor v está em libcore/slice/mod.rs, e é onde o panic! está realmente acontecendo.

A próxima linha de observação nos diz que podemos definir a variável RUST_BACKTRACE de ambiente para obter um backtrace de exatamente o que aconteceu para causar o erro. Um backtrace é uma lista de todas as funções que foram chamadas para chegar a este ponto. Os backtraces no Rust funcionam como em outras línguas: a chave para ler o backtrace é começar do início e ler até ver os arquivos que você escreveu. Esse é o local onde o problema se originou. As linhas acima das linhas que mencionam seus arquivos são códigos que seu código chamou; as linhas abaixo são códigos que chamaram seu código. Essas linhas podem incluir código Rust central, código de biblioteca padrão ou crates que você está usando. Vamos tentar obter um backtrace definindo a variável RUST_BACKTRACE de ambiente para qualquer valor, exceto 0. A Listagem 9-2 mostra uma saída semelhante ao que você verá.

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483
   1: core::panicking::panic_fmt
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85
   2: core::panicking::panic_bounds_check
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15
   5: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982
   6: panic::main
             at ./src/main.rs:4
   7: core::ops::function::FnOnce::call_once
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Listagem 9-2: O backtrace gerado por uma chamada para panic! exibido quando a variável de ambiente RUST_BACKTRACE é definida

É muita produção! A saída exata que você vê pode ser diferente dependendo do seu sistema operacional e da versão do Rust. Para obter backtraces com essas informações, os símbolos de depuração devem ser habilitados. Os símbolos de depuração são ativados por padrão ao usar cargo build ou cargo run sem o sinalizador --release, como temos aqui.

Na saída na Listagem 9-2, linha 6 dos backtrace aponta para a linha em nosso projeto que está causando o problema: linha 4 do arquivo src/main.rs. Se não quisermos que nosso programa entre em pânico, o local apontado pela primeira linha que menciona um arquivo que escrevemos é onde devemos começar a investigar. Na Listagem 9-1, onde escrevemos deliberadamente o código que entraria em pânico para demonstrar como usar backtraces, a maneira de consertar o pânico é não solicitar um elemento no índice 99 de um vetor que contém apenas 3 itens. Quando seu código entrar em pânico no futuro, você precisará descobrir que ação o código está realizando com quais valores causam o pânico e o que o código deve fazer em vez disso.

Voltaremos para panic! e quando devemos e não devemos usar panic! para lidar com as condições de erro na seção “Chamar panic! ou Não chamar panic!” posteriormente neste capítulo. A seguir, veremos como se recuperar de um erro usando Result.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

Manipulação de erros em Rust

O compromisso da Rust com a confiabilidade se estende ao tratamento de erros. Os erros são um fato da vida no software, portanto, o Rust tem vários recursos para lidar com situações em que algo dá errado. Em muitos casos, o Rust exige que você reconheça a possibilidade de um erro e execute alguma ação antes que seu código seja compilado. Esse requisito torna seu programa mais robusto, garantindo que você descobrirá erros e os tratará de maneira apropriada antes de implantar seu código para produção!

Rust agrupa os erros em duas categorias principais: erros recuperáveis e irrecuperáveis. Para um erro recuperável, como um erro de arquivo não encontrado, é razoável relatar o problema ao usuário e tentar a operação novamente. Erros irrecuperáveis são sempre sintomas de bugs, como tentar acessar um local além do final de um array.

A maioria das linguagens não faz distinção entre esses dois tipos de erros e os trata da mesma maneira, usando mecanismos como exceções. Rust não tem exceções. Em vez disso, ele tem o tipo Result<T, E> de erros recuperáveis e a macro panic! que interrompe a execução quando o programa encontra um erro irrecuperável. Este capítulo cobre a chamada panic! primeiro e depois fala sobre o retorno de valores Result<T, E>. Além disso, exploraremos as considerações ao decidir se devemos tentar se recuperar de um erro ou interromper a execução.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

terça-feira, 13 de abril de 2021

Armazenamento de chaves com valores associados em mapas de hash

A última de nossas coleções comuns é o mapa hash. O tipo HashMap<K, V> armazena um mapeamento de chaves de tipo K para valores de tipo V. Ele faz isso por meio de uma função hash, que determina como ele coloca essas chaves e valores na memória. Muitas linguagens de programação suportam esse tipo de estrutura de dados, mas geralmente usam um nome diferente, como hash, mapa, objeto, tabela hash, dicionário ou matriz associativa, apenas para citar alguns.

Os mapas hash são úteis quando você deseja pesquisar dados não usando um índice, como você pode fazer com vetores, mas usando uma chave que pode ser de qualquer tipo. Por exemplo, em um jogo, você pode acompanhar a pontuação de cada equipe em um mapa hash no qual cada chave é o nome de uma equipe e os valores são a pontuação de cada equipe. Dado o nome de uma equipe, você pode recuperar sua pontuação.

Examinaremos a API básica de mapas hash nesta seção, mas muitos outros recursos estão escondidos nas funções definidas em HashMap<K, V> pela biblioteca padrão. Como sempre, verifique a documentação da biblioteca padrão para obter mais informações.

Criando um Novo Mapa Hash

Você pode criar um mapa hash vazio com new e adicionar elementos com insert. Na Listagem 8-20, estamos acompanhando as pontuações de duas equipes cujos nomes são Azul e Amarelo. O time Azul começa com 10 pontos, e o time Amarelo começa com 50.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}

Listagem 8-20: Criando um novo mapa de hash e inserindo algumas chaves e valores

Observe que primeiro precisamos usar o HashMap da porção de coleções da biblioteca padrão. De nossas três coleções comuns, esta é a menos usada, portanto, não está incluída nos recursos trazidos ao escopo automaticamente no prelúdio. Os mapas hash também têm menos suporte da biblioteca padrão; não há macro embutida para construí-los, por exemplo.

Assim como os vetores, os mapas hash armazenam seus dados no heap. Esse HashMap tem chaves de tipo String e valores de tipo i32. Como os vetores, os mapas hash são homogêneos: todas as chaves devem ter o mesmo tipo e todos os valores devem ter o mesmo tipo.

Outra maneira de construir um mapa hash é usando iteradores e o método collect em um vetor de tuplas, onde cada tupla consiste em uma chave e seu valor. Entraremos em mais detalhes sobre os iteradores e seus métodos associados na seção “Processando uma série de itens com iteradores” do Capítulo 13. O método collect reúne dados em vários tipos de coleção, incluindo HashMap. Por exemplo, se tivéssemos os nomes dos times e pontuações iniciais em dois vetores separados, poderíamos usar o método zip para criar um vetor de tuplas onde “Azul” é pareado com 10 e assim por diante. Então, poderíamos usar o método collect para transformar esse vetor de tuplas em um mapa hash, conforme mostrado na Listagem 8-21.

fn main() {
    use std::collections::HashMap;

    let teams = vec![String::from("Blue"), String::from("Yellow")];
    let initial_scores = vec![10, 50];

    let mut scores: HashMap<_, _> =
        teams.into_iter().zip(initial_scores.into_iter()).collect();
}

Listagem 8-21: Criando um mapa hash de uma lista de equipes e uma lista de pontuações

A anotação de tipo HashMap<_, _> é necessária aqui porque é possível collect entrar em muitas estruturas de dados diferentes e Rust não sabe qual você deseja, a menos que você especifique. Para os parâmetros dos tipos de chave e valor, no entanto, usamos sublinhados e Rust pode inferir os tipos que o mapa de hash contém com base nos tipos de dados nos vetores. Na Listagem 8-21, o tipo de chave será String e o tipo de valor será i32, exatamente como os tipos eram na Listagem 8-20.

Mapas Hash e propriedade

Para tipos que implementam a característica Copy, como i32, os valores são copiados para o mapa hash. Para valores de propriedade como String, os valores serão movidos e o mapa de hash será o proprietário desses valores, conforme demonstrado na Listagem 8-22.

fn main() {
    use std::collections::HashMap;

    let field_name = String::from("Favorite color");
    let field_value = String::from("Blue");

    let mut map = HashMap::new();
    map.insert(field_name, field_value);
    // field_name e field_value são inválidos nesse ponto, tente usalos e
    // veja que erro você recebe
}

Listagem 8-22: Mostrando que as chaves e valores são de propriedade do mapa hash, uma vez que são inseridos

Não podemos usar as variáveis field_name e field_value depois que elas forem movidas para o mapa hash com a chamada para insert.

Se inserirmos referências a valores no mapa hash, os valores não serão movidos para o mapa hash. Os valores para os quais as referências apontam devem ser válidos pelo menos enquanto o mapa de hash for válido. Falaremos mais sobre esses problemas na seção “Validando referências com tempos de vida” no Capítulo 10.

Acessando Valores em um Mapa Hash

Podemos obter um valor do mapa de hash fornecendo sua chave para o método get, conforme mostrado na Listagem 8-23.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    let team_name = String::from("Blue");
    let score = scores.get(&team_name);
}

Listagem 8-23: Acessando a pontuação da equipe Blue armazenada no mapa hash

Aqui, score terá o valor que está associado ao time Blue, e o resultado será Some(&10). O resultado é agrupado em Some porque get retorna um Option<&V>; se não houver valor para essa chave no mapa hash, get retornará None. O programa precisará lidar com a Opção (Option) de uma das maneiras que abordamos no Capítulo 6.

Podemos iterar sobre cada par de chave / valor em um mapa hash de maneira semelhante à que fazemos com vetores, usando um loop for:

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
}

Este código imprimirá cada par em uma ordem arbitrária:

Yellow: 50
Blue: 10

Atualizando um Hash Map

Embora o número de chaves e valores possam ser aumentados, cada chave pode ter apenas um valor associado a ela por vez. Quando você deseja alterar os dados em um mapa hash, você deve decidir como lidar com o caso em que uma chave já possui um valor atribuído. Você pode substituir o valor antigo pelo novo valor, desconsiderando completamente o valor antigo. Você poderia manter o valor antigo e ignorar o novo valor, apenas adicionando o novo valor se a chave ainda não tiver um valor. Ou você pode combinar o valor antigo e o novo valor. Vejamos como fazer cada um deles!

Sobrescrever um valor

Se inserirmos uma chave e um valor em um mapa de hash e, em seguida, inserirmos a mesma chave com um valor diferente, o valor associado a essa chave será substituído. Mesmo que o código na Listagem 8-24 chame insert duas vezes, o mapa de hash conterá apenas um par de chave / valor porque estamos inserindo o valor para a chave da equipe Blue nas duas vezes.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Blue"), 25);

    println!("{:?}", scores);
}

Listagem 8-24: Substituindo um valor armazenado por uma chave específica

Este código imprimirá {"Blue": 25}. O valor original de 10 foi substituído.

Inserindo apenas um valor se a chave não tiver valor

É comum verificar se uma determinada chave tem um valor e, se não tiver, inserir um valor para ela. Os mapas hash têm uma API especial para isso chamada, entry que usa a chave que você deseja verificar como parâmetro. O valor de retorno do método entry é um enum chamado Entry que representa um valor que pode ou não existir. Digamos que queremos verificar se a chave da equipe amarela tem um valor associado a ela. Caso contrário, queremos inserir o valor 50, e o mesmo para a equipe Azul. Usando a API entry, o código se parece com a Listagem 8-25.

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();
    scores.insert(String::from("Blue"), 10);

    scores.entry(String::from("Yellow")).or_insert(50);
    scores.entry(String::from("Blue")).or_insert(50);

    println!("{:?}", scores);
}

Listagem 8-25: Usando o método entry para inserir apenas se a chave ainda não tiver um valor

O método or_insert em Entry é definido para retornar uma referência mutável ao valor da chave Entry correspondente se essa chave existir e, caso não exista, insere o parâmetro como o novo valor para esta chave e retorna uma referência mutável ao novo valor. Essa técnica é muito mais limpa do que escrever a lógica nós mesmos e, além disso, funciona mais bem com o verificador de empréstimo.

A execução do código na Listagem 8-25 será impressa {"Yellow": 50, "Blue": 10}. A primeira chamada para entry irá inserir a chave para o time Amarelo com o valor 50 porque o time Amarelo ainda não tem um valor. A segunda chamada para entry não mudará o hash map porque a equipe Blue já tem o valor 10.

Atualizando um valor com base no valor antigo

Outro caso de uso comum para mapas de hash é pesquisar o valor de uma chave e atualizá-lo com base no valor antigo. Por exemplo, a Listagem 8-26 mostra o código que conta quantas vezes cada palavra aparece em algum texto. Usamos um mapa hash com as palavras como chaves e incrementamos o valor para controlar quantas vezes vimos essa palavra. Se for a primeira vez que vimos uma palavra, primeiro inseriremos o valor 0.

fn main() {
    use std::collections::HashMap;

    let text = "hello world wonderful world";

    let mut map = HashMap::new();

    for word in text.split_whitespace() {
        let count = map.entry(word).or_insert(0);
        *count += 1;
    }

    println!("{:?}", map);
}

Listagem 8-26: Contando ocorrências de palavras usando um mapa hash que armazena palavras e contagens

Este código será impresso {"world": 2, "hello": 1, "wonderful": 1}. O método or_insert realmente retorna uma referência mutável ( &mut V) para o valor desta chave. Aqui, armazenamos essa referência mutável na variável count, portanto, para atribuir a esse valor, devemos primeiro desreferenciar count usando o asterisco (*). A referência mutável sai do escopo no final do loop for, portanto, todas essas alterações são seguras e permitidas pelas regras de empréstimo.

Funções de hash

Por padrão, HashMap usa uma função de hashing “criptograficamente forte” que pode oferecer resistência a ataques de negação de serviço (DoS). Este não é o algoritmo de hash mais rápido disponível, mas a compensação por melhor segurança que vem com a queda no desempenho vale a pena. Se você analisar seu código e descobrir que a função hash padrão é muito lenta para seus propósitos, você pode alternar para outra função especificando um hasher diferente. Um hasher é um tipo que implementa o traço BuildHasher. Falaremos sobre características e como implementá-las no Capítulo 10. Você não precisa necessariamente implementar seu próprio hasher do zero; crates.io tem bibliotecas compartilhadas por outros usuários do Rust que fornecem hashers que implementam muitos algoritmos de hash comuns.

Resumo

Vetores, strings e mapas hash fornecerão uma grande quantidade de funcionalidade necessária em programas quando você precisar armazenar, acessar e modificar dados. Aqui estão alguns exercícios que você deve estar preparado para resolver:

  • Dada uma lista de inteiros, use um vetor e retorne a média (o valor médio), a mediana (quando classificado, o valor na posição intermediária) e o modo (o valor que ocorre com mais frequência; um mapa hash será útil aqui) da lista.
  • Converta strings em latim pig. A primeira consoante de cada palavra é movida para o final da palavra e "ay" é adicionado, então "primeiro" se torna "primeiro-fay". Palavras que começam com uma vogal têm “feno” adicionado ao final (“maçã” torna-se “maçã-feno”). Lembre-se dos detalhes sobre a codificação UTF-8!
  • Usando um mapa hash e vetores, crie uma interface de texto para permitir que um usuário adicione nomes de funcionários a um departamento de uma empresa. Por exemplo, “Adicionar Sally à Engenharia” ou “Adicionar Amir às Vendas”. Em seguida, deixe o usuário recuperar uma lista de todas as pessoas em um departamento ou todas as pessoas na empresa por departamento, classificadas em ordem alfabética.

A documentação da API da biblioteca padrão descreve métodos que vetores, strings e mapas hash têm que serão úteis para esses exercícios!

Estamos entrando em programas mais complexos nos quais as operações podem falhar, portanto, é o momento perfeito para discutir o tratamento de erros. Faremos isso a seguir!

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

Armazenamento de texto codificado em UTF-8 com strings em Rust

Falamos sobre strings no Capítulo 4, mas vamos examiná-las com mais profundidade agora. Os novos Rustáceos geralmente ficam presos em strings por uma combinação de três motivos: propensão de Rust em expor possíveis erros, strings sendo uma estrutura de dados mais complicada do que muitos programadores acreditam e UTF-8. Esses fatores se combinam de uma maneira que pode parecer difícil quando você vem de outras linguagens de programação.

É útil discutir strings no contexto de coleções porque strings são implementadas como uma coleção de bytes, além de alguns métodos para fornecer funcionalidade útil quando esses bytes são interpretados como texto. Nesta seção, vamos falar sobre as operações em String que cada tipo de coleção tem, como criação, atualização e leitura. Também discutiremos as maneiras pelas quais String é diferente de outras coleções, ou seja, como a indexação em uma String é complicada pelas diferenças entre como as pessoas e os computadores interpretam os dados String.

O que é uma String?

Definiremos primeiro o que queremos dizer com o termo string. Rust tem apenas um tipo de string na linguagem central, que é a fatia de string str que geralmente é vista em sua forma emprestada &str. No Capítulo 4, falamos sobre fatias de string, que são referências a alguns dados de string codificados em UTF-8 armazenados em outro lugar. Literais de string, por exemplo, são armazenados no binário do programa e, portanto, são fatias de string.

O tipo String, que é fornecido pela biblioteca padrão do Rust em vez de codificado na linguagem principal, é um tipo de string codificado em UTF-8 que pode ser ampliado, mutável e possuído. Quando Rustáceos se referem a "strings" em Rust, eles geralmente se referem aos tipos de String e fatia de string &str e não apenas um desses tipos. Embora esta seção seja amplamente sobre String, ambos os tipos são usados intensamente na biblioteca padrão do Rust e as String e fatias de string são codificadas em UTF-8.

A biblioteca padrão do Rust também inclui uma série de outros tipos de Strings, tais como OsString, OsStr, CString, e CStr. Os crates de bibliotecas podem fornecer ainda mais opções para armazenar dados de string. Veja como todos esses nomes terminam em String ou Str? Eles se referem a variantes próprias e emprestadas, assim como os tipos String e str que você viu anteriormente. Esses tipos de string podem armazenar texto em codificações diferentes ou ser representados na memória de uma maneira diferente, por exemplo. Não discutiremos esses outros tipos de string neste capítulo; consulte a documentação da API para saber mais sobre como usá-los e quando cada um é apropriado.

Criação de uma nova string

Muitas das mesmas operações disponíveis com Vec<T> também estão disponíveis com String, começando com a função new para criar uma string, mostrada na Listagem 8-11.

fn main() {
    let mut s = String::new();
}

Listagem 8-11: Criando uma nova e vazia String

Essa linha cria uma nova string vazia chamada s, na qual podemos carregar os dados. Frequentemente, teremos alguns dados iniciais com os quais queremos iniciar a string. Para isso, usamos o método to_string, que está disponível em qualquer tipo que implemente o traço Display, como fazem os literais de string. A Listagem 8-12 mostra dois exemplos.

fn main() {
    let data = "conteúdo inicial";

    let s = data.to_string();

    // the method also works on a literal directly:
    let s = "conteúdo inicial".to_string();
}

Listagem 8-12: Usando o método to_string para criar uma String de uma string literal

Este código cria uma string contendo conteúdo inicial.

Também podemos usar a função String::from para criar uma String partir de uma string literal. O código da Listagem 8-13 é equivalente ao código da Listagem 8-12 que usa to_string.

fn main() {
    let s = String::from("conteúdo inicial");
}

Listagem 8-13: Usando a função String::from para criar uma String de uma string literal

Como as strings são usadas para tantas coisas, podemos usar muitas APIs genéricas diferentes para strings, fornecendo muitas opções. Algumas delas podem parecer redundantes, mas todos têm o seu lugar! Nesse caso, String::from e to_string fazem a mesma coisa, então o que você escolher é uma questão de estilo.

Lembre-se de que as strings são codificadas em UTF-8, portanto, podemos incluir quaisquer dados codificados corretamente nelas, conforme mostrado na Listagem 8-14.

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

Listagem 8-14: Armazenando saudações em diferentes idiomas em strings

Todos esses são valores String válidos.

Atualizando uma string

A String pode aumentar de tamanho e seu conteúdo pode mudar, assim como o conteúdo de Vec<T>, se você inserir mais dados nele. Além disso, você pode usar convenientemente o operador + ou a macro format! para concatenar valores String.

Anexando a uma string com push_str e push

Podemos aumentar uma String usando o método push_str para anexar uma fatia de string, conforme mostrado na Listagem 8-15.

fn main() {
    let mut s = String::from("foo");
    s.push_str("bar");
}

Listagem 8-15: Anexando uma fatia de string a uma String usando o método push_str

Após essas duas linhas, s conterá foobar. O método push_str usa uma fatia de string porque não queremos necessariamente nos apropriar do parâmetro. Por exemplo, o código na Listagem 8-16 mostra que seria lamentável se não pudéssemos usar s2 depois de anexar seu conteúdo a s1.

fn main() {
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s2 is {}", s2);
}

Listagem 8-16: Usando uma fatia de string após anexar seu conteúdo a uma String

Se o método push_str assumisse a propriedade de s2, não poderíamos imprimir seu valor na última linha. No entanto, este código funciona como esperávamos!

O método push pega um único caractere como parâmetro e o adiciona a String. A Listagem 8-17 mostra o código que adiciona a letra "l" a uma String usando o método push.

fn main() {
    let mut s = String::from("lo");
    s.push('l');
}

Listagem 8-17: Adicionando um caractere a um valor String usando push

Como resultado deste código, s conterá lol.

Concatenação com o operador + ou a macro format!

Frequentemente, você desejará combinar duas strings existentes. Uma maneira é usar o operador +, conforme mostrado na Listagem 8-18.

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2; // nota s1 foi movido aqui e não pode mais ser usado
}

Listagem 8-18: Usando o operador + para combinar dois valores String em um novo valor String

A string s3 conterá Hello, world! como resultado deste código. O motivo de s1 não ser mais válido após a adição e o motivo pelo qual usamos uma referência s2 tem a ver com a assinatura do método que é chamado quando usamos o operador +. O operador + usa o método add, cuja assinatura se parece com isto:

fn add(self, s: &str) -> String {

Esta não é a assinatura exata que está na biblioteca padrão: na biblioteca padrão, add é definida usando genéricos. Aqui, estamos olhando para a assinatura de add com tipos concretos substituídos pelos genéricos, que é o que acontece quando chamamos esse método com valores String. Discutiremos os genéricos no Capítulo 10. Essa assinatura nos dá as pistas de que precisamos para entender as partes complicadas do operador +.

Primeiro, s2 tem um &, o que significa que estamos adicionando uma referência da segunda string à primeira string por causa do parâmetro s na função add: só podemos adicionar &str a uma String; não podemos somar dois valores String. Mas espere - o tipo de &s2 é &String, não &str, conforme especificado no segundo parâmetro para add. Então, por que a Listagem 8-18 é compilada?

A razão pela qual podemos usar &s2 na chamada de add é que o compilador pode forçar o argumento &String a se tornar um &str. Quando chamamos o método add, Rust usa uma coação de referencial, que aqui transforma &s2 em &s2[..]. Discutiremos a coerção desreferencial com mais detalhes no Capítulo 15. Como add não assume a propriedade do parâmetro s, s2 ainda será um valor String válido após esta operação.

Em segundo lugar, podemos ver na assinatura que add toma posse de self, porque self não têm um &. Isso significa que s1 na Listagem 8-18 será movido para a chamada add e não será mais válido depois disso. Portanto, embora let s3 = s1 + &s2; pareça que irá copiar ambas as strings e criar uma nova, esta instrução realmente assume a propriedade de s1, anexa uma cópia do conteúdo de s2 e, em seguida, retorna a propriedade do resultado. Em outras palavras, parece que está fazendo muitas cópias, mas não está; a implementação é mais eficiente do que copiar.

Se precisarmos concatenar várias strings, o comportamento do operador + ficará complicado:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = s1 + "-" + &s2 + "-" + &s3;
}

Neste ponto, s será tic-tac-toe. Com todos os caracteres + e ", é difícil ver o que está acontecendo. Para combinações de strings mais complicadas, podemos usar a macro format!:

fn main() {
    let s1 = String::from("tic");
    let s2 = String::from("tac");
    let s3 = String::from("toe");

    let s = format!("{}-{}-{}", s1, s2, s3);
}

Este código também define s como tic-tac-toe. A macro format! funciona da mesma forma que println!, mas em vez de imprimir a saída na tela, ela retorna uma String com o conteúdo. A versão do código usando format! é muito mais fácil de ler e não se apropria de nenhum de seus parâmetros.

Indexando em Strings

Em muitas outras linguagens de programação, acessar caracteres individuais em uma string referenciando-os por índice é uma operação válida e comum. No entanto, se você tentar acessar partes de uma Stringsintaxe de indexação em Rust, obterá um erro. Considere o código inválido na Listagem 8-19.

Esse código não compila Esse código não compila.

fn main() {
    let s1 = String::from("hello");
    let h = s1[0];
}

Listagem 8-19: Tentativa de usar sintaxe de indexação com uma string

Este código resultará no seguinte erro:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `String` cannot be indexed by `{integer}`
 --> src/main.rs:3:13
  |
3 |     let h = s1[0];
  |             ^^^^^ `String` cannot be indexed by `{integer}`
  |
  = help: the trait `Index<{integer}>` is not implemented for `String`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections`

To learn more, run the command again with --verbose.

O erro e a nota contam a história: Strings Rust não suportam indexação. Mas porque não? Para responder a essa pergunta, precisamos discutir como Rust armazena strings na memória.

Representação Interna

Uma String é um invólucro sobre Vec<u8>. Vejamos algumas de nossas sequências de exemplo UTF-8 devidamente codificadas da Listagem 8-14. Primeiro, este:

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

Neste caso, len será 4, o que significa que o vetor que armazena a string “Hola” tem 4 bytes de comprimento. Cada uma dessas letras leva 1 byte quando codificada em UTF-8. Mas e quanto à seguinte linha? (Observe que esta string começa com a letra maiúscula cirílica Ze, não o número arábico 3.)

fn main() {
    let hello = String::from("السلام عليكم");
    let hello = String::from("Dobrý den");
    let hello = String::from("Hello");
    let hello = String::from("שָׁלוֹם");
    let hello = String::from("नमस्ते");
    let hello = String::from("こんにちは");
    let hello = String::from("안녕하세요");
    let hello = String::from("你好");
    let hello = String::from("Olá");
    let hello = String::from("Здравствуйте");
    let hello = String::from("Hola");
}

Quando perguntado sobre o comprimento da string, você dirá 12. No entanto, a resposta de Rust é 24: esse é o número de bytes necessários para codificar “Здравствуйте” em UTF-8, porque cada valor escalar Unicode nessa string ocupa 2 bytes de armazenamento. Portanto, um índice nos bytes da string nem sempre se correlacionará com um valor escalar Unicode válido. Para demonstrar, considere este código Rust inválido:

let hello = "Здравствуйте";
let answer = &hello[0];

Qual deve ser o valor de answer? Deveria ser З, a primeira carta? Quando codificado em UTF-8, o primeiro byte de З é 208 e o segundo é 151, então answer deveria ser 208, mas 208 não é um caractere válido por si só. O retorno 208 provavelmente não é o que um usuário gostaria se pedisse a primeira letra desta string; no entanto, esses são os únicos dados que Rust tem no índice de byte 0. Os usuários geralmente não querem que o valor do byte seja retornado, mesmo se a string contiver apenas letras latinas: se &"hello"[0] fosse um código válido que retornasse o valor do byte, ele retornaria 104, não h. Para evitar retornar um valor inesperado e causar bugs que podem não ser descobertos imediatamente, o Rust não compila esse código e evita mal-entendidos no início do processo de desenvolvimento.

Bytes e valores escalares e clusters de grafemas! Oh meu!

Outro ponto sobre o UTF-8 é que existem três maneiras relevantes de olhar para as strings da perspectiva de Rust: como bytes, valores escalares e clusters de grafemas (a coisa mais próxima do que chamaríamos de letras).

Se olharmos para a palavra hindi “नमस्ते” escrita na escrita Devanagari, ela é armazenada como um vetor de valores u8 semelhante a este:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

São 18 bytes e é como os computadores armazenam esses dados. Se olharmos para eles como valores escalares Unicode, que são o tipo char de Rust, esses bytes se parecem com isto:

['न', 'म', 'स', '्', 'त', 'े']

Existem seis valores char aqui, mas o quarto e o sexto não são letras: são diacríticos que não fazem sentido por si próprios. Finalmente, se olharmos para eles como grupos de grafemas, obteremos o que uma pessoa chamaria de quatro letras que compõem a palavra em hindi:

["न", "म", "स्", "ते"]

O Rust fornece diferentes maneiras de interpretar os dados de string brutos que os computadores armazenam para que cada programa possa escolher a interpretação de que precisa, independentemente da linguagem humana em que os dados estão.

Um último motivo pelo qual o Rust não nos permite indexar numa String para obter um caractere é que as operações de indexação devem sempre levar um tempo constante (O(1)). Mas não é possível garantir esse desempenho com uma String, porque Rust teria que percorrer o conteúdo desde o início até o índice para determinar quantos caracteres válidos havia.

Fatias de corte

Indexar em uma string geralmente é uma má ideia porque não está claro qual deve ser o tipo de retorno da operação de indexação de string: um valor de byte, um caractere, um cluster de grafema ou uma fatia de string. Portanto, Rust pede que você seja mais específico se você realmente precisa usar índices para criar fatias de string. Para ser mais específico em sua indexação e indicar que deseja uma fatia de string, em vez de indexar usando [] com um único número, você pode usar [] com um intervalo para criar uma fatia de string contendo bytes específicos:


#![allow(unused)]
fn main() {
let hello = "Здравствуйте";

let s = &hello[0..4];
}

Aqui, s será um &str que contém os primeiros 4 bytes da string. Anteriormente, mencionamos que cada um desses caracteres tinha 2 bytes, o que significa que s será Зд.

O que aconteceria se usássemos &hello[0..1]? A resposta: Rust entraria em pânico em tempo de execução da mesma forma que se um índice inválido fosse acessado em um vetor:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/collections`
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/main.rs:4:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Você deve usar intervalos para criar fatias de string com cuidado, porque isso pode travar seu programa.

Métodos para iteração de strings

Felizmente, você pode acessar elementos em uma string de outras maneiras.

Se você precisar executar operações em valores escalares Unicode individuais, a melhor maneira de fazer isso é usar o charsmétodo. Chamar chars“नमस्ते” separa e retorna seis valores de tipo char, e você pode iterar sobre o resultado para acessar cada elemento:


#![allow(unused)]
fn main() {
for c in "नमस्ते".chars() {
    println!("{}", c);
}
}

Este código imprimirá o seguinte:

न
म
स
्
त
े

O método bytes retorna cada byte bruto, que pode ser apropriado para seu domínio:


#![allow(unused)]
fn main() {
for b in "नमस्ते".bytes() {
    println!("{}", b);
}
}

Este código imprimirá os 18 bytes que compõem essa String:

224
164
// --recorte--
165
135

Mas lembre-se de que os valores escalares Unicode válidos podem ser compostos por mais de 1 byte.

Obter clusters de grafemas de strings é complexo, portanto, essa funcionalidade não é fornecida pela biblioteca padrão. Os crates estão disponíveis em crates.io se esta for a funcionalidade de que você precisa.

Strings não são tão simples

Para resumir, as strings são complicadas. Linguagens de programação diferentes fazem escolhas diferentes sobre como apresentar essa complexidade ao programador. O Rust optou por tornar o tratamento correto dos dados String o comportamento padrão para todos os programas Rust, o que significa que os programadores precisam pensar mais no manuseio de dados UTF-8 antecipadamente. Essa compensação expõe mais da complexidade das strings do que é aparente em outras linguagens de programação, mas evita que você tenha que lidar com erros envolvendo caracteres não ASCII posteriormente em seu ciclo de vida de desenvolvimento.

Vamos mudar para algo um pouco menos complexo: mapas hash!

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

segunda-feira, 12 de abril de 2021

Armazenamento de listas de valores com vetores em Rust

O primeiro tipo de coleção que veremos é Vec<T>, também conhecido como vetor. Os vetores permitem que você armazene mais de um valor em uma única estrutura de dados que coloca todos os valores próximos uns dos outros na memória. Os 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 dos itens em um carrinho de compras.

Criação de um novo vetor

Para criar um novo vetor vazio, podemos chamar a função Vec::new, conforme mostrado na Listagem 8-1.

fn main() {
    let v: Vec<i32> = Vec::new();
}

Listagem 8-1: Criando um novo vetor vazio para conter valores do tipo i32

Observe que adicionamos uma anotação de tipo aqui. Como não estamos inserindo nenhum valor neste vetor, Rust não sabe que tipo de elementos pretendemos armazenar. esse é um ponto importante. Os vetores são implementados usando genéricos; vamos cobrir como usar genéricos com seus próprios tipos no Capítulo 10. Por agora, saiba que o tipo Vec<T> fornecido pela biblioteca padrão pode conter qualquer tipo, e quando um vetor específico contém um tipo específico, o tipo é especificado entre colchetes angulares. Na Listagem 8-1, dissemos a Rust que o Vec<T> em v conterá elementos do tipo i32.

Em um código mais realista, Rust pode frequentemente inferir o tipo de valor que você deseja armazenar, uma vez que você insere valores, portanto, raramente você precisa fazer essa anotação de tipo. É mais comum criar um Vec<T> que tenha valores iniciais, e Rust fornece a macro vec! por conveniência. A macro criará um novo vetor que contém os valores que você fornecer. 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, conforme discutimos na seção “Tipos de dados” do Capítulo 3.

fn main() {
    let v = vec![1, 2, 3];
}

Listagem 8-2: Criando um novo vetor contendo valores

Como fornecemos valores i32 iniciais, Rust pode inferir que o tipo de v é Vec<i32> e a anotação de tipo não é necessária. A seguir, 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, conforme mostrado na Listagem 8-3.

fn main() {
    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 acontece com qualquer variável, se quisermos ser capazes de alterar seu valor, precisamos torná-la mutável usando a palavra-chave mut, conforme discutido no Capítulo 3. Os números que colocamos dentro são todos do tipo i32, e Rust infere isso a partir dos dados, portanto, não precisamos da anotação Vec<i32>.

A eliminação de um vetor elimina seus elementos

Como qualquer outra struct, um vetor é liberado quando sai do escopo, conforme anotado na Listagem 8-4.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // trabalhe com v
    } // <- v sai do escopo e é liberado aqui
}

Listagem 8-4: Mostrando onde o vetor e seus elementos são descartados

Quando o vetor é eliminado, todo o seu conteúdo também é eliminado, o que significa que os inteiros que ele contém serão apagados. Isso pode parecer um ponto direto, mas pode se tornar um pouco mais complicado quando você começa a introduzir referências aos elementos do vetor. Vamos tratar disso a seguir!

Lendo Elementos de Vetores

Agora que você sabe como criar, atualizar e destruir vetores, saber como ler seu conteúdo é um bom próximo passo. Existem duas maneiras de fazer referência a um valor armazenado em um vetor. Nos exemplos, anotamos os tipos de valores que são retornados dessas funções para maior clareza.

A Listagem 8-5 mostra os dois métodos de acesso a um valor em um vetor, seja com a sintaxe de indexação ou o método get.

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let third: &i32 = &v[2];
    println!("O terceiro elemento é {}", third);

    match v.get(2) {
        Some(third) => println!("O terceiro elemento é {}", third),
        None => println!("Não há um terceiro elemento."),
    }
}

Listagem 8-5: Usando a sintaxe de indexação ou o método get para acessar um item em um vetor

Observe dois detalhes aqui. Primeiro, usamos o valor de í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 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, que nos dá um Option<&T>.

Rust possui duas maneiras de referenciar um elemento, de modo que você pode escolher como o programa se comporta ao tentar usar um valor de índice para o qual o vetor não possui um elemento. Como exemplo, vamos ver o que um programa fará se tiver um vetor que contém cinco elementos e, em seguida, tenta acessar um elemento no índice 100, conforme mostrado na Listagem 8-6.

Este código entra em pânico Este código entra em pânico!

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}

Listagem 8-6: Tentativa de 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 faz referência a 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 acontecesse ocasionalmente em circunstâncias normais. Seu código terá lógica para lidar com Some(&element) ou None, conforme discutido no Capítulo 6. Por exemplo, o índice pode vir de uma pessoa que está inserindo um número. Se eles inserirem acidentalmente um número muito grande e o programa obtiver um valor None, você pode informar ao usuário quantos itens estão no vetor atual e dar a ele 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 impõe as regras de propriedade e empréstimo (abordadas 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-7, onde mantemos uma referência imutável ao primeiro elemento em um vetor e tentamos adicionar um elemento ao final, o que não funcionará se também tentarmos nos referir a esse elemento posteriormente na função:

Esse código não compila Esse código não compila.

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("O primeiro elemento é: {}", first);
}

Listagem 8-7: Tentativa de adicionar um elemento a um vetor enquanto mantém uma referência a um item

Compilar este código resultará neste erro:

$ cargo run
   Compiling collections v0.1.0 (file:///projects/collections)
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!("O primeiro elemento é: {}", first);
  |                                          ----- immutable borrow later used here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections`

To learn more, run the command again with --verbose.

O código na Listagem 8-7 pode parecer que deve funcionar: por que uma referência ao primeiro elemento deveria se preocupar com quais mudanças no final do vetor? Este erro é devido à forma como os vetores funcionam: adicionar um novo elemento no 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 a cada um outro onde o vetor está atualmente. Nesse caso, a referência ao primeiro elemento estaria apontando para a memória desalocada. As regras de empréstimo evitam que os programas terminem nessa situação.

Nota: Para obter mais detalhes sobre a implementação do tipo Vec<T>, consulte "The Rustonomicon".

Iterando sobre os valores em um vetor

Se quisermos acessar cada elemento em um vetor, por sua vez, podemos iterar por todos os elementos em vez de usar índices para acessar um de cada vez. A Listagem 8-8 mostra como usar um forloop para obter referências imutáveis para cada elemento em um vetor de i32valores e imprimi-los.

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{}", i);
    }
}

Listagem 8-8: Imprimindo cada elemento em um vetor iterando sobre os elementos usando um loop for

Também podemos iterar referências mutáveis para cada elemento em um vetor mutável para fazer alterações em todos os elementos. O loop for na Listagem 8-9 será adicionado 50a cada elemento.

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

Listagem 8-9: Iterando sobre referências mutáveis para elementos em um vetor

Para alterar o valor ao qual a referência mutável se refere, temos que usar o operador dereference (*) para obter o valor em i antes de podermos usar o operador +=. Falaremos mais sobre o operador de desreferenciação na seção “Seguindo o ponteiro para o valor com o operador de desreferenciação” do Capítulo 15.

Usando um Enum para armazenar vários tipos

No início deste capítulo, dissemos que os vetores só podem armazenar valores do mesmo tipo. Isso pode ser inconveniente; definitivamente há casos de uso para a necessidade de armazenar uma lista de itens de diferentes tipos. Felizmente, as variantes de um enum são definidas no mesmo tipo de enum, portanto, 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 na qual algumas das colunas da linha contenham inteiros, alguns números de ponto flutuante e algumas strings. Podemos definir um enum cujas variantes conterão os diferentes tipos de valor e, em seguida, todas as variantes do enum serão consideradas do mesmo tipo: o do enum. Então, podemos criar um vetor que contém esse enum e, portanto, em última análise, contém diferentes tipos. Demonstramos isso na Listagem 8-10.

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),
    ];
}

Listagem 8-10: Definindo um enum para armazenar valores de diferentes tipos em um vetor

O Rust precisa saber quais tipos estarão no vetor no momento da compilação para saber exatamente quanta memória no heap será necessária para armazenar cada elemento. Uma vantagem secundária é que podemos ser explícitos sobre quais tipos são permitidos neste vetor. Se Rust permitisse que um vetor contivesse qualquer tipo, haveria uma chance de que um ou mais dos tipos causassem erros nas operações realizadas nos elementos do vetor. Usar um enum mais uma expressão match significa que o Rust garantirá no momento da compilação que todos os casos possíveis sejam tratados, conforme discutido no Capítulo 6.

Quando você está escrevendo um programa, se você não conhece o conjunto exaustivo de tipos que o 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 de característica, 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 Vec<T> pela biblioteca padrão. Por exemplo, além de push, um método pop remove e retorna o último elemento. Vamos passar para o próximo tipo de coleção: String!

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença

sábado, 10 de abril de 2021

Coleções Comuns em Rust

A biblioteca padrão do Rust inclui várias estruturas de dados muito úteis chamadas coleções. A maioria dos outros tipos de dados representam um valor específico, mas as coleções podem conter vários valores. Ao contrário do array integrado e dos tipos de tupla, os dados para os quais essas coleções apontam são armazenados no heap, o que significa que a quantidade de dados não precisa ser conhecida no momento da compilação e pode aumentar ou diminuir à medida que o programa é executado. Cada tipo de coleção tem recursos e custos diferentes, e escolher um apropriado para sua situação atual é uma habilidade que você desenvolverá com o tempo. Neste capítulo, discutiremos três coleções que são usadas com frequência em programas Rust:

  • Um vetor permite armazenar um número variável de valores próximos um do outro.
  • Uma string é uma coleção de caracteres. Mencionamos o tipo String anteriormente, mas neste capítulo falaremos sobre ele em profundidade.
  • Um mapa de hash permite que você associe um valor a uma chave específica. É uma implementação particular da estrutura de dados mais geral chamada de mapa.

Para aprender sobre os outros tipos de coleções fornecidas pela biblioteca padrão, consulte a documentação.

Discutiremos como criar e atualizar vetores, strings e mapas hash, bem como o que torna cada um especial.

Traduzido por Acervo Lima. O original pode ser acessado aqui.

Licença