Então, como você decide quando deve ligar panic!
e quando deve retornar Result
? Quando o código entra em pânico, não há como se recuperar. Você pode chamar panic!
para qualquer situação de erro, se há uma maneira possível de recuperar ou não, mas então você está tomando a decisão em nome do código que chama seu código que a situação é irrecuperável. Quando você opta por retornar um valor Result
, fornece as opções de código de chamada em vez de tomar a decisão por isso. O código de chamada pode escolher tentar se recuperar de uma maneira apropriada para sua situação ou pode decidir que um valor Err
, nesse caso, é irrecuperável, para que possa chamar panic!
e transformar seu erro recuperável em irrecuperável. Portanto, retornar Result
é uma boa escolha padrão ao definir uma função que pode falhar.
Em raras situações, é mais apropriado escrever um código que entre em pânico em vez de retornar um Result
. Vamos explorar por que é apropriado entrar em pânico em exemplos, código de protótipo e testes. Em seguida, discutiremos situações em que o compilador não pode dizer que a falha é impossível, mas você, como um humano, pode. O capítulo será concluído com algumas diretrizes gerais sobre como decidir se deve entrar em pânico com o código da biblioteca.
Exemplos, código de protótipo e testes
Quando você está escrevendo um exemplo para ilustrar algum conceito, ter um código robusto de tratamento de erros no exemplo também pode tornar o exemplo menos claro. Em exemplos, entende-se que uma chamada a um método como unwrap
que pode causar pânico é um espaço reservado para a maneira como você deseja que seu aplicativo trate os erros, que podem ser diferentes com base no que o resto do seu código está fazendo.
Da mesma forma, os métodos unwrap
e expect
são muito úteis durante a prototipagem, antes que você esteja pronto para decidir como lidar com os erros. Eles deixam marcadores claros em seu código para quando você estiver pronto para tornar seu programa mais robusto.
Se uma chamada de método falhar em um teste, você desejará que todo o teste falhe, mesmo que esse método não seja a funcionalidade em teste. Pois panic!
é assim que um teste é marcado como falha, ligando unwrap
ou expect
é exatamente o que deve acontecer.
Casos em que você tem mais informações do que o compilador
Também seria apropriado chamar unwrap
quando você tiver alguma outra lógica que garanta que Result
terá um valor Ok
, mas a lógica não é algo que o compilador entende. Você ainda terá um valor Result
que precisa controlar: qualquer operação que você estiver chamando ainda tem a possibilidade de falhar em geral, embora seja logicamente impossível em sua situação particular. Se você puder garantir, inspecionando manualmente o código, que nunca terá uma variante Err
, é perfeitamente aceitável chamar unwrap
. Aqui está um exemplo:
Estamos criando uma instância IpAddr
analisando uma string codificada. Podemos ver que 127.0.0.1
é um endereço IP válido, portanto, é aceitável usar unwrap
aqui. No entanto, ter uma string válida e codificada não altera o tipo de retorno do método parse
: ainda obtemos um valor Result
, e o compilador ainda nos fará lidar com Result
como se a variante Err
fosse uma possibilidade porque o compilador não é inteligente o suficiente para ver que esta string é sempre um endereço IP válido. Se a string do endereço IP veio de um usuário em vez de ser codificada no programa e, portanto, tinha a possibilidade de falha, definitivamente queremos lidar com Result
de uma forma mais robusta.
Diretrizes para tratamento de erros
É aconselhável fazer com que seu código entre em pânico quando for possível que ele acabe em um estado ruim. Nesse contexto, um estado ruim é quando alguma suposição, garantia, contrato ou invariante foi quebrado, como quando valores inválidos, valores contraditórios ou valores ausentes são passados para seu código - mais um ou mais dos seguintes:
- O mau estado não é algo que se espera que aconteça ocasionalmente.
- Seu código após este ponto precisa confiar em não estar nesse estado ruim.
- Não há uma boa maneira de codificar essas informações nos tipos que você usa.
Se alguém chamar seu código e passar valores que não façam sentido, a melhor escolha pode ser chamar panic!
e alertar a pessoa que usa sua biblioteca sobre o bug em seu código para que ela possa corrigi-lo durante o desenvolvimento. Da mesma forma, panic!
geralmente é apropriado se você estiver chamando um código externo que está fora de seu controle e ele retorna um estado inválido que você não tem como corrigir.
No entanto, quando a falha é esperada, é mais apropriado retornar um Result
do que fazer uma chamada panic!
. Os exemplos incluem um analisador recebendo dados malformados ou uma solicitação HTTP que retorna um status que indica que você atingiu um limite de taxa. Nesses casos, retornar um Result
indica que a falha é uma possibilidade esperada que o código de chamada deve decidir como lidar.
Quando seu código executa operações em valores, seu código deve verificar se os valores são válidos primeiro e entrar em pânico se os valores não forem válidos. Isso ocorre principalmente por razões de segurança: tentar operar com dados inválidos pode expor seu código a vulnerabilidades. Este é o principal motivo pelo qual a biblioteca padrão chamará panic!
se você tentar um acesso à memória fora dos limites: tentar acessar a memória que não pertence à estrutura de dados atual é um problema de segurança comum. As funções costumam ter contratos: seu comportamento só é garantido se as entradas atenderem a requisitos específicos. Pânico quando o contrato é violado faz sentido porque uma violação do contrato sempre indica um bug do lado do chamador e não é um tipo de erro que você deseja que o código de chamada tenha que lidar explicitamente. Na verdade, não há uma maneira razoável de recuperar o código de chamada; os programadores de chamada precisam corrigir o código. Os contratos para uma função, especialmente quando uma violação causar pânico, devem ser explicados na documentação da API para a função.
No entanto, ter muitas verificações de erro em todas as suas funções seria prolixo e irritante. Felizmente, você pode usar o sistema de tipo do Rust (e, portanto, a verificação de tipo que o compilador faz) para fazer muitas das verificações para você. Se sua função tiver um tipo específico como parâmetro, você pode prosseguir com a lógica de seu código sabendo que o compilador já garantiu que você tenha um valor válido. Por exemplo, se você tiver um tipo em vez de um Option
, seu programa espera ter algo em vez de nada. Seu código, então, não precisa lidar com dois casos para as variantes Some
e None
: terá apenas um caso para ter definitivamente um valor. O código que tenta passar nada para a sua função nem mesmo é compilado, portanto, sua função não precisa verificar esse caso em tempo de execução. Outro exemplo é o uso de um tipo inteiro sem sinal, como u32
, que garante que o parâmetro nunca seja negativo.
Criação de tipos personalizados para validação
Vamos pegar a ideia de usar o sistema de tipos do Rust para garantir que temos um valor válido um passo adiante e ver como criar um tipo customizado para validação. Lembre-se do jogo de adivinhação no Capítulo 2, no qual nosso código pedia ao usuário que adivinhasse um número entre 1 e 100. Nunca validamos se a estimativa do usuário estava entre esses números antes de compará-la com nosso número secreto; apenas validamos se a suposição era positiva. Nesse caso, as consequências não foram muito terríveis: nosso resultado de “Muito alto” ou “Muito baixo” ainda estaria correto. Mas seria um aprimoramento útil guiar o usuário em direção a suposições válidas e ter um comportamento diferente quando um usuário adivinha um número que está fora do intervalo e quando um usuário digita, por exemplo, letras em vez disso.
Uma maneira de fazer isso seria analisar a estimativa como um i32
em vez de apenas u32
para permitir números potencialmente negativos e, em seguida, adicionar uma verificação para o número estar no intervalo, assim:
A declaração if
verifica se nosso valor está fora do intervalo, informa ao usuário sobre o problema e chama continue
para iniciar a próxima iteração do loop e solicitar outra estimativa. Após a declaração if
, podemos prosseguir com as comparações entre guess
e o número secreto sabendo que guess
está entre 1 e 100.
Porém, esta não é uma solução ideal: se fosse absolutamente crítico que o programa operasse apenas em valores entre 1 e 100, e tivesse muitas funções com este requisito, ter uma verificação como esta em todas as funções seria tedioso (e poderia impactar na atuação).
Em vez disso, podemos fazer um novo tipo e colocar as validações em uma função para criar uma instância do tipo em vez de repetir as validações em todos os lugares. Dessa forma, é seguro para as funções usarem o novo tipo em suas assinaturas e usarem com segurança os valores que recebem. A Listagem 9-10 mostra uma maneira de definir um tipo Guess
que criará apenas uma instância de Guess
se a função new
receber um valor entre 1 e 100.
Primeiro, definimos uma estrutura chamada Guess
que possui um campo chamado value
que contém um i32
. É aqui que o número será armazenado.
Em seguida, implementamos uma função associada chamada new
em Guess
que cria instâncias de valores Guess
. A função new
é definida para ter um parâmetro denominado value
de tipo i32
e retornar um Guess
. O código no corpo da função new
testa value
para certificar-se de que está entre 1 e 100. Se value
não passar neste teste, fazemos uma chamada panic!
, que irá alertar o programador que está escrevendo o código de chamada que ele tem um bug que precisa corrigir, porque criar um Guess
com um valor value
fora desse intervalo violaria o contrato do qual Guess::new
se baseia. As condições em que Guess::new
pode entrar em pânico deve ser discutido em sua documentação de API voltada ao público; cobriremos as convenções de documentação indicando a possibilidade de um panic!
na documentação da API que você criou no Capítulo 14. Se value
passar no teste, criamos um novo Guess
com seu campo value
definido para o parâmetro value
e retornamos o Guess
.
Em seguida, implementamos um método chamado value
que empresta self
, não tem nenhum outro parâmetro e retorna um i32
. Esse tipo de método às vezes é chamado de getter, porque seu objetivo é obter alguns dados de seus campos e retorná-los. Este método público é necessário porque o campo value
da estrutura Guess
é privado. É importante que o campo value
seja privado, de forma que o código que usa a estrutura Guess
não possa ser definido o valor de value
diretamente: o código fora do módulo deve usar a função Guess::new
para criar uma instância de Guess
, garantindo assim que não haja nenhuma maneira de um Guess
ter um value
que não foi verificado pelas condições na função Guess::new
.
Uma função que tem um parâmetro ou retorna apenas números entre 1 e 100 poderia então declarar em sua assinatura que ela recebe ou retorna um Guess
em vez de um i32
e não precisaria fazer nenhuma verificação adicional em seu corpo.
Resumo
Os recursos de tratamento de erros do Rust são projetados para ajudá-lo a escrever um código mais robusto. A macro panic!
sinaliza que seu programa está em um estado que não pode controlar e permite que você diga ao processo para parar em vez de tentar prosseguir com valores inválidos ou incorretos. O enum Result
usa o sistema de tipos de Rust para indicar que as operações podem falhar de forma que seu código possa se recuperar. Você pode usar Result
para informar ao código que chama seu código de que ele também precisa lidar com o sucesso ou a falha em potencial. Usar panic!
e Result
nas situações apropriadas tornará seu código mais confiável em face de problemas inevitáveis.
Agora que você viu maneiras úteis de como a biblioteca padrão usa genéricos com os enums Option
e Result
, falaremos sobre como os genéricos funcionam e como você pode usá-los em seu código.
Traduzido por Acervo Lima. O original pode ser acessado aqui.
0 comentários:
Postar um comentário