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

0 comentários:

Postar um comentário