Excepciones
Generando una excepción
Una excepción es una clase de objeto especial, una instancia de la clase Exception
o de una clase descendiente de esa clase que representa una condición especial; indica que algo
ha salido mal. Cuando esto ocurre, se genera una excepción. Por defecto, los programas Ruby
terminan cuando una excepción ocurre pero es posible escribir código que maneje estas excepciones.
Este código es un bloque que es ejecutado si una excepción ocurre durante la ejecución de otro
bloque de código. Generar una excepción significa detener la ejecución normal del programa y
transferir el control al código que maneja la excepción en donde puedes ocuparte del problema que
ha sido encontrado o bien, terminar la ejecución del programa. Una de estas dos opciones
(solucionar el problema de alguna manera o detener la ejecución de programa) ocurre dependiendo
de si has praparcionado una cláusula rescue. Si no has proporcionado dicha cláusula,
el programa termina, por el contrario, si la cláusula existe, el control de ejecución fluye hacia
esta.
Ruby tiene algunas clases predefinidas -descendientes de la clase Exception- que te
ayudan a manejar errores que ocurren en tu programa. La siguiente figura muestra la jerarquía de
excepciones: 1
La tabla anterior muestra que la mayoría de las subclases extienden a la clase
StandardError. Estas son excepciones "normales" que los programas
Ruby tratan de manejar. Las otras excepciones representan errores más serios
de bajo nivel que los programas Ruby normales no intentan manejar.
El siguiente método genera una excepción cada vez que es llamado. El segundo mensaje
nunca va a ser mostrado. p43genera.rb
1 def genera_excepcion 2 puts 'Antes de la excepcion.' 3 raise 'Ha ocurrido un error' 4 puts 'Despues de la excepcion' 5 end 6 7 genera_excepcion
El resultado es:
El método raise está definido en el módulo Kernel. Por defecto,
raise genera una excepción de la clase RuntimeError. Para
generar una excepción de una clase en específico, puedes pasar el nombre de la clase
como argumento al método raise. p044inverso.rb
1 def inverse(x) 2 raise ArgumentError, 'El argumento no es un numero' unless x.is_a? Numeric 3 1.0 / x 4 end 5 puts inverse(2) 6 puts inverse('hola')
El resultado es:
Recuerda, los métodos que funcionan como preguntas (que regresan verdadero o falso),
con frecuencia son nombrados con un ? al final. is_a? es un
método de la clase Object que regresa true o false.
Cuando unless es utilizado al final de una expresión, quiere decir: ejecuta
la expresión anterior a menos que la condición sea verdadera.
Definiendo nuevas clases de excepciones: Para ser más específicos acerca de
un error, puedes definir tu propia subclase de Exception:
1 class NoReversibleError < StandandError 2 3 end
Manejando una excepción
Para manejar excepciones (handle exceptions), incluímos el código que pueda generar
una excepción en un bloque begin-end y usamos una o más cláusulas rescue
para indicarle a Ruby los tipos de excepción que queremos manejar. Es importante notar que el
cuerpo de la definición de un método es un bloque begin-end explícito; begin
es omitido y todo el cuerpo de la definición del método está sujeto al manejo de excepciones hasta
que aparezca la palabra end.
El programa p045manejandoexcep.rb ilustra lo anterior:
1 def genera_y_rescata 2 begin 3 puts 'Estoy antes de raise.' 4 raise 'Ha ocurrido un error.' 5 puts 'Estoy despues de raise.' 6 rescue 7 puts 'He sido rescatado.' 8 end 9 puts 'Estoy despues de begin.' 10 end 11 genera_y_rescata
El resultado es:
Observa que el código interrumpido por la excepción nunca es ejecutado.
Una vez que la excepción es rescatada, la ejecución continúa inmediatamente
después del bloque begin que la generó.
Si escribes una cláusula rescue sin lista de parámetros, el
parámetro por defecto es StandardError. Cada cláusula rescue
puede especificar múltiples excepciones que rescatar. Al final de cada cláusula
rescue puedes darle a Ruby el nombre de una varible local
para que reciba la excepción. Los parámetros de la cláusula rescue
pueden ser también expresiones arbitrarias (incluyendo llamadas a métodos)
que regresan una clase Exception. Si utilizamos raise
sin parámetros, genera la excepción.
Puedes apilar cláusulas rescue en un bloque begin-end.
Las excepciones que no sean manejadas por una cláusula rescue
fluirán hacia la siguiente:
1 begin 2 # ... 3 rescue UnTipoDeExcepcion 4 # ... 5 rescue OtroTipoDeExcepcion 6 # .. 7 else 8 # Otras excepciones 9 end
Para cada cláusula rescue en el bloque begin, Ruby
compara la excepción generada con cada uno de los parámetros en turno.
La ocurrencia tiene éxito si la excepción nombrada en la cláusula rescue
es del mismo tipo que la excepción generada. El código en una cláusula else
es ejecutado si el código en la expresiôn begin es ejecutado sin
excepciones. Si una excepción ocurre, entonces la cláusula else
no es ejecutada. El uso de una cláusula eles no es particularmente
común en Ruby.
Si quieres interrogar a una excepción rescatada, puedes asignar el objeto
de clase Exception a una variable en la cláusula rescue,
como se muestra en el programa p046excpvar.rb
1 begin 2 raise 'Una excepcion.' 3 rescue Exception => e 4 puts e.message 5 puts e.backtrace.inspect 6 end
El resultado es:
La clase Exception define dos métodos que regresan detalles acerca
de la excepción. El método message regresa una cadena que
puede proporcionar detalles legibles acerca de lo que ocurrió mal. El otro
método importante es backtrace. Este método regresa un array
de cadenas que representa la pila de ejecución hasta el punto en que la
excepción fue generada.
Si necesitas garantizar que algún proceso es ejecutado al final de un
bloque de código sin importar si se generó una excepción o no, puedes
usar la cláusula ensure. ensure va al final
de la última cláusula rescue y contiene un bloque de código
que siempre va a ser ejecutado.
Algunas excepciones comunes se muestran en la siguiente tabla, cortesía del libro Ruby for Rails:
Ejemplo: Vamos a modificar el programa p027.leeryescribir.rb
para incluír manejo de excepciones como se muestra en el ejemplo p046leeryescribir.rb
1 # Abrir un archivo y leer su contenido 2 # Nota que ya que esta presente un bloque, el archivo 3 # es cerrado automaticamente cuando se termina la ejecucion 4 # del bloque 5 begin 6 File.open('p014estructuras.rb', 'r') do |f1| 7 while linea = f1.gets 8 puts linea 9 end 10 end 11 12 # Crer un archivo y escribir en el 13 File.open('prueba.txt', 'w') do |f2| 14 f2.puts "Creado desde un programa Ruby!" 15 end 16 rescue Exception => msg 17 # mostar el mensaje de error generado por el sistema 18 puts msg 19 end
Mensajes de error inapropiados pueden proporcionar información crítica
de una aplicación que puede ayudar a personas que quieran atacar nuestra
aplicación. El problema más común ocurre cuando un mensaje de error
detallados como pilas de ejecución, información de la base de datos
y códigos de error son mostrados al usuario. Los analistas en seguridad
ven al manejo de excepciones y al logging como áreas potenciales
de riesgo. Es recomendable que las aplicaciones en producción no utilicen,
por ejemplo, llamadas a puts e.backtrace.inspect a menos
que sean dirigidas directamente a un log que no es visible
para los usuarios finales.
Ejemplo de validación
Este es un ejemplo del libro Ruby Cookbook que muestro cómo podemos realizar validaciones de datos proporcionados por el usuario.
1 class Nombre 2 3 # Definir metodos getter pero no metodos setter 4 attr_reader :nombre, :apellido 5 6 # Establocer reglas para cuando alguien trata de 7 # asignar un nombre 8 9 def nombre=(nombre) 10 if nombre == nil or nombre.size == 0 11 raise ArgumentError.new("Todos las personas deben tener un nombre") 12 end 13 nombre = nombre.dup 14 nombre[0] = nombre[0].chr.capitalize 15 @nombre = nombre 16 end 17 18 # Establocer reglas para cuando alguien trata de 19 # asignar un apellido 20 21 def apellido=(apellido) 22 if apellido == nil or apellido.size == 0 23 raise ArgumentError.new("Todas las personas deben tener un apellido") 24 end 25 @apellido = apellido 26 end 27 28 def nombre_completo 29 "#{@nombre} #{@apellido}" 30 end 31 32 # Delegar a los metodos setter en vez de asignar 33 # las variables de instancia directamente 34 35 def initialize(nombre, apellido) 36 self.nombre = nombre 37 self.apellido = apellido 38 end 39 end 40 41 jacobo = Nombre.new('Jacobo', 'Berendes') 42 jacobo.nombre = 'Mary Sue' 43 puts jacobo.nombre_completo 44 45 john = Nombre.new('john', 'von Neumann') 46 puts john.nombre_completo 47 john.nombre = nil #genera una excepcion 48 49 Nombre.new('Carmen', nil) #genera una excepcion
La clase Nombre registra los nombres y apellidos de
personas. Utiliza metodos setter para forzar dos
reglas: todas las personas deben tener nombres y apellidos y
todos los nombres deben comenzar con una letra mayúscula.
La clase Nombre ha sido escrita de tal manera que
estas reglas se aplican tanto en el método constructor como
una vez que el objeto ha sido creado. Algunas veces no puedes
confiar en los datos que vienen de los métodos setter.
Es entonces cuando puedes definir tus propios métodos para dener
datos corruptos antes de que infecte tus objetos.
Dentro de una clase, tienes acceso directo a las variables de
instancia. Puedes simplemente asignar a variables de instancia
y los métodos setter no son ejecutados. Si quieres que se ejecute
el método setter debes llamarlo explícitamente. Nota
come en el método Nombre#initialize anterior, llamamos
a los métodos nombre= y apellido= en
lugar de asignarlos directamente a @nombre y
@apellido. De esta manera nos aseguramos que los
métodos que contienen la validación son ejecutados.
1 Fuente: Programming Ruby