En un mundo ideal, todo funciona perfectamente. Pero en el mundo real las cosas pueden salir mal, y la forma en que manejamos estas situaciones importa para que nuestro software sea robusto y confiable. Las excepciones son un concepto crucial en la programación que nos permite manejar errores y situaciones inesperadas de forma elegante.
Lanzar un error, si no se maneja, detiene el programa y arroja un mensaje de error. En la mayoría de los casos, no quieres que tu programa se detenga cuando ocurre un error; en su lugar, quieres manejar el error y seguir ejecutando el programa.
En Crystal, las excepciones se lanzan con la palabra clave raise y se les puede dar un String o un objeto Exception.
Si no se maneja, el programa se detendrá e imprimirá el mensaje de error.
raise "This is an error"
Crystal tiene varias excepciones integradas, como ArgumentError, IndexError, KeyError, IOError, SystemCallError, TypeError, ZeroDivisionError y muchas más.
Estas requieren que le pases un mensaje a la excepción.
raise ArgumentError.new("This is an argument error")
No queremos que nuestro programa falle cuando se lanza una excepción.
Por eso, cuando sabemos que un fragmento de código es propenso a errores, podemos envolverlo en un bloque begin y capturar la excepción con un bloque rescue.
El bloque begin marca el inicio del código que podría lanzar una excepción, y el bloque rescue maneja la excepción.
begin
raise "This is an error"
rescue
puts "An error occurred!"
end
El bloque rescue también se puede especificar con una variable para obtener el objeto de la excepción.
begin
raise "This is an error"
rescue ex
puts "An error occurred: #{ex.message}"
end
El bloque rescue también se puede especificar con un tipo de excepción específico para capturar solo esa excepción.
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
Se pueden usar varios bloques rescue para manejar distintos tipos de excepciones.
En el ejemplo de abajo, el primer bloque rescue capturará un ArgumentError, y el segundo bloque rescue capturará cualquier otra excepción.
begin
raise ArgumentError.new("This is an argument error")
rescue ArgumentError
puts "An argument error occurred!"
rescue
puts "An error occurred!"
end
El bloque begin también puede tener un bloque else, que se ejecuta si no se lanza ninguna excepción.
begin
puts "No error occurred"
rescue
puts "An error occurred!"
else
puts "No error occurred"
end
Por último, hay un bloque ensure que siempre se ejecuta, sin importar si se lanzó una excepción.
begin
raise "This is an error"
rescue
puts "An error occurred!"
ensure
puts "This is always executed"
end
Algunos métodos tienen dos versiones: una con ! y otra sin él.
Esto puede significar dos cosas distintas.
Una es que el método muta el objeto, y la otra es que el método puede lanzar una excepción.
Pero también existe otra convención sobre terminar un método con ?, mencionada en el concepto de Boolean.
Algunos métodos lanzan una excepción de forma predeterminada, pero también tienen una versión que termina con ? y que devuelve nil en lugar de lanzar una excepción.
Esto es ideal cuando quieres evitar que se lance un error. Puede beneficiar el rendimiento, ya que no tiene que crear una traza de pila y, si se configura correctamente, podría hacer que el código sea más seguro.
También puedes crear tus propias excepciones heredando de la clase Exception.
Si lo haces, puedes sobrescribir de forma opcional el método initialize para establecer el mensaje de la excepción.
Esto se puede hacer asignando a una variable de instancia llamada @message el mensaje.
class MyException < Exception
def initialize
@message = "This is my exception"
end
end
raise MyException.new
Por fin llegó el día que tanto esperabas: ahora tienes una hermosa granja en los Alpes.
Aún no te gusta levantarte muy temprano para alimentar a tus vacas.
Como eres un ingeniero excelente, construyes un dispensador de comida: el FEED-M-ALL.
Lo último que te falta para terminar tu proyecto es un fragmento de código que calcule la cantidad de forraje que debe recibir cada vaca. Es importante que cada vaca reciba la misma cantidad, así evitas conflictos. Las vacas son muy sensibles.
Por suerte, no necesitas deducir por tu cuenta todas las fórmulas para calcular las cantidades de forraje. Usas una misteriosa biblioteca externa que encontraste en internet. Se supone que da como resultado las vacas más felices. La biblioteca expone una clase que cumple la siguiente interfaz. Te basarás en ella en el código que escribas tú mismo.
class FodderCalculator
def fodder_amount! : Number
end
def fattening_factor! : Number
end
end
A medida que avances con tu código, irás mejorando el manejo de errores para que sea más robusto y más fácil de depurar después, cuando lo uses en tu día a día en la granja.
En primer lugar, te centras en escribir el código necesario para calcular la cantidad de forraje por vaca.
Implementa un método TheFarm.divide_food que acepte como argumentos un objeto FodderCalculator y un número de vacas como entero.
Para esta tarea, asume que el número de vacas que se pasa siempre es mayor que cero.
El método debe devolver la cantidad de comida por vaca.
Para hacer el cálculo, primero necesitas obtener la cantidad total de forraje para todas las vacas.
Esto se hace llamando al método fodder_amount! y pasándole el número de vacas.
Además, necesitas un factor por el que hay que multiplicar esa cantidad.
Ese factor lo obtienes llamando al método fattening_factor!.
Con estos dos valores y el número de vacas, ya puedes calcular la cantidad de comida por vaca (como un float64).
Si uno de los métodos que llamas devuelve un error, la ejecución debe detenerse y 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
Mientras trabajabas en la primera tarea, te diste cuenta de que la biblioteca externa que usas no tiene la calidad que esperabas. Por ejemplo, no sabe manejar bien los valores de entrada no válidos. Quieres sortear esa limitación agregando en tu propio código una comprobación del valor de entrada.
Escribe una función TheFarm.validate_input_and_divide_food que tenga la misma firma que TheFarm.divide_food de arriba.
TheFarm.divide_food y devolver el resultado de esa llamada."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
Comprobar el número de vacas antes de pasarlo fue una buena idea, pero no quedas del todo conforme con ese mensaje de error tan poco específico.
Decides hacerlo mejor creando una clase de error personalizada llamada InvalidCowsError.
El error personalizado debe contener el número de vacas (Int32) y un mensaje personalizado (String), y el método Expection debe serializar los datos con el siguiente formato:
{number of cows} cows are invalid: {custom message}
Ya con tu error personalizado, implementa una función TheFarm.validate_number_of_cows que acepte el número de vacas como entero y devuelva un error (o nil).
InvalidCowsError con el mensaje personalizado definido como "there are no negative cows".InvalidCowsError con el mensaje personalizado definido como "no cows don't need food".nil para indicar que la validación fue exitosa.TheFarm.validate_number_of_cows(5)
# => nil
TheFarm.validate_number_of_cows(-5)
# => InvalidCowsError: -5 cows are invalid: there are no negative cows
Después del duro trabajo de preparar esta función de validación, te das cuenta de que ya es de noche y dejas tu escritorio para disfrutar del atardecer sobre las montañas. La tarea de agregar de verdad la nueva función de validación a tu código la dejas para otro día.
Regístrate en Exercism para aprender y dominar Crystal con 26 conceptos142 ejercicios y mentoría humana real, todo gratis.