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:
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");
}
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.
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::open
retornado. A Listagem 9-4 mostra uma maneira de lidar com o Result
uso de uma ferramenta básica, a match
expressã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),
};
}
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)
}
},
};
}
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
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
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
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
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.
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.
0 comentários:
Postar um comentário