EXCEPCIONES EN DELPHI 3


Este Articulo esta escrito para todo aquel que no tenga ni idea del uso Excepciones dentro de Delphi, o sus conocimientos sean minimos, introduciendo al lector en esta filosofía de programación, cuya complejidad es mínima, pero sus resultados muy grandes.

Para los programadores que venimos de otros lenguajes, como Clipper, dBase o de mas Alto Nivel nos preocupa el control de los posibles errores que se produzcan en nuestro programa, por ejemplo en Clipper el famoso ERRORSYS.PRG controla todo error desviando el programa a su código, dando la posibilidad al programador de corregir la situación que produjo el error y devolver la ejecución al programa en el punto donde se produjo el error.

En Delphi esta filosofía cambia introduciendo el concepto de las Excepciones, una excepción definida de una forma poco formal seria "un mecanismo que salta cuando se produce un error", en una programación sin excepciones el código debe verificar cada situación de programa, verificando que cada parámetro, variable..., contiene los valores validos para ejecutar un algoritmo, ahora, con el uso de las excepciones no nos debe preocupar las validaciones y escribimos el código como si todo fuera a ir bien, si se produce un error las excepciones atrapan el error, impidiendo que se ejecuten líneas de código con valores no validos.

ATRAPANDO ERRORES

Cuando queremos proteger unas líneas de código en las que puede haber error utilizamos un bloque de control try....except...end de esta forma:

try
//Líneas a proteger
...
...
Except
//Procesado del error
...
End;
Por ejemplo, si queremos cargar un fichero de texto en una TstringList, en programación sin excepciones verificaríamos la existencia de ese fichero y si esta, lo cargaríamos, con excepciones no lo verificamos, lo haríamos así:
try
MiStringList.LoadFromFile(‘Fichero_de_marras’);
...
...
except
Showmessage(‘No se puede abrir el fichero...’);
end;
Si el fichero no existe se produce la excepción y la ejecución del programa salta a la línea siguiente a except.

Delphi en si, empieza la ejecución del programa en un bloque try....except...end; que procesa todas las excepciones que se produzcan en nuestro programa, y que no atrapemos nosotros, en el ejemplo anterior si no encerramos el LoadFromFile dentro de un bloque protegido, al saltar la excepción Delphi la procesaría en su manejador por defecto y el flujo de programa continuaría en la instrucción que llamo a la función/Método anterior donde se produjo el error.

En ocasiones desearemos que una función acabe si se produce un error ya que los datos posiblemente no sean validos, volviendo al ejemplo anterior en nuestra función que necesitan los datos cargados desde el fichero de disco, si después de mostrar el mensaje del bloque protegido deseamos que la función termine podemos REASIGNAR la excepción, o dicho de otra forma volver a ejecutarla, esto se consigue con la palabra clave y solo se puede usar dentro de except....end; ya que lo que hace es volver a ejecutar la excepción que produjo el error, así nuestro código lo modificaríamos de esta manera:

try
MiStringList.LoadFromFile(‘Fichero_de_marras’);
...
...
except
Showmessage(‘No se puede abrir el fichero...’);
raise; //volvemos a ejecutar la excepción
end;
// Código que necesita los valores cargados de disco
.....
.....
De esta forma nos aseguramos que no se volverán a producir mas excepciones por tener la lista vacía.

De esta forma, al escribir código protegido nos olvidamos de los posibles errores que se pueden producir y nos centramos en el código, sabiendo que si algo va mal se cortara el flujo del programa de una forma ordenada.

El avispado lector se habrá dado cuenta que eso de atrapar errores esta muy bien, pero que poco podemos hacer para evitarlos o procesarlos ya que una vez que se produce una excepción el programa no vuelve ni puede volver a la línea donde se produjo ese error, ni podemos variar valores de variables para impedir el error, si bien esto es así, por lo menos nuestro programa nunca terminara de una forma poco amable. ¿Que hacer para cambiar una condición de error?, en primer lugar cambiar nuestra filosofía de programación y empezar a usar excepciones, con la practica veremos toda su potencia y lo bien que van, ahorrando mucho código de comprobación, deberemos escribir funciones/métodos como si todo va a funcionar bien, pero protegiendo el código dentro de bloques try...except, si algo falla deberemos, en lo posible paliar sus efecto. Supongamos que al hacer unas operaciones para grabar un dato en un registro de una Tablas esperamos unos valores validos, pero el pesado del usuario normalmente se olvida rellenar algún campo:

procedure onClickMiBoton( Sender: Tobject);
begin
try
Table1.Edit;
//Operaciones...
....
....
Table1.Post;
except
ShowMessage(‘Los datos introducidos no son correctos’);
Table1.Cancel
end;
end;
En un formulario en el que al pulsar un botón se debe editar un registro y hacer diversas operaciones con los datos introducidos por el usuario en una programación tradicional deberíamos verificar cada una de las operaciones y de que sus parámetros son correctos, con excepciones nos olvidamos de todo y pensamos que todo esta bien, si como suele ocurrir, esto no es así, nuestro simple código de después del Except se encarga de paliar el mal, presenta un mensaje y evita que se graben datos erróneos.

ANIDACION DE EXCEPCIONES

Dentro de un bloque try...except podemos escribir otro bloque try..except, ya que siempre que se produce una excepción después de ejecutar el código salta al bloque superior de esta forma podemos ir controlando diversos posibles errores y reconducir las condiciones de error segur nuestras necesidades.

Supongamos que en un fichero de texto guardamos unos valores que grabamos y restauramos para usarlos en un determinado proceso, en el caso de que no exista el fichero deberemos de tomar unos valores por defecto e inicializar el fichero:

....
....
try
try
MiStringList.LoadFromFile(‘DATOS.TXT’);
.....
except
MiStringList.Add( ‘Dato por defecto’);
.....
MiStringList.SavetoFile(‘DATOS.TXT’);
end;
//Operación siempre con datos validos
....
....
except
ShowMessage(‘No puedo abrir/crear fichero de datos’);
end;
....
Con este código nos aseguramos de dos cosas, que los datos de MiStringList siempre son validos, y en el caso de que no se pueda crear el fichero se recogerá el error, nombre no valido, disco lleno...

TIPOS DE EXCEPCIONES

Delphi tiene predefinidas diversas clases de excepciones, que lanza dependiendo del tipo de error, dentro de nuestros bloques except....end, podremos escribir código que diferencie entre diversos tipos de excepciones y tomar decisiones al respecto, en nuestro aburrido ejemplo nos pudría interesar diferenciar entre un error I/O y de otro tipo, nuestro bloque except quedaría:

....
except
on e:EIOexcept do ShowMessage(‘Error en Disco: ‘+ e.Message)
else
ShowMessage(‘Error indeterminado...’);
end;
De esta forma al averiguar el tipo de excepción podremos afinar mas a la hora de escribir código que procese el error.

EXCEPCIONES PROPIAS

No siempre deberemos manejar las excepciones propias de Delphi, nuestros programas pueden crear nuevos tipos y crearlas según nos interés, en determinados momentos nos puede interesar crear un tipo especial de excepción para usarlo no ya para lanzar errores de programa, si no errores lógicos de nuestro programa, lanzando una excepción que interrumpa el proceso en marcha con un mensaje personalizado. Podemos crear clases nuevas o utilizar la clase Exception genérica de Delphi.

Para crear una clase propia:

EmiExcepcion = class(Exception);
Simplemente cambiando el nombre o añadiendo métodos y variables nuevas.

Para interrumpir un proceso según una condición propia lo haremos de la forma habitual dentro de un try..except o sin el para que el manejador de Delphi se encargué de procesarlo:

try
....
....
If MiTablaCodigoProveedor.Value = ‘’ then EmiExcepcion.Create(‘Mensaje....’);
except
on e:EmiException do ShowMessage( e.Message )
else Application.ShowExcecption( e);
end;
Ya que este tipo de errores no son errores de programación si no errores de la lógica del programa, la ejecución del programa debe seguir, pero informar al usuario de que algo lo ha hecho mal, es aconsejable usar la funcionalidad de las excepciones antes que enormes hileras de if’s.., nuestro programa puede comprobar lo que necesite y en cualquier momento lanzar la excepción impidiendo que se ejecute código con valores no validos.

Obsérvese que ponemos una cláusula , ya que si se produce un error no de nuestra lógica, seguirá siendo procesado pero dejando que sea Delphi.

Usando con inteligencia bloques anidados de try..except y lanzando excepciones propias nuestros programas estarán garantizados de que los datos que procesan son validos y con un esfuerzo mínimo de codificación.

Repitiendo la filosofía de las excepciones, Codificamos como si todo va a ir bien, si hay error excepción y el código siguiente no se ejecuta.

Otro buen ejemplo de anidar excepciones seria llamar a funciones o métodos dentro de bloques locales try..except si en la ejecución de la función se produce una excepción protegeremos a el resto de código, de igual forma en nuestras funciones de utilidades podremos lanzar excepciones propias mediante raise Exception.Create(..), que serán recogidas en el bloque try..except anterior.

Todo esto implica que si nos dicidimos ha usar excepciones a pleno rendimiento todo nuestro código debe ir orientado a su procesamiento, creando excepciones y atrapándolas por todos nuestros fuentes.

Application.OnException
La classe TApplication de delphi nos brinda la posibilidad de redefinir el manejador de excepciones por defecto de un programa, esta facilidad es muy interesante si queremos personalizar el manejo de errores y controlarlos al máximo, para ejemplarizar esta metodología se incluye un componente para facilitarnos la tarea.
{ ****************************************************************
* Ejemplo para Delphi--, de un componente para redirigir las *
* excepciones de nuestros programas *
****************************************************************
}

unitCErrorSys;
interface

uses SysUtils, WinTypes, Classes, Dialogs, Forms,DsgnIntf, Graphics, db, dbtables;

type

EMiGenericError = class(Exception);

TShowError = class(TComponent)
Protected
procedure AppException(Sender: TObject; E: Exception);
procedure Show(E: exception);
Public
constructor Create(AOwner: TComponent); Override;
end;

implementation

constructor TShowError.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
//Redirigimos el manejador princpal de errores
Application.OnException := AppException;
end;

procedure TShowError.Show(E: exception);
begin
ShowMessage( E.Message + ‘ / ‘ + e.ClassName );
end;

procedure TShowError.AppException(Sender: TObject; E: Exception);
begin
if (E is EDBEngineError) then // Errores de Bases de Datos
begin
with EDBEngineError(E) do //Errores Cazados (no errores fatales)
case Errors[0].ErrorCode of // Segun codigo de Error
10241: //Error de bloqueos
begin
ShowMessage(‘Registro ya bloqueado por: ‘+ Errors[2].Message);
Exit;
end;
else
ShowMessage( e.Message );
end;
end
else if (E is EDatabaseError) then // Error de base de datos
ShowMessage(E.message) // Show mensaje
else
if (E is TMiGenericError) then // Aqui controlamos errores Genericos
ShowMessage(E.message) // Show mensaje
else // Errores fatales con
Show(E); // Show mensaje
end;

end;
end.

Coger fuente Completo ErrorSys.pas

En el fuente del componente se puede observar que definimos una clase EgenericError de uso general que desviamos a un ShowMessage normal, para que en nuestro programas podamos crear en cualquier momento este tipo de excepción y el manejador de errores reinstalado se encargara de mostrar el mensaje sin mas.

En este sencillo componente solo controlamos el error de registro bloqueado por otro usuario, pero nos da pie para poder controlar otros muchos, yo por ejemplo tengo un DO CASE con mas de 200 líneas.

También el método Show puede reescribirse y mostrar un cuadro de dialogo mas personalizado, grabar un histórico de errores y todo lo que se nos pueda ocurrir, partiendo de este esqueleto puede salir un manejador de errores a nuestro gusto.

Excepciones Silenciosas

Cuando queremos que una excepción no tenga efecto visual, que no se muestre ningún mensaje ( como en el ejemplo en el que añadíamos datos a una TstringList por defecto), simplemente tendremos que hacer eso, no escribir código que muestre ningún mensaje o impedir que el manejador de Delphi capture la excepción.

Otra practica común de excepciones silenciosas es para pasar la excepción a un nivel mas superior y que el cuadro con el mensaje solo salga una vez, así si tenemos instalado un manejador propio de errores en una estructura anidada:

try
.....
.....
try
.....
.....
except
raise; //pasamos la excepción a un nivel superior
end;
except
on e:exception do Application.ShowException(e);
end;
Usando esta técnica, un método puede capturar una excepción local, y pasarla aun nivel superior pasando inadvertida para el programa, también podemos definir código dependiente de condiciones especiales, que puede ser evitado lanzando una excepción silenciosa, para este caso utilizaríamos la palabra reservada que es la madre de las excepciones silenciosas.

Protegiendo recursos

Otra metodología de protección, cuando se producen excepciones, es el uso de otro tipo de bloque, try .... finally ...end; este bloque lo que hace es asegurar que el código después de siempre se ejecutara, aunque se produzcan excepciones. Su uso principal es de proteger la asignación y liberación de recursos y memoria.

Imaginemos que un proceso critico puede ser lanzada una excepción ya sea de Delphi o propia, pero que en el transcurso de la ejecución hemos creado objetos o asignado memoria, si bien queremos que dicha excepción se ejecute e impida la ejecución de todo el código, también deberemos de liberar o destruir los objetos creados, con este tipo de bloques de código garantizamos que esto ocurra, así

try
MiStringList := TstringList.Create;
try
....
....
finally
MiStringList.Free; //siempre se ejecuta
end;
except
//.....
end;
La liberación de memoria siempre se producirá, ya que aunque se produzca un error que lance una excepción el bloque esta garantizado que se ejecutara. Un error común es crear el objeto dentro del bloque try..finally, y delphi muy educadamente nos da un Warning, lógicamente la creación debe estar fuera ya que si la propia creación no tiene éxito y esto se produce dentro del bloque try..., al ejecutarse el código finally se producirá otra nueva excepción, liando mas aun el tema.

Otro error común es precisamente el no usar los bloques ya que si bien el programa no finaliza al producirse excepciones, nuestro programa va acaparando memoria sin liberarla con los resultados que podemos imaginar, la experiencia me dice que mantener muchos punteros a memoria no útil en un programa Delphi, tarde o temprano producen una excepción de protección general, terminando la ejecución del programa.

Otras consideraciones a tener en cuenta al asignar memoria o crear objetos, es verificar la existencia de tales, por ejemplo una clase que cree varios objetos en su constructor deberá destruirlos en su destructor, ¿pero que pasa si antes de que se ejecute todo el código del constructor se produce una excepción?, Delphi llama al destructor ya que la propia condición de error activa el mecanismo interno, que garantiza que si en un constructor se da una condición de error, ese objeto no se crea, pero nuestro código puede intentar destruir objetos que aun no están creados, con el consiguiente anidamiento de errores tras errores y el famoso error de protección general. Forma de evitarlo, verificar la existencia de un objeto antes de destruirlo, en el código siguiente:

constructor Foo.Create;
begin
inherited Create;
try
FmiClase := TmiClase.Create;
FmiOtraClase := TmiOtraClase.Create;
except
Abort;
end;
end;

destructor Foo.Destroy;
begin
if FmiClase nil then FmiClase.Free;
if FmiOtraClase nil then FmiOtraClase.Free;
inherited Destroy;
end;

aquí tenemos un destructor ideal, nunca dará un error ya que si el propio constructor falla, por ejemplo TmiOtraClase no logra crearse, pero si TmiClase al ejecutarse el destructor liberara la memoria de FmiClase pero no dará error en FmiOtraClase ya que nunca se ejecutara su destructor.

En Otro Orden de cosas y en programación en general es aconsejable verificar la existencia de objetos antes de destruirlos y aquí no vale hacer bloques try...except ya que pudríamos caer en circulo de excepciones con final fatal.

Los programadores nuevos me dicen "me sale un error de protección general y no se quemas....", normalmente todos los errores de protección general son debidos a que se intenta acceder a una variable o método de un objeto nulo o no inicializado, cuando usamos variables que apuntan a objetos creados en otros módulos (punteros) deberemos de codificar con sumo cuidado y proteger nuestro código mediante try.., ya que es posible que un objeto ya no exista (destruido por otro proceso) y esto siempre ocurrirá en casa del cliente, nuca en la oficina....

Otra consideración sobre el acceso a objetos, si programamos usando Threads, una tarea nunca debe acceder directamente a Objetos VCL creados en otra tarea directamente, si bien, puede que no nos de errores algunas veces, estaremos creando un código inestable, para ello usaremos el método de los objetos Tthread (mirar la ayuda para mas info).


Este trabajo se ha realizado como un aporte a toda la comunidad por: valentin