Erros recuperáveis com Result
A maior parte dos erros não são sérios o suficiente para precisar que o programa pare totalmente. Às vezes, quando uma função falha, é por uma razão que nós podemos facilmente interpretar e responder. Por exemplo, se tentamos abrir um arquivo e essa operação falhar porque o arquivo não existe, nós podemos querer criar o arquivo em vez de terminar o processo.
Lembre-se do Capítulo 2, na seção “Tratando Potenciais Falhas com o Tipo Result
” que o enum `Result` é definido como tendo duas variantes,Ok e Err, como mostrado a seguir:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
O T e E são parâmetros de tipos genéricos: nós os discutiremos em mais
detalhe no Capítulo 10. O que você precisa saber agora é que T representa
o tipo do valor que vai ser retornado dentro da variante Ok em caso de sucesso,
e E representa o tipo de erro que será retornado dentro da variante Err
em caso de falha. Por Result ter esses parâmetros de tipo genéricos, nós
podemos usar o tipo Result e as funções que a biblioteca padrão definiu sobre
ele em diversas situações em que o valor de sucesso e o valor de erro que
queremos retornar possam divergir.
Vamos chamar uma função que retorna um valor Result porque a função poderia
falhar: na Listagem 9-3 tentamos abrir um arquivo:
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? Poderíamos olhar na documentação
da API da biblioteca padrão, ou poderíamos perguntar para o compilador! Se damos à f
uma anotação de tipo que sabemos não ser o tipo retornado pela função e tentamos
compilar o código, o compilador nos dirá que os tipos não casam. A mensagem de erro
vai então nos dizer qual é, de fato, o tipo de f. Vamos tentar isso: nós sabemos que
o tipo retornado por File::open não é u32, então vamos mudar a declaração
let f para isso:
let f: u32 = File::open("hello.txt");
Tentar compilar agora nos dá a seguinte saída:
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
= note: found type `std::result::Result<std::fs::File, std::io::Error>`
Isso nos diz que o valor de retorno de File::open é um Result<T, E>.
O parâmetro genérico T foi preenchido aqui com o tipo do valor de sucesso,
std::fs::File, que é um handle de arquivo. O tipo de E usado no valor
de erro é std::io::Error.
Esse tipo de retorno significa que a chamada a File::open pode dar certo
e retornar para nós um handle de arquivo que podemos usar pra ler ou escrever
nele. Essa chamada de função pode também falhar: por exemplo, o arquivo pode não
existir ou talvez não tenhamos permissão para acessar o arquivo. A função File::open
precisa ter uma maneira de nos dizer se ela teve sucesso ou falhou e ao mesmo tempo
nos dar ou o handle de arquivo ou informação sobre o erro. Essa informação é
exatamente o que o enum Result comunica.
No caso em que File::open tem sucesso, o valor na variável f será uma instância
de Ok que contém um handle de arquivo. No caso em que ela falha, o valor em f
será uma instância de Err que contém mais informação sobre o tipo de erro que
aconteceu.
Devemos fazer com que o código na Listagem 9-3 faça diferentes ações dependendo
do valor retornado por File::open. A Listagem 9-4 mostra uma maneira de lidar
com o Result usando uma ferramenta básica: a expressão match que discutimos
no Capítulo 6.
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!("Houve um problema ao abrir o arquivo: {:?}", error) }, }; }
Listagem 9-4: Usando uma expressão match para tratar as
variantes de Result que podemos encontrar.
Note que, como no enum Option, o enum Result e suas variantes foram importadas
no prelúdio, então não precisamos especificar Result:: antes das variantes Ok
e Err nas linhas de match.
Aqui dizemos ao Rust que quando o resultado é Ok ele deve retornar o valor
interno file de dentro da variante Ok e nós então podemos atribuir este
valor de handle de arquivo à variável f. Depois do match, nós podemos então
usar o handle de arquivo para ler ou escrever.
A outra linha de match trata do caso em que recebemos um valor de Err de
File::open. Nesse exemplo, nós escolhemos chamar a macro panic!. Se não
há nenhum arquivo chamado hello.txt no diretório atual e rodarmos esse código,
veremos a seguinte saída da macro panic!:
thread 'main' panicked at 'Houve um problema ao abrir o arquivo: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
Como sempre, essa saída nos diz exatamente o que aconteceu de errado.
Usando match com Diferentes Erros
O código na Listagem 9-4 chamará panic! não importa a razão pra File::open
ter falhado. O que queremos fazer em vez disso é tomar diferentes ações para diferentes
motivos de falha: se File::open falhou porque o arquivo não existe, nós
queremos criar um arquivo e retornar o handle para ele. Se File::open
falhou por qualquer outra razão, por exemplo porque não temos a permissão para
abrir o arquivo, nós ainda queremos chamar panic! da mesma maneira que fizemos
na Listagem 9-4. Veja a Listagem 9-5, que adiciona outra linha ao match:
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(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
"Tentou criar um arquivo e houve um problema: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"Houve um problema ao abrir o arquivo: {:?}",
error
)
},
};
}
Listagem 9-5: Tratando diferentes tipos de erros de diversas maneiras.
O tipo do valor que File::open retorna dentro da variante Err é io::Error,
que é uma struct fornecida pela biblioteca padrão. Essa struct tem o método
kind que podemos chamar para receber um valor de io::ErrorKind. io::ErrorKind
é um enum fornecido pela biblioteca padrão que tem variantes representanto diversos
tipos de erros que podem ocorrer em uma operação de io. A variante que queremos
usar é ErrorKind::NotFound, que indica que o arquivo que queremos abrir não existe
ainda.
A condição if error.kind() == ErrorKind::NotFound é chamada de um match guard:
é uma condição extra dentro de uma linha de match que posteriormente refina
o padrão da linha. Essa condição deve ser verdadeira para o código da linha ser
executado; caso contrário a análise de padrões vai continuar considerando as
próximas linhas no match. O ref no padrão é necessário para que o error
não seja movido para a condição do guard, mas meramente referenciado por ele.
A razão de ref ser utilizado em vez de & para pegar uma referência vai ser
discutida em detalhe no Capítulo 18. Resumindo, no contexto de um padrão, &
corresponde a uma referência e nos dá seu valor, enquanto ref corresponde a um valor
e nos dá uma referência a ele.
A condição que queremos checar no match guard é se o valor retornado pelo
error.kind() é a variante NotFound do enum ErrorKind. Se é, queremos
tentar criar um arquivo com File::create. No entanto, como File::create
pode também falhar, precisamos adicionar um match interno também. Quando
o arquivo não pode ser aberto, outro tipo de mensagem de erro será mostrada.
A última linha do match externo continua a mesma de forma que o programa
entre em pânico pra qualquer erro além do de arquivo ausente.
Atalhos para Pânico em Erro: unwrap e expect
Usar match funciona bem o suficiente, mas pode ser um pouco verboso e nem
sempre comunica tão bem a intenção. O tipo Result<T, E> tem vários métodos
auxiliares definidos para fazer diversas tarefas. Um desses métodos, chamado
unwrap, é um método de atalho que é implementado justamente como o match que
escrevemos na Listagem 9-4. Se o valor de Result for da variante Ok, unwrap
vai retornar o valor dentro de Ok. Se o Result for da variante Err, unwrap
vai chamar a macro panic!. Aqui um exemplo de unwrap em ação:
Arquivo: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt").unwrap(); }
Se rodarmos esse 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" } }',
/stable-dist-rustc/build/src/libcore/result.rs:868
Outro método, expect, que é semelhante a unwrap, nos deixa também escolher
a mensagem de erro do panic!. Usar expect em vez de unwrap e fornecer
boas mensagens de erros podem transmitir sua intenção e tornar a procura pela
fonte de pânico mais fácil. A sintaxe de expect é a seguinte:
Arquivo: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt").expect("Falhou ao abrir hello.txt"); }
Nós usamos expect da mesma maneira que unwrap: para retornar o handle de arquivo
ou chamar a macro de panic!. A mensagem de erro usada por expect na sua chamada
de panic! será o parâmtero que passamos para expect em vez da mensagem padrão
que o unwrap usa. Aqui está como ela aparece:
thread 'main' panicked at 'Falhou ao abrir hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }',
/stable-dist-rustc/build/src/libcore/result.rs:868
Como essa mensagem de erro começa com o texto que especificamos, Falhou ao abrir hello.txt, será mais fácil encontrar o trecho do código de onde vem essa mensagem de erro. Se usamos unwrap em diversos lugares, pode tomar mais tempo encontrar
exatamente qual dos unwrap está causando o pânico, dado que todas as chamadas
a unwrap chamam o print de pânico com a mesma mensagem.
Propagando Erros
Quando você está escrevendo 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 que a chamou de forma que ele possa decidir o que fazer. Isso é conhecido como propagar o erro e dá mais controle ao código que chamou sua função, onde talvez haja mais informação sobre como tratar o erro do 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, essa função vai retornar esses erros ao código que chamou essa função:
Arquivo: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> 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 ao código que a chamou
usando match
Vamos olhar primeiro ao tipo retornado pela função: 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 pelo tipo concreto String e o tipo genérico
E foi preenchido pelo tipo concreto io::Error. Se essa função tem sucesso sem
nenhum problema, o código que chama essa função vai receber um valor Ok que contém
uma String- o nome de usuário que essa função leu do arquivo. Se essa função
encontra qualquer problema, o código que a chama receberá um valor de Err
que contém uma instância de io::Error, que contém mais informação
sobre o que foi o problema. Escolhemos io::Error como o tipo de retorno
dessa função porque é este o tipo de erro retornado pelas
duas operações que estamos chamando no corpo dessa 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. Nós então tratamos
o valor de Result retornado usando um match semelhante ao da Listagem 9-4,
só que em vez de chamar panic! no caso de Err, retornamos mais cedo dessa função
e passamos o valor de erro de File::open de volta ao código que a chamou, como o
valor de erro da nossa função. Se File::open tem sucesso, nós guardamos o handle de
arquivo na variável f e continuamos.
Então, criamos uma nova String na variável s e chamamos o método read_to_string
no handle de arquivo f para ler o conteúdo do arquivo e armazená-lo em s. O método
read_to_string também retorna um Result porque ele pode falhar, mesmo que
File::open teve sucesso. Então precisamos de outro match para tratar esse
Result: se read_to_string teve sucesso, então nossa função teve sucesso, e nós
retornamos o nome de usuário lido do arquivo que está agora em s, encapsulado em um Ok.
Se read_to_string falhou, retornamos o valor de erro da mesma maneira que retornamos
o valor de erro no match que tratou o valor de retorno de File::open.
No entanto, não precisamos explicitamente escrever return, porque essa já é a
última expressão na função.
O código que chama nossa função vai então receber ou um valor Ok que
contém um nome de usuário ou um valor de Err que contém um io::Error. Nós
não sabemos o que o código que chamou nossa função fará com esses valores. Se o
código que chamou recebe um valor de Err, ele poderia chamar panic! e causar
um crash, usar um nome de usuário padrão, ou procurar o nome de usuário em outro
lugar que não um arquivo, por exemplo. Nós não temos informação o suficiente sobre
o que o código que chamou está de fato tentando fazer, então propagamos toda a
informação de sucesso ou erro para cima para que ele a trate apropriadamente.
Esse padrão de propagação de erros é tão comum em Rust que a linguagem disponibiliza
o operador de interrogação ? para tornar isso mais fácil.
Um Atalho Para Propagar Erros: ?
A Listagem 9-7 mostra uma implementação de read_username_from_file que tem a
mesma funcionalidade que tinha na Listagem 9-6, mas esta implementação usa o operador
de interrogação:
Arquivo: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> 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
que a chamou usando ?.
O ? colocado após um valor de Result é definido para funcionar quase
da mesma maneira que as expressões match que definimos para tratar o valor
de Result na Listagem 9-6. Se o valor de Result é um Ok, o valor dentro dele
vai ser retornado dessa expressão e o programa vai continuar. Se o valor
é um Err, o valor dentro dele vai ser retornado da função inteira como se
tivéssemos usado a palavra-chave return de modo que o valor de erro é propagado
ao código que chamou a função.
A única diferença entre a expressão match da Listagem 9-6 e o que o operador
de interrogação faz é que quando usamos o operador de interrogação, os valores
de erro passam pela função from definida no trait From na biblioteca
padrão. Vários tipos de erro implementam a função from para converter um
erro de um tipo em outro. Quando usado pelo operador de
interrogação, a chamada à função from converte o tipo de erro que o
operador recebe no tipo de erro definido no tipo de retorno da função em
que estamos usando ?. Isso é útil quando partes de uma função podem falhar
por várias razões diferentes, mas a função retorna um tipo de erro que
representa todas as maneiras que a função pode falhar. Enquanto cada
tipo de erro implementar a função from para definir como se converter
ao tipo de erro retornado, o operador de interrogação lida com a conversão
automaticamente.
No contexto da Listagem 9-7, o ? no final da chamada de File::open vai
retornar o valor dentro do Ok à variável f. Se um erro ocorrer, ?
vai retornar mais cedo a função inteira e dar um valor de Err ao código
que a chamou. O mesmo se aplica ao ? ao final da chamada de read_to_string.
O ? elimina um monte de excesso e torna a implementação dessa
função mais simples. Poderíamos até encurtar ainda mais esse código
ao encadear chamadas de método imediatamente depois do ?, como mostrado
na Listagem 9-8:
Arquivo: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } }
Listagem 9-8: Encadeando chamadas de método após o operador de interrogação.
Nós movemos a criação da nova String em s para o começo da função;
essa parte não mudou. Em vez de criar uma variável f, nós encadeamos
a chamada para read_to_string diretamente ao resultado de
File::open("hello.txt")?. Nós ainda temos um ? ao fim da chamada a
read_to_string, e ainda retornamos um valor de Ok contendo o nome de usuário
em s quando ambos os métodos File::open e read_to_string tiveram sucesso ao invés
de retornarem erros. Essa funcionalidade é novamente a mesma da Listagem 9-6 e
Listagem 9-7; essa é só uma maneira diferente e mais ergonômica de escrevê-la.
? Somente Pode Ser Usado em Funções Que Retornam Result
O ? só pode ser usado em funções que tem um tipo de retorno de Result,
porque está definido a funcionar da mesma maneira que a expressão match que
definimos na Listagem 9-6. A parte do match que requer um tipo de retorno de
Result é return Err(e), então o tipo de retorno da função deve ser
um Result para ser compatível com esse return.
Vamos ver o que ocorre quando usamos ? na função main, que como vimos, tem
um tipo de retorno de ():
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
Quando compilamos esse código recebemos a seguinte mensagem de erro:
error[E0277]: the `?` operator can only be used in a function that returns
`Result` (or another type that implements `std::ops::Try`)
--> src/main.rs:4:13
|
4 | let f = File::open("hello.txt")?;
| ------------------------
| |
| cannot use the `?` operator in a function that returns `()`
| in this macro invocation
|
= help: the trait `std::ops::Try` is not implemented for `()`
= note: required by `std::ops::Try::from_error`
Esse erro aponta que só podemos usar o operador de interrogação em funções
que retornam Result. Em funções que não retornam Result, quando você chama
outras funções que retornam Result, você deve usar um match ou um dos métodos
de Result para tratá-lo em vez de usar ? para potencialmente
propagar o erro ao código que a chamou.
Agora que discutimos os detalhes de chamar panic! ou retornar Result, vamos
retornar ao tópico de como decidir qual é apropriado para utilizar em quais
casos.