|
|||
open devuelve un valor específico para indicar que ha fallado.
Este valor se propaga hacia atrás a través de las sucesivas llamadas, hasta
que alguna función se hace responsable de él.
El problema de esta solución es que manejar todos los posibles códigos de
error es penoso. Si una función llama a open, luego a
read y por último a close, y todas las llamadas
pueden devolver un indicador de error, ¿cómo puede la función distinguir estos
códigos de error en el valor que retorna al código que la ha llamado?
De forma general, las excepciones resuelven este problema. Permiten empaquetar
la información relativa a un error en un objeto. Dicho objeto es entonces
propagado hacia atrás en la pila de llamadas automáticamente hasta que el
sistema encuentra código que declara de forma explícita cómo manejar ese tipo
de excepción.
Exception, o alguna de sus clases herederas. Ruby
predefine una precisa jerarquía de excepciones, mostrada en la Figura 8.1 de
la página XX. Como veremos más adelante, dicha jerarquía transforma el manejo
de excepciones en una tarea mucho más sencilla.
| Figura no disponible... |
Exception por defecto, o puedes crear una tú mismo. Si
creas una, quizá quieras que herede de StandardError o
alguna de sus hijas. De no hacerlo así, tu excepción no será capturada por
defecto.
Cada Exception tiene asociados un mensaje y una pila de
llamadas. Si defines tus propias excepciones, puedes añadir información
adicional.
opFile = File.open(opName, "w") while data = socket.read(512) opFile.write(data) end |
begin/end y usamos cláusulas rescue para informar a
Ruby de los tipos de excepciones que queremos manejar. En este caso, estamos
interesados en atrapar excepciones SystemCallError (y,
por extensión, cualquier excepción que sea una subclase de
SystemCallError), así
que eso es lo que aparece en la línea rescue. En el bloque que maneja
la excepción, anunciamos el error, cerramos y borramos el fichero de salida y
relanzamos la excepción.
opFile = File.open(opName, "w") begin # Exceptions raised by this code will # be caught by the following rescue clause while data = socket.read(512) opFile.write(data) end rescue SystemCallError $stderr.print "IO failed: " + $! opFile.close File.delete(opName) raise end |
Exception
asociado en la variable global $! (el punto de
exclamación, presumiblemente, denota la sorpresa fruto de constatar que
nuestro código podría causar errores). En el ejemplo previo, usamos
dicha variable para formatear el mensaje de error.
Tras cerrar y borrar el fichero, llamamos a raise sin parámetros
para relanzar la excepción contenida en $!. Esta técnica es útil,
pues permite filtrar excepciones, pasando aquellas que no puedes manejar aún a
niveles superiores. Es casi como implementar una jerarquía de herencia para el
procesado de errores.
Puede haber múltiples cláusulas rescue en un bloque begin, y
cada una puede especificar múltiples excepciones a atrapar. Al final de cada
cláusula, puedes dar a Ruby el nombre de una variable local que reciba la
excepción específica. Mucha gente encuentra esto más legible que usar
$! por sistema.
begin eval string rescue SyntaxError, NameError => boom print "String doesn't compile: " + boom rescue StandardError => bang print "Error running script: " + bang end |
case. Para cada cláusula rescue en
un bloque begin, Ruby compara la excepción lanzada con cada uno de
los parámetros por turno. Si se produce una coincidencia, ejecuta el cuerpo de
dicho rescue y pára de mirar. La concordancia se busca usando
$!.kind_of?(parámetro), de forma que tendrá éxito si el
parámetro es de la misma clase que la excepción o si es un ancestro. Si
escribes una cláusula rescue sin lista de parámetros, se usará como
valor por defecto StandardError.
Si ninguna cláusula rescue coincide, o si se lanza una excepción
fuera de un bloque begin/end, Ruby asciende en la pila y
busca un manejador de excepciones en la función que ha efectuado la llamada, y
luego en la que ha llamado a esta última, y así sucesivamente.
Aunque los parámetros de una cláusula rescue normalmente son los
nombres de clases Exception, en realidad pueden ser
expresiones arbitrarias (incluyendo llamadas a métodos) que retornen una clase
Exception.
ensure se encarga justo de esto.
ensure va después del último rescue y contiene un fragmento
de código que se ejecutará siempre cuando el bloque termine. No importa si el
bloque finaliza normalmente, si lanza y atrapa una excepción, o si se produce
una excepción que no sabe manejar. El bloque ensure se ejecutará.
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
|
else, aunque menos útil, tiene su valor. De estar
presente, va después de las cláusulas rescue y antes de
ensure. El cuerpo de un else se ejecuta sólo si no se lanza
ninguna excepción en el bloque principal de código.
f = File.open("testfile")
begin
# .. procesa
rescue
# .. maneja error
else
puts "Felicidades-- ¡no hay errores!"
ensure
f.close unless f.nil?
end
|
retry dentro de una cláusula
rescue para repetir el bloque begin/end completo.
Ya que este hecho conlleva un claro riesgo de meterse en bucles infinitos, es
conveniente usarlo con precaución (y con un dedo acariciando
la tecla de interrupción).
Como ejemplo de código que contiene excepciones, echa un vistazo a lo
siguiente, adaptado de la biblioteca net/smtp.rb de Minero Aoki.
@esmtp = true begin # First try an extended login. If it fails because the # server doesn't support it, fall back to a normal login if @esmtp then @command.ehlo(helodom) else @command.helo(helodom) end rescue ProtocolError if @esmtp then @esmtp = false retry else raise end end |
EHLO, que no está soportado universalmente. Si el intento de conexión
falla, el código da a la variable @esmtp el valor
false y reintenta la conexión. Si falla de nuevo, la excepción
es relanzada a la función que ha llamado al código.
Kernel::raise
.
raise raise "bad mp3 encoding" raise InterfaceException, "Keyboard failure", caller |
RuntimeError). Se usa en manejadores de
excepción que necesitan interceptar una excepción antes de pasarla a otro
nivel.
La segunda forma crea una nueva excepción RuntimeError,
estableciendo su mensaje a la cadena dada. Esta es lanzada a la pila de
llamadas.
La tercera forma usa el primer argumento para crear una excepción y establece
el segundo argumento como mensaje asociado y el tercer argumento como traza
de la pila. Por lo general, el primer argumento será, o bien el nombre de una
clase en la jerarquía Exception, o una referencia al
objeto de una de dichas clases.[Técnicamente, puede ser cualquier
objeto que responda al mensaje exception retornando otro objeto
tal que object.kind_of?(Exception) sea verdadero.] La traza
de la pila se produce normalmente usando el método
Kernel::caller
.
He aquí algunos ejemplos típicos de raise en acción.
raise
raise "Missing name" if name.nil?
if i >= myNames.size
raise IndexError, "#{i} >= size (#{myNames.size})"
end
raise ArgumentError, "Name too big", caller
|
raise ArgumentError, "Name too big", caller[1..-1] |
class RetryException < RuntimeError attr :okToRetry def initialize(okToRetry) @okToRetry = okToRetry end end |
def readData(socket) data = socket.read(512) if data.nil? raise RetryException.new(true), "transient read error" end # .. normal processing end |
begin stuff = readData(socket) # .. process stuff rescue RetryException => detail retry if detail.okToRetry raise end |
raise y rescue es
adecuado para abandonar la ejecución cuando las cosas van mal, a veces es
conveniente poder saltar fuera de una construcción profundamente anidada
durante el procesamiento normal. Es aquí donde catch y throw
se tornan útiles.
catch (:done) do while gets throw :done unless fields = split(/\t/) songList.add(Song.new(*fields)) end songList.play end |
catch define un bloque al que se asigna un determinado nombre (que
puede ser un Symbol o un
String). El bloque se ejecuta normalmente hasta que un
throw es encontrado.
Cuando Ruby encuentra un throw, recorre la pila de llamadas hacia
atrás a la caza de un bloque catch coincidente. Cuando lo encuentra,
Ruby retorna la pila a ese punto y finaliza el bloque. Si el throw es
llamado con un segundo parámetro opcional, dicho valor se devuelve como el
valor de catch. Así, en el ejemplo previo, si la entrada no contiene
líneas correctamente formateadas, el throw saltará al final del
correspondiente catch, no sólo finalizando el bucle while,
sino evitando, además, reproducir la lista de canciones.
El siguiente ejemplo usa un throw para finalizar la interacción con
el usuario si ``!'' se introduce como respuesta a cualquier petición de datos.
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
|
throw no tiene por qué aparecer dentro del
ámbito estático de catch.