jueves, 30 de octubre de 2014

Parte 1ª - Excepciones, tratamiento de errores en Lazarus

Bien, ha pasado ya tiempo desde que no toco algún tema por aquí. He mantenido todo mi tiempo y atención en otros asuntos. Así que continuaré detallando, de vez en cuando, un poquito lo que todos hemos visto alguna vez que debemos conservar. Así que intentaré crear una pequeña recopilación de las ideas recogidas de otros blog's sobre programación.

Para ser sincero y que me perdone su autor, parte de la información que estoy publicando en esta publicación pertenece a un autor desconocido para mi. En su día fue un manual que recopile en formato pdf y que no apunté su procedencia. Así que si alguien reconoce a su autor, agradecería me lo hiciese llegar para detallar aquí a su autor original. Otra parte de su contenido la fuente ha sido substraida de otro blog bastante interesante, Delphi al Límite, muy nombrado en www.clubdelphi.com

Hoy trataré el tema de las excepciones en Pascal. ¿Porqué?, pues porque en toda aplicación alguna vez se producirá un error y entre tanto código ya desarrollado, nos puede costar tiempo y dolores de cabeza protegernos de los errores. Por lo que quiero dejar claro que es indispensable contar con algún método para protegerse de ellos.

Utilizaremos un programa muy simple que calcula el cuadrado de un número entero. A continuación vemos el form principal y el único método del programa, asignado al evento OnClick del botón:


procedure TForm1.Button1Click(Sender: TObject);
var
  i:integer;
begin
  i:= StrToInt(edit1.text);
  label1.caption:= format('El cuadrado es: %d',[i*i]);
end;

   Cuando pulsamos el botón, se convierte el texto (introducido en el TEdit) en un número entero, lo eleva al cuadrado y se muestra el resultado en el TLabel. Pero si escribimos algo que no sea un número entero (incluso dejando vacío el TEdit),se producirá un error de conversión.

   Pascal (en este caso Lazarus / freePascal) procesa este error mostrando un pequeño mensaje:


   Como vemos, no pasa nada grave. La respuesta por defecto de a las excepciones es sólo mostrar el mensaje correspondiente en una caja de diálogo como la anterior.

   Pero y si nos interesara controlar las excepciones, ¿cómo lo conseguiríamos?


Cambiar la respuesta por defecto a las excepciones

   Para modificar la respuesta estándar ante un error debemos escribir un procedimiento de respuesta al evento OnException, que se produce en el objeto Application

   El objeto Application no es un componente visible; se crea automáticamente cada vez que se ejecuta una aplicación y se referencia con la variable global Application. Para indicar que queremos que se ejecute un método al producirse el evento OnException, debemos entender que los eventos son propiedades a las que se les puede asignar un valor (en el caso de los eventos, un procedimiento que se ejecutará al producirse el evento). La definición de la propiedad OnException nos indica qué tipo de método podemos asignarle. En el caso de OnException:

property OnException: TExceptionEvent;

El tipo TExceptionEvent es un tipo procedural:

TExceptionEvent = procedure (Sender: TObject; E: Exception) of object;

Por lo tanto, el procedimiento debe esperar dos parámetros, uno de tipo tObject y otro de tipo
Exception, y debe ser un método de un objeto (el método puede pertenecer a cualquiera de los forms que componen el proyecto). Escribamos entonces un método nuevo al form principal:

procedure tForm1.TratarExcepciones(sender:tObject; e:Exception);
begin
  MessageDlg('Se ha producido un error. Por favor intente de nuevo',
    mtError,[mbOk],0);
end;

   La declaración de este método debe ser escrita en la definición del tipo Tform1. A continuación detallamos la clase entera:

TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    procedure Button1Click(Sender: TObject);
  private
    procedure TratarExcepciones(sender:tObject; e:Exception);
  public
    { Public declarations }
  end;

   Haremos la asignación al crear el form principal, en el evento OnCreate:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnException:= TratarExcepciones;
end;

Bien, ahora el programa presenta un mensaje más amable cuando se produce un error de cualquier tipo. El paso siguiente es tratar los errores, respondiendo de manera diferente a cada uno de ellos. Podemos ver de qué tipo es una excepción mirando la clase a que pertenece el parámetro E.

Debemos primero repasar la jerarquía de clases de excepciones definida en Pascal.


Clases de excepciones

Las excepciones se pueden dividir en las siguientes categorías:
  • Conversión de tipo. Se producen cuando tratamos de convertir un tipo de dato en otro, por ejemplo utilizando las funciones IntToStr, StrToInt, StrToFloat. Se dispara una excepción EConvertError.
  • Tipo forzado (typecast). Se producen cuando tratamos de forzar el reconocimiento de una expresión de un tipo como si fuera de otro usando el operador as. Si no son compatibles, se dispara la excepción EInvalidCast.
  • Aritmética de punto flotante. Se producen al hacer operaciones con expresiones de tipo real. Existe una clase general para este tipo de excepciones EmathError, pero Lazarus utiliza sólo los descendientes de ésta:
    • EinvalidOp: el procesador encontró una instrucción inválida.
    • EzeroDivide: división por cero.
    • Eoverflow: se excede en más la capacidad aritmética (números demasiado grandes).
    • Eunderflow: se excede en menos la capacidad aritmética (números demasiados
    • queños).
  • Aritmética entera. Se producen al hacer operaciones con expresiones de tipo entero. Existe una clase general definida para este tipo de excepciones llamada EintError, pero Lazarus sólo utiliza los descendientes:
    • EDivByZero: división por cero.
    • ERangeError: número fuera del rango disponible según el tipo de dato. La comprobación de rango debe estar activada (indicador $R).
    • EIntOverflow: se excede en más la capacidad aritmética (números demasiado grandes). La comprobación de sobrepasamiento debe estar activada (indicador $O).
  • Falta de memoria. Se producen cuando hay un problema al acceder o reservar memoria. Se definen dos clases:
    • EOutOfMemory: no hay suficiente memoria disponible para completar la operación.
    • EInvalidPointer: la aplicación trata de disponer de la memoria referenciada por un puntero que indica una dirección inválida (fuera del rango de direcciones permitido a la aplicación). Generalmente significa que la memoria ya ha sido liberada.
  • Entrada/Salida. Se producen cuando hay un error al acceder a dispositivos de entrada/salida o archivos. Se define una clase genérica EInOutError con una propiedad que contiene el código de error ErrorCode.
  • Hardware. Se producen cuando el procesador detecta un error que no puede manejar o cuando la aplicación genera intencionalmente una interrupción para detener la ejecución. El código para manejar estas excepciones no se incluye en las DLL compiladas, sólo en las aplicaciones. Se define una clase base que no es directamente utilizada EProcessorException. Las clases útiles son los descendientes:
    • EFault: es una clase base para todas las excepciones de faltas del procesador.
    • EGPFault: error de Protección General, cuando un puntero trata de acceder posiciones de memoria protegidas.
    • EStackFault: acceso ilegal al segmento de pila del procesador.
    • EPageFault: el manejador de memoria de Windows tuvo problemas al utilizar el archivo de intercambio.
    • EInvalidOpcode: el procesador trata de ejecutar una instrucción inválida.
    • EBreakpoint: la aplicación ha generado una interrupción de la ejecución (punto de ruptura, utilizado por el debugger de Lazarus para inspeccionar las variables en un punto).
    • ESingleStep: la aplicación ha generado una interrupción de ejecución paso a paso. Luego de cada paso de programa se produce la interrupción. Es utilizada también por el debugger.
  • Excepciones silenciosas. Se disparan intencionalmente por la aplicación para interrumpir el flujo de ejecución. No generan mensajes de error. Se define una clase EAbort. Es digno de mencionar que esta excepción es automáticamente generada cuando invocamos el procedimiento global Abort, que lo podemos usar para interrumpir la ejecución del programa en cualquier punto. Por ejemplo, para dar un toque profesional a un programa hay ocasiones en que nos interesa controlar la excepción pero que no se entere el usuario del programa. Lo que no se puede hacer es abandonar la excepción con Break o con Exit ya que puede ser peor el remedio que la enfermedad. Habría que como hemos visto anteriormente usar Abort:
    try
      {sentencias}
    except
      Abort;
    end;

Como podemos ver en la imagen siguiente, la jerarquía de clases descendientes de Exception es bastante amplia, sin embargo muchos de los descendientes son de uso interno de los componentes y no los trabajamos directamente. 


Otras excepciones no tratadas anteriormente, pero que las he encontrado en otro blog (Delphi al límite) son las siguientes:

EAccessViolation: Comprueba errores de acceso a memoria inválidos.
EBitsError: Previene intentos para acceder a arrays de elementos booleanos.
EComponentError: Nos informa de un intento inválido de registar o renombar un componente.
EDatabaseError: Especifica un error de acceso a bases de datos.
EDBEditError: Error al introducir datos incompatibles con una máscara de texto.
EExternalException: Significa que no reconoce el tipo de excepción (viene de fuera).
EIntOutError: Representa un error de entrada/salida a archivos.
EInvalidGraphic: Indica un intento de trabajar con gráficos que tienen un formato desconocido.
EInvalidOperation: Ocurre cuando se ha intentado realizar una operación inválida sobre un componente.
EMenuError: Controla todos los errores relacionados con componentes de menú.
EOleCtrlError: Detecta problemas con controles ActiveX.
EOleError: Especifica errores de automatización de objetos OLE.
EPrinterError: Errores al imprimir.
EPropertyError: Ocurre cuando se intenta asignar un valor erroneo a una propiedad del componente.
ERegistryExcepcion: Controla los errores en el resigtro.

Bien, con todo lo que hasta ahora hemos podido ver podríamos tratar las excepciones que se produzcan y responder a cada clase de manera diferente si fuera necesario. Este tratamiento se utiliza para responder a distintas excepciones en el lugar donde se producen, de manera tal que podamos recuperarnos del error y poder proseguir el código. El ejemplo más común es el error de lectura de un archivo, normalmente permitiremos al usuario reintentar la operación además de cancelarla. Pero recordemos que el evento OnException se produce en el objeto Application, después de lo cual la ejecución queda a la espera de nuevos eventos. Debemos encontrar una forma de detectar y corregir el error sin abandonar el procedimiento en curso.

Esto se logra mediante la protección de bloques de código. 


Bloques de código controlados
Try ... Except ... End

Para proteger una porción de código debemos encerrarla en un bloque try...except. Entre estas dos palabras reservadas se ubica el código que está expuesto a errores; después de except se procesan estos últimos, cerrando todo el bloque con end. La sintaxis sería:

try
  {Bloque de código propenso a errores}
except
  on <clase de excepción> do
  {una sola instrucción o un bloque begin..end}

  on <otra excepción diferente> do
  begin
  end;
end;

Por ejemplo, en la aplicación que creamos al principio, cuando se producía un error la ejecución saltaba al procedimiento TratarExcepciones, y el texto del label 1 no se actualizaba. Podríamos cambiar el mensaje del label cuando se produce la excepción. ¿Cómo?, cambiando el código de respuesta al botón "Convertir" por el del listado siguiente:

procedure TForm1.Button1Click(Sender: TObject);
var
  i:integer;
begin
  try
    i:= StrToInt(edit1.text);
    label1.caption:= format('El cuadrado es: %d',[i*i]);
  except
    on eConvertError do
    Label1.Caption:= 'Número erróneo';
  end;
end;

Los bloques protegidos forman como capas al resto del programa, cuando se produce una excepción en un lugar del código la ejecución salta directamente a la capa de protección donde se está ejecutando.

Si el error no se procesa en esta capa se pasa a la siguiente, y así sucesivamente. En Lazarus las aplicaciones corren dentro de un bloque protegido, por lo que todas las excepciones no tratadas se procesan en el objeto Application.

En la imagen siguiente observamos un esquema de una aplicación con varios bloques protegidos y cómo se redirige la ejecución cuando se produce un error.


En el gráfico vemos que los errores que se producen fuera de las estructuras try..except son tratadas en la parte correspondiente al bloque anterior y que en última instancia se procesan en el evento OnException de la aplicación. Es como si toda la aplicación estuviera encerrada en un gran bloque try..except.

La técnica de declarar como clases a las excepciones permite el tratamiento jerárquico de las mismas. Esto quiere decir que un manejador de una clase de excepciones trata también las clases descendientes, con un código como el del listado:

Try
  {código}
except
  on <clase de la excepción> do
  {manejar la excepción o sus descendientes}

  on <otra clase> do
end;

Por ejemplo si queremos atrapar cualquier error aritmético que se pueda producir en una operación, podemos escribir:

try
  {operación}
except
  on eMathError do
    ShowMessage('Error de punto flotante');

  on eIntError do
    ShowMessage('Error aritmético entero');
end;

Podemos hacer algo más que mostrar un mensaje, por ejemplo asignar al resultado de la operación un valor por defecto.

Pero ahora imaginemos que queremos saber el tipo de excepción y sacarlo por pantalla, pues podríamos usar estas líneas de abajo:

var
  s: string;
  i: Integer;
begin
  s := 'prueba';

  try
    i := StrToInt( s );
  except
    on E: Exception  do
      Application.MessageBox( PChar( E.ClassName + ': ' + 
          E.Message ), 'Error', MB_ICONSTOP );
  end;
end;

Hemos incluido en el mensaje de la excepción la clase y el mensaje de error. Este sería el resultado:

EConvertError: 'prueba' not is a valid integer value

Así, mediante la propiedad ClassName de la clase Exception podemos averiguar la clase a la que pertenece la excepción. Ahora mediante la setencia on podemos aislar la excepción de la forma:

on tipo do sentencia

En nuestro caso sería así:

  try
    i := StrToInt( s );
  except
    on E: EConvertError  do
      Application.MessageBox( 'Error de conversión', 'Error',
                  MB_ICONSTOP )
    else
      Application.MessageBox( 'Error desconocido', 'Error', 
                  MB_ICONSTOP );
  end;

Si se produjera una excepción que no fuese de la clase EConvertError mostraría el mensaje Error desconocido.

De este modo podemos aislar dentro de un mismo bloque de código los distintos tipos de excepción que se puedan producir.

En próximas publicaciones, veremos una serie de situaciones comunes donde es necesario un control de errores y cómo se corrigen los mismos.

Hasta la próxima y gracias por vuestra atención.