Manual de Ruby: Manejo de excepciones
De Foros del Web
En el mundo real, los errores ocurren. Los buenos programas (y programadores) deben anticipar y disponer lo necesario para manejarlos correctamente. Esto no siempre es tan fácil como se escucha. A menudo, el código que detecta un error no tiene el contexto para saber qué hacer al respecto. Por ejemplo, al intentar abrir un archivo que no existe es aceptable en algunas circunstancias y es un error fatal en otras. ¿Que es lo que su modulo de manejo de archivos debería hacer?
El enfoque tradicional consiste en utilizar códigos de retorno. El método "open" devuelve un valor específico para decir que falló. Este valor se propaga de nuevo a través de las capas de las rutinas de llamada hasta que alguien quiera asumir la responsabilidad por ello.
El problema con este enfoque es que la gestión de todos estos códigos de error puede ser un dolor de cabeza. Si una función llama a los metodos "open", y luego "read", y finalmente "close", y cada uno puede devolver una indicación de error, ¿cómo distingue la función estos códigos de error en el valor que devuelve a su invocador?
En gran medida, las excepciones resuelven este problema. Las excepciones le permiten empaquetar información acerca de un error en un objeto. Ese objeto de tipo excepción después se propagan de regreso en la pila de llamadas de forma automática hasta que el sistema de ejecución encuentra el código que explícitamente declara que sabe manejar ese tipo de excepción.
Contenido |
La clase de "Exception"
El paquete que contiene la información acerca de una excepción es un objeto de la clase "Exception" o uno de la clase que sean hijos de la clase "Exception". Ruby predefine una jerarquía ordenada de las excepciones. Como veremos más adelante, esta jerarquía hace que el manejo de excepciones mucho más fácil.
Cuando usted tenga que lanzar una excepción, puede utilizar una de las clases incorporadas de "Exception", o usted puede crear una propia. Si usted crea su propia, es posible que desee hacer una subclase de "StandardError" o uno de sus hijos. Si no lo hace, su excepción, no podrá capturarse de forma predeterminada.
Cada "Exception" está asociada a un de mensaje "string" y una "stack backtrace". Si usted define sus propias excepciones, puede agregar información adicional.
La sintaxis para escribir una imagen la podemos relacionar con una analogia en Java
- Un bloque begin/end para el codigo candidato (como el codigo dentro del try en Java)
- Un bloque de codigo rescue que sirve para el manejo de la excepcion (como el catch en Java)
- Utilizamos la palabra reservada raise para levantar una excepcion (como el throw en Java)
- La palabra reservada ensure (como el final de Java)
Veamos el siguiente ejemplo:
- begin
- iFile = File.open(name,"r")
- buffer = ifile.read(512)
- end
- rescue SystemCallError
- print "File Operation failed"
- raise
- end
- ensure
- ifile.close unless ifile.nil?
- end
Definiendo nuestras propias Excepciones
Ruby nos permite crear nuestras propias Excepciones siempre y cuando herenden de la clase StandardError o sus hijas. En caso contrario no son tomadas como una excepcion.
Manejo de excepciones
Nuestras descargas jukebox de canciones desde Internet a través una conexión TCP. El código básico es simple:
opFile = File.open (opName, "w") while data = socket.read (512)
opFile.write (data)
end
¿Qué sucede si se obtiene un error fatal a medio camino a través de la descarga? Desde luego, no se desea almacenar una canción incompleta en la lista de canciones.
Vamos a añadir algunas lineas de codigo de manejo de excepciones veamos cómo ayuda. Adjuntamos el código que podría plantear una excepción en un bloque "begin/end" y el usamos las cláusulas "rescue" para decirle a Ruby los tipos de excepciones que se desea manejar. En este caso estamos interesados en atrapar excepciones "SystemCallError" (y, en consecuencia, las excepciones que son subclases de "SystemCallError"), así que eso es lo que aparece en la línea "rescue". En el bloque de manejo de errores, reportamos el error, cerrar y borrar el archivo de salida, y luego volver a subir la excepción.
opFile = File.open (opName, "w") begin
# Excepciones planteadas por este código # aplicadas por la cláusula rescue while data = socket.read (512) opFile.write (data) end
rescue SystemCallError
$stderr.print "E/S ha fallado: " + $! opFile.close File.Delete (opName) raise
end Cuando se produce una excepción, e independiente de cualquier manejo de excepciones posteriores, Ruby coloca una referencia al objeto "Exception" asociado con la excepción de la variable global "$!" (El signo de exclamación, refleja nuestra sorpresa de que algo de nuestro código puede causar errores). En el ejemplo anterior, se utilizó esta variable para dar formato a nuestro mensaje de error.
Después de cerrar y borrar el archivo, llamamos a "raise" sin parámetros, el cual carga la excepción en "$!". Esta es una técnica útil, ya que le permite escribir código que filtra excepciones, pasando de los que no puede manejar a niveles superiores. Es casi como la aplicación de una jerarquía de herencia para el procesamiento de errores.
Usted puede tener varias cláusulas "rescue" en un bloque "begin", y cada cláusula "rescue" puede especificar múltiples excepciones a capturar. Al final de cada cláusula "rescue" usted puede dar a Ruby el nombre de una variable local para recibir la excepción que coincida. Muchas personas encuentran esto más fácil de leer que el uso de "$!" por todos lados.
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compile: " + boom
rescue StandardError => bang
print "Error running script: " + bang
end
¿Cómo decide Ruby que cláusula de "rescue" se debe ejecutar? Resulta que el tratamiento es muy similar a la utilizada por la sentencia "case". Para cada cláusula "rescue" en el bloque "begin", Ruby compara la excepción frente a cada uno de los parámetros a su vez. Si la excepción propuesta coincide con un parámetro, Ruby ejecuta el cuerpo de "rescue" se detiene. La coincidencia se hace con "$!.kind_of?(parameter)", y así tendrá éxito si el parámetro tiene la misma clase que la excepción o es un ancestro de la excepción. Si se escribe una cláusula "rescue" sin lista de parámetros, los valores predeterminados de parámetros para "StandardError".
Si ninguna cláusula "rescue" coincide, o si se produce una excepción fuera de un bloque "begin/end", Ruby se mueve hacia arriba de la pila y busca un controlador de excepciones para quien la invoca, luego en quien realizo la llamada, y así sucesivamente.
Aunque los parámetros de la cláusula "rescue" son típicamente los nombres de las clases "Exception", en realidad pueden ser expresiones arbitrarias (incluyendo las llamadas a métodos) que devuelven una clase "Exception".
Tidying up
A veces es necesario garantizar que algun proceso se realiza al final de un bloque de código, independientemente de si la excepción se planteó. Por ejemplo, usted puede tener un archivo abierto a la entrada del bloque, y necesita asegurarse de que se cierra cuando el bloque exista.
La cláusula "ensure" precisamente hace esto. "ensure" va después de la ultima cláusula "rescue" y contiene un trozo de código que siempre se ejecutará cuando el bloque termina. No importa si el bloque salide con normalidad, si se levanta y rescata una excepción, o si se ha interrumpido por una excepción no capturada. El bloque "ensure" funciona asi.
f = File.open("testfile") begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
La cláusula "else" es una similar, aunque menos útil, construirla. Si está presente, va después de las cláusulas "rescue" y antes de que la "ensure". El cuerpo de una cláusula "else" se ejecuta solamente si hay excepciones y son planteados por el cuerpo principal del código.
f = File.open("testfile") begin
# .. process
rescue
# .. handle error
else
puts "Congratulations-- no errors!"
ensure
f.close unless f.nil?
end
Catch y Throw
Mientras el mecanismo de excepción de "raise" y "rescue" es muy bueno para abandonar la ejecución cuando las cosas van mal, a veces es bueno poder saltar de algunas anidaciones construidas durante el procesamiento normal. Aquí es donde "Catch" y "Throw" vienen de la mano.
catch (:done) do
while gets throw :done unless fields = split(/\t/) songList.add(Song.new(*fields)) end songList.play
end
"catch" define un bloque que tiene la etiqueta con el nombre dado (que puede ser un "Symbol" o un "String"). El bloque se ejecuta con normalidad hasta que se encuentra un "throw".
Cuando Ruby se encuentra con un "throw", hace una copia a la pila de llamadas en busca de un bloque "catch" con el símbolo correspondiente. Cuando lo encuentra, Ruby lleva la pila a ese punto y termina el bloque. Si el "throw" se llama con el segundo parámetro opcional, entonces el valor se devuelve como el valor del "catch". Así, en el ejemplo anterior, si la entrada no contiene el formato correcto, un "throw" saltará al final del "catch" correspondiente, no sólo se da por concluido el bucle "while", sino también la reproducción saltara la lista de canciones.
En el ejemplo siguiente se utiliza un "throw" para terminar la interacción con el usuario si "!" se escribe.
def promptAndGet(prompt)
print prompt res = readline.chomp throw :quitRequested if res == "!" return res
end
catch :quitRequested do
name = promptAndGet("Name: ")
age = promptAndGet("Age: ")
sex = promptAndGet("Sex: ")
# ..
# process information
end
Como ilustra este ejemplo, un "throw" no tiene que aparecer dentro del ámbito estatico del "catch".
Traducido de Programming Ruby The Pragmatic Programmer's Guide.
