Excepciones en ADA

Una excepción es una situación que requiere de un tratamiento especial que escapa al funcionamiento normal de un programa (o parte de él). En los capítulos anteriores varias veces se ha indicado que si ocurre un error durante la ejecución de un programa se origina una excepción (generalmente CONSTRAIN _ERROR). En este capítulo se describirá el mecanismo de manejo de excepciones.
Algunas de las excepciones predefinidas dentro del lenguaje Ada son:

CONSTRAIN_ERROR: generalmente indica que algo se ha salido de rango.

NUMERIC_ERROR: esto ocurre cuando algo erróneo pasó en los cálculos aritméticos, por ejemplo, el intentar dividir por cero.

PROGRAM_ERROR: ocurre si se intenta violar de alguna manera el control de ejecución, por ejemplo, al llamar a un subprograma cuyo cuerpo (body) todavía no ha sido construido.

STORAGE_ERROR:  ocurre  al  salirse  del  espacio  de  memoria,  por  ejemplo,  se  llama  a  una  función recursiva FACTORIAL con un parámetro muy grande.

 

    • Manejo de excepciones

Si sabemos que una excepción puede ocurrir en alguna parte de nuestro programa, podemos escribir un “manejador de excepciones” para tratar la situación. Por ejemplo, supongamos que escribimos

begin


— secuencia de comandos


exception
when CONSTRAIN_ERROR => — hacer algo
. . .
end;

Si se origina una excepción CONSTRAIN_ERROR mientras se ejecuta la secuencia de comandos entre begin y excepction, entonces el flujo de control es interrumpido e inmediatamente transferido a la secuencia de comandos que sigue a =>. A la cláusula que comienza con when se le conoce como “manejador de excepción” (exception handler).
Un ejemplo trivial sería el determinar el valor de una variable TOMORROW a partir de TODAY
(ambas de tipo DAY)


begin


TOMORROW:= DAY´SUCC(TODAY);


exception
when CONSTRAIN_ERROR => TOMORROW:= DAY´FIRST;
end;

Si TODAY es DAY´LAST (es decir, SUN) cuando se intente evaluar DAY´SUCC(TODAY) surgirá la excepción CONSTRAIN_ERROR. El control pasará entonces al manejador de CONSTRAIN_ERROR y se evaluará la expresión DAY´FIRST, cuyo valor será entregado como resultado.
En realidad este no es un buen ejemplo puesto que las excepciones deberían ser usadas en casos con
baja probabilidad de ocurrencia. Puesto que el 14% de los días son domingo sería más adecuado el siguiente código.

if TODAY = DAY´LAST then
TOMORROW:= DAY´FIRST;


else end if;


TOMORROW:= DAY´SUCC(TODAY);


sin embargo, es un ejemplo sencillo que ilustra el mecanismo involucrado.
Es importante recalcar que el control nunca vuelve a la unidad donde apareció la excepción. La secuencia de comandos que sigue a => reemplaza al resto de la unidad y completa la ejecución de ésta.
Entre exception y end es posible escribir varios manejadores de excepciones,

 


begin


 

— secuencia de comandos


 

exception
when NUMERIC_ERROR|CONSTRAIN_ERROR =>
PUT(“Numeric or Constrain error ocurred”);
when STORAGE_ERROR =>
PUT(“Ran out of space”);
. . .
when others =>
PUT (“Something else went wrong”);


end;


. . .


En el ejemplo se envía un mensaje de acuerdo al tipo de excepción. Nótese la similitud con el comando case. Cada when es seguido por uno o más nombres de excepciones separados  por  barras verticales. Como es usual, podemos escribir others; con las restricciones y significado análogos al comando case.
Los manejadores de excepciones pueden aparecer al final de un bloque, cuerpo de subprograma,
cuerpo de paquete o cuerpo de una tarea, y tienen acceso a todas las entidades declaradas en la unidad. Los ejemplos anteriores mostraban un bloque degenerado que no contiene una parte declarativa. Una versión correcta para determinar TOMORROW como una función sería

function TOMORROW(TODAY:DAY) return DAY is begin
return DAY´SUCC(TODAY);
exception
when CONSTRAIN_ERROR =>
return  DAY´FIRST;
end TOMORROW;

Es importante recalcar que el control nunca vuelve a la unidad donde se originó la excepción. La secuencia de comandos que sigue a => reemplaza la que queda de la unidad en cuestión y así completa su ejecución. Por lo tanto, un manejador de excepciones dentro de una función debería contener una instrucción return para entregar un resultado de “emergencia”.
Una instrucción goto no puede transferir en control desde una unidad a uno de sus manejadores o viceversa, o desde un manejador a otro. Sin embargo, aparte de esta restricción, las instrucciones dentro de un manejador pueden ser tan complejas como se requiera.
Un manejador declarado al final del cuerpo de un paquete se aplica sólo a la secuencia de inicialización del paquete y no a los subprogramas del paquete. Estos últimos deberían tener sus propios manejadores.
¿Qué ocurre si se origina una excepción para la cual la unidad involucrada no posee un manejador? La respuesta es que la excepción se propaga dinámicamente. Esto significa que la ejecución de la unidad termina y la excepción es traspasada al punto en el cual se realizó la llamada a la unidad. En el caso de un bloque se busca un manejador en la unidad que lo contenga.
En el caso de un subprograma, la llamada termina y se busca un manejador en la unidad desde la que se llamó al subprograma. Este proceso se repite hasta que se llega a una unidad que contenga el manejador apropiado o se alcanza al nivel superior, es decir, se llega al programa principal y se obtendrá un mensaje ad- hoc del ambiente de ejecución (run time environment).
Es importante entender que las excepciones se propagan dinámica y no estáticamente. Es decir, una excepción que no es manejada por un subprograma es propagada a la unidad que llama al subprograma y no a la unidad que contiene su declaración (el que puede o no ser el mismo).
Si las instrucciones de un manejador a su vez originan una excepción, la unidad es terminada y la excepción se propaga a la unidad llamadora: los manejadores no entran en loop.

    • Declaración y generación de excepciones

En general, no es una buena práctica el utilizar las excepciones predefinidas para detectar situaciones inusuales, porque no se garantiza que las excepciones han surgido por las situaciones que se están modelando, y no por alguna otra situación anómala.
Como ejemplo consideremos el paquete STACK de la sección 8.1. Si llamamos a PUSH cuando la pila está llena, entonces la instrucción TOP:= TOP + 1; conducirá a CONSTRAIN_ERROR. Análogamente, tendremos una excepción del mismo tipo con TOP:= TOP – 1; con la función POP. Puesto que ninguno de los subprogramas tiene manejador de excepción, esta se propagará a la unidad que los llama. Entonces, podríamos escribir


declare


 

use STACK;

begin


 

. . .
PUSH(M);
. . .
N:=POP;
. . .

exception
when CONSTRAIN_ERROR =>
— ¿manipulación incorrecta de la pila?
end;

y el uso incorrecto de la pila hará que el control se transfiera al manejador de la excepción CONSTRAIN_ERROR. Sin embargo, no existe garantía de que la excepción se origine debido al uso incorrecto de la pila; puesto que alguna otra situación dentro del bloque y ajena al uso de la pila podría provocar una excepción CONSTRAIN_ERROR
Una solución mejor es generar una excepción especialmente declarada para indicar el uso incorrecto de la pila. Entonces, el paquete podría reescribirse de la siguiente manera

package STACK is
ERROR: exception;
procedure PUSH(X:INTEGER);
function POP return INTEGER;
end STACK;

package body STACK is
MAX: constant INTEGER:= 100; S: array (1 .. MAX) of INTEGER; TOP:INTEGER range 0 .. MAX;

procedure PUSH(X:INTEGER) is begin
if TOP = MAX then
raise ERROR;
end if;
TOP := TOP + 1; S(TOP):=X;
end PUSH;

function POP return INTEGER is begin
if TOP = 0 then
raise ERROR;
end if;
TOP := TOP – 1;
return S(TOP +1);
end POP;

begin

end STACK;

TOP:=0;

Una excepción se declara de manera similar a una variable y es generada explícitamente por una instrucción raise junto con el nombre de la excepción. Las reglas de manejo y propagación son las mismas que las de las excepciones predefinidas. Ahora podemos escribir

declare begin

use STACK;

. . .
PUSH(M);
. . .
N:=POP;
. . .


exception
when ERROR =>
— uso incorrecto de la pila.
when others   =>
— alguna otra cosa errónea.


 

end;

(Nótese que si no se hubiese colocado la cláusula use habría sido necesario referirse a la excepción como STACK.ERROR.)
Ya sabemos como declarar una excepción y como originarla ¿Pero qué deberíamos colocar dentro del manejador de la excepción? Además de reportar que ha habido un error en el uso de la pila, deberíamos dejar la pila en un estado aceptable, sin embargo hasta ahora no hemos provisto la forma de hacer esto. Sería útil agregar un procedimiento RESET al paquete STACK. Otra cosa que sería necesario hacer es devolver todos los recursos que habían sido tomados en el bloque, para así evitar que sean retenidos inadvertidamente. Supongamos, por ejemplo, que también habíamos estado utilizando el paquete PRINTER_MANAGER que contiene entre otros procedimientos ASSIGN_PRINTER (que permiten asignar una impresora al proceso que llama al procedimiento) y RETURN_PRINTER (que devuelve la impresora al sistema).
Además, sería conveniente limpiar la pila y devolver la impresora en caso de ocurrir cualquier otra excepción. Para realizar esto lo mejor sería declarar un procedimiento CLEAN_UP. Entonces, el bloque quedaría

declare

use STACK, PRINTER_MANAGER; MY_PRINTER: PRINTER;

procedure CLEAN_UP is begin

end;

RESET;
RETURN_PRINTER(MY_PRINTER);



begin

ASSIGN_PRINTER(MY_PRINTER);
. . .
PUSH(M);
. . .
N:=POP;
. . .
RETURN_PRINTER(MY_PRINTER);

exception
when ERROR  =>  PUT(“STACK used incorrectly”); CLEAN_UP;
when others      =>  PUT(“Something else went wrong”);
CLEAN_UP;
end;

(Se ha asumido que en el paquete STACK se ha declarado un procedimiento RESET.)

A veces las acciones que se deben tomar ante una excepción se deben realizar por niveles. En el ejemplo anterior se devuelve la impresora y se limpia la pila, pero probablemente se requiera que el bloque completo sea desechado. Podríamos indicar esto generando una excepción como la acción final del manejador.

excepcion
when ERROR =>  PUT(“Stack used incorrectly”); CLEAN_UP;
raise ANOTHER_ERROR;
when others =>
. . .
end;

De este modo la excepción ANOTHER_ERROR se propagará a la unidad que contenga el bloque.

A veces es conveniente manejar una excepción y luego propagar la misma excepción. Esto puede hacerse escribiendo simplemente

raise;

Esto es particularmente útil cuando se manejan varias excepciones con un solo manejador debido a que no es posible nombrar explícitamente la excepción que ha ocurrido. Entonces, podríamos tener
. . .
when others =>  PUT (“Something else went wrong”);
CLEAN_UP;


 


 

end;


raise;


El ejemplo de la pila muestra un uso legítimo de excepciones. La excepción ERROR debería ocurrir muy rara vez y sería, por ende, poco conveniente chequear esta condición cada vez que se pudiera producir. Para hacer esto seguramente tendríamos que agregar un parámetro adicional (en modo out) de tipo BOOLEAN al procedimiento PUSH para indicar que algo no funcionó correctamente y luego chequear el valor de esta parámetro después de cada llamada. En el caso de POP el cambio sería mayor puesto que ya no podría ser una función debido al uso de un parámetro de modo out. Entonces, la especificación del paquete quedaría

package STACK is
procedure PUSH(X: in INTEGER; B: out BOOLEAN);
procedure POP(X: out INTEGER; B: out BOOLEAN);
end;

y para usarlo deberíamos escribir

declare

begin

end;

use STACK; OK: BOOLEAN;

. . .
PUSH(M,OK);
if not OK then  . . .      end if;
. . .
POP(N, OK);
if not OK then  . . .   end if;
. . .

Es claro que en este caso el uso de excepciones contribuye a una mejor estructuración del programa.
Finalmente, nótese que nada impide generar excepciones predefinidas. Por ejemplo en la sección 7.1 cuando se analizaba la función INNER (pág. 40) se dijo que probablemente la mejor manera de controlar que los límites de los dos parámetros fuesen los mismos era generando explícitamente la excepción CONSTRAIN_ERROR.

Ambito de validez de las excepciones

En gran parte las excepciones siguen las mismas reglas de validez que las otras entidades (variables, tipos, subprogramas). Una excepción puede ocultar o ser ocultada por otra declaración; puede hacerse visible mediante la notación punto, etc. Sin embargo, son diferentes en otros aspectos. No podemos declarar arreglos de excepciones, y no pueden ser componentes de registros, parámetros de subprogramas, etc. De hecho, son sólo etiquetas, identificadores de una situación.
Una característica importante de las excepciones es que no son creadas dinámicamente a medida que se ejecuta el programa, sino que se les debe conceptualizar como existentes a través de toda la vida del programa. Esto se relaciona con la forma en que las excepciones se propagan dinámicamente por la cadena de ejecución, y no estáticamente por la cadena determinada por las reglas de ámbito de validez. Una excepción puede ser propagada fuera de su ámbito de validez, pero sólo puede ser manejada anónimamente por others. Veamos el ejemplo siguiente

declare

procedure P is
X: exception;

begin

begin end P;
P;


raise X;


exception
when others =>
— Aquí se maneja la excepción X
end;

El procedimiento P declara y genera la excepción X, pero no la maneja. Cuando llamamos a P, la excepción X es propagada al bloque que llama a P donde es manejada anónimamente.

 

Incluso es posible propagar una excepción  fuera de su rango de validez, donde se vuelve anónima, y luego volver a donde puede ser manejada explícitamente por su nombre. Consideremos

declare

package P is
procedure F;
procedure H;

end P; procedure G is begin
P.H;
exception
when others=>
raise;
end G,
package body P is
X: exception; procedure  F is begin
G;
exception
when X =>
PUT(“Got it”);
end F; procedure H is begin

 

begin end;

end P; P.F;

end H;

raise X,

En el bloque se declara un paquete P que contiene los procedimientos F y H; y también un procedimiento G. El bloque llama a F (que está en P), el que llama a G (que está fuera de P), el que a su vez llama a H (que está en P). El procedimiento H genera la excepción X cuyo ámbito de validez es el cuerpo de
P. El procedimiento H no maneja X , entonces esta se propaga a G (que llamó a H). El procedimiento G está fuera del paquete P, es decir, la excepción X está fuera de su ámbito de validez; a pesar de ello G maneja la excepción anónimamente y la propaga. G es llamado por F, por lo tanto, X es propagada de vuelta al paquete y puede ser manejada explícitamente  por F.
Otro característica de las excepciones la muestra el siguiente ejemplo de una función recursiva. A diferencia de las variables, no se obtiene una nueva excepción por cada llamada recursiva. Cada activación recursiva hace referencia a la misma excepción.

procedure F(N:INTEGER) is
X: exception;

begin

if N=0 then
raise X;

else

end if; exception


F(N-1);

end F;

when X =>
PUT(“Got it”);
raise; when others =>
null;

Supongamos que ejecutamos F(4); se tendrán las llamadas recursivas F(3), F(2), F(1) y F(0). Cuando F es llamado con parámetro cero genera la excepción X, la maneja: escribe un mensaje de confirmación y la propaga. La activación recursiva que llama (en este caso F(1)) recibe la excepción y nuevamente la maneja, y así para todas las llamadas. Por lo tanto, el mensaje se escribe 5 veces y finalmente la excepción se propaga anónimamente. Obsérvese que si cada activación recursiva hubiese creado su propia (diferente) excepción el mensaje se hubiese escrito sólo una vez.
En todos los ejemplos que hemos visto las excepciones se generan en instrucciones. Sin embargo, también pueden generarse en declaraciones. Por ejemplo, la declaración

N:POSITIVE:=0;

generaría CONSTRAIN_ERROR porque el valor inicial de N no satisface la restricción 1 .. INTEGER´LAST del subtipo POSITIVE. Una excepción generada en una declaración no es manejada por un manejador (si existe) de la unidad que la contiene, sino que se propaga inmediatamente a nivel superior. Esto significa que en cualquier manejador estamos seguros que todas las declaraciones de la unidad fueron elaboradas satisfactoriamente y, por lo tanto, no hay riesgo de utilizar algo que no existe.

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *