No mundo ideal, tudo funciona perfeitamente. Mas, no mundo real, as coisas podem correr mal, e a forma como tratamos estas situações é importante para garantir que o nosso software é robusto e fiável. As exceções são um conceito fundamental na programação que nos permite tratar erros e situações inesperadas com elegância.
Lançar um erro, se não for tratado, interrompe o programa e lança uma mensagem de erro. Na maioria dos casos, não queres que o programa pare quando ocorre um erro; queres antes tratar o erro e continuar a executar o programa.
No Crystal, as exceções são lançadas com a palavra-chave raise e podem receber uma String ou um objeto Exception.
Se não for tratada, o programa para e imprime a mensagem de erro.
raise "This is an error"
Existem várias exceções incorporadas no Crystal, como ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError e muitas mais.
Estas exigem que passes uma mensagem à exceção.
raise ArgumentError.new("This is an argument error")
Não queremos que o nosso programa vá abaixo quando é lançada uma exceção.
Por isso, quando sabemos que um trecho de código é propício a erros, podemos envolvê-lo num bloco begin e capturar a exceção com um bloco rescue.
O bloco begin marca o início do código que pode lançar uma exceção, e o bloco rescue trata a exceção.
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
O bloco rescue também pode ser especificado com uma variável para obter o objeto da exceção.
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
O bloco rescue também pode indicar um tipo específico de exceção, para capturar apenas essa exceção.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
end
# or
begin
raise ArgumentError.new("This is an argument error")
rescue ex : ArgumentError
puts "An argument error occurred: #{ex.message}"
end
Podem usar-se vários blocos rescue para tratar diferentes tipos de exceções.
No exemplo abaixo, o primeiro bloco rescue captura um ArgumentError, e o segundo bloco rescue captura qualquer outra exceção.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
O bloco begin também pode ter um bloco else, que é executado se não for lançada nenhuma exceção.
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
Por último, existe um bloco ensure que é sempre executado, independentemente de ter sido lançada uma exceção.
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
Alguns métodos têm duas versões: uma com ! e outra sem.
Isto pode significar duas coisas diferentes.
Uma é que o método altera o objeto; a outra é que o método pode lançar uma exceção.
Mas existe também outra convenção, mencionada no conceito de Boolean, de terminar um método com ?.
Alguns métodos lançam uma exceção por predefinição, mas têm também uma versão terminada em ? que devolve nil em vez de lançar uma exceção.
Isto é ideal quando queres evitar que seja lançado um erro. Isto pode beneficiar o desempenho, uma vez que não é preciso criar um stack trace e, se for bem configurado, pode tornar o código mais seguro.
Também podes criar as tuas próprias exceções herdando da classe Exception.
Ao fazê-lo, podes, opcionalmente, substituir o método initialize para definir a mensagem da exceção.
Isto pode ser feito atribuindo a mensagem a uma variável de instância chamada @message.
class MyException < Exception
def initialize
@message = "This is my exception"
end
end
raise MyException.new
O dia que esperaste tanto tempo chegou finalmente e és agora o orgulhoso proprietário de uma bela quinta nos Alpes.
Continuas a não gostar de acordar demasiado cedo para alimentar as tuas vacas.
Como és um excelente engenheiro, construis um distribuidor de comida, o FEED-M-ALL.
A última coisa de que precisas para terminar o teu projeto é um pedaço de código que calcule a quantidade de forragem que cada vaca deve receber. É importante que cada vaca receba a mesma quantidade, tens de evitar conflitos. As vacas são muito sensíveis.
Felizmente, não precisas de descobrir sozinho todas as fórmulas para calcular as quantidades de forragem. Usas uma biblioteca externa misteriosa que encontraste na internet. Supostamente, o resultado são as vacas mais felizes. A biblioteca expõe uma classe que cumpre a seguinte interface. Vais apoiar-te nisso no código que escreveres.
class FodderCalculator
def fodder_amount! : Number
end
def fattening_factor! : Number
end
end
À medida que trabalhas no teu código, vais melhorar o tratamento de erros para o tornar mais robusto e mais fácil de depurar quando o usares no teu dia a dia na quinta.
Antes de mais, concentras-te em escrever o código necessário para calcular a quantidade de forragem por vaca.
Implementa um método TheFarm.divide_food que aceita como argumentos um objeto FodderCalculator e um número de vacas como inteiro.
Para esta tarefa, assumes que o número de vacas recebido é sempre maior que zero.
O método deve devolver a quantidade de comida por vaca.
Para fazer o cálculo, precisas primeiro de obter a quantidade total de forragem para todas as vacas.
Fazes isso chamando o método fodder_amount! e passando o número de vacas.
Além disso, precisas de um fator pelo qual essa quantidade tem de ser multiplicada.
Obténs esse fator chamando o método fattening_factor!.
Com estes dois valores e o número de vacas, podes agora calcular a quantidade de comida por vaca (como um float64).
Se um dos métodos que chamas devolver um erro, a execução deve parar e devolver 0.
fodder_calculator = FodderCalculator.new(50, 1.5)
TheFarm.divide_food(fodder_calculator, 5)
# => 15.0
fodder_calculator = FodderCalculator.new(50, nil)
TheFarm.divide_food(fodder_calculator, 5)
# => 0
Enquanto trabalhavas na primeira tarefa acima, apercebeste-te de que a biblioteca externa que usas não tem tanta qualidade como pensavas. Por exemplo, não consegue lidar corretamente com valores de entrada inválidos. Queres contornar esta limitação acrescentando uma verificação do valor de entrada no teu próprio código.
Escreve uma função TheFarm.validate_input_and_divide_food com a mesma assinatura que a TheFarm.divide_food acima.
TheFarm.divide_food e devolver o resultado dessa chamada."Number of cows must be greater than 0".TheFarm.validate_input_and_divide_food(fodderCalculator, 5)
# => 15
TheFarm.validate_input_and_divide_food(fodderCalculator, -2)
# Error: Number of cows must be greater than 0
Verificar o número de vacas antes de o passar adiante foi uma boa jogada, mas não estás totalmente satisfeito com a mensagem de erro pouco específica.
Decides fazer melhor, criando uma classe de erro personalizada chamada InvalidCowsError.
O erro personalizado deve guardar o número de vacas (Int32) e uma mensagem personalizada (String), e o método Expection deve serializar os dados no seguinte formato:
{number of cows} cows are invalid: {custom message}
Com o teu erro personalizado em mãos, implementa uma função TheFarm.validate_number_of_cows que aceita o número de vacas como inteiro e devolve um erro (ou nil).
InvalidCowsError com a mensagem personalizada definida como "there are no negative cows".InvalidCowsError com a mensagem personalizada definida como "no cows don't need food".nil para indicar que a validação foi bem-sucedida.TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
Depois do trabalho árduo de montar esta função de validação, reparas que já é noite e deixas a secretária para apreciar o pôr do sol sobre as montanhas. Deixas para outro dia a tarefa de acrescentar mesmo a nova função de validação ao teu código.
Inscreve-te no Exercism para aprenderes e dominares Crystal com 26 conceitos142 exercícios, e mentoria humana real, tudo grátis.