Excepciones

Concepto de excepción.

    Las excepciones son el mecanismo recomendado en la plataforma .NET para propagar los que se produzcan durante la ejecución de las aplicaciones (divisiones por cero, lectura de archivos no disponibles, etc.) Básicamente, son objetos derivados de la clase System.Exception que se generan cuando en tiempo de ejecución se produce algún error y que contienen información sobre el mismo. Esto es una diferencia respecto a su implementación en el C++ tradicional que les proporciona una cierta homogeneidad,  consistencia y sencillez, pues en éste podían ser valores de cualquier tipo. 

    Tradicionalmente, el sistema que en otros lenguajes y plataformas se ha venido usando para informar estos errores consistía simplemente en hacer que los métodos en cuya ejecución pudiesen producirse devolvieran códigos que informasen sobre si se han ejecutado correctamente o, en caso contrario, sobre cuál fue el error producido. Sin embargo, las excepciones proporcionan las siguientes ventajas frente a dicho sistema:

  • Claridad: El uso de códigos especiales para informar de error suele dificultar la legibilidad del fuente en tanto que se mezclan las instrucciones propias de la lógica del mismo con las instrucciones propias del tratamiento de los errores que pudiesen producirse durante su ejecución. Por ejemplo:


int resultado = obj.Método();
if (resultado == 0)  // Sin errores al ejecutar obj.Método();
{...}
else if (resultado == 1)  // Tratamiento de error de código 1
{...}
else if (resultado == 2) // Tratamiento de error de código 2
...

Como se verá, utilizando excepciones es posible escribir el código como si nunca se fuesen a  producir errores y dejar en una zona aparte todo el código de tratamiento de errores, lo que contribuye a facilitar la legibilidad de los fuentes.

  • Más información: A partir del valor de un código de error puede ser difícil deducir las causas del mismo y conseguirlo muchas veces implica tenerse que consultar la documentación que proporcionada sobre el método que lo provocó, que puede incluso que no especifique claramente su causa.

Por el contrario, una excepción es un objeto que cuenta con campos que describen las causas del error y a cuyo tipo suele dársele un nombre que resuma claramente su causa. Por ejemplo, para informar errores de división por cero se  suele utilizar una excepción predefinida de tipo DivideByZeroException en cuyo campo Message se detallan las  causas del error producido

  • Tratamiento asegurado: Cuando se utilizan códigos de error nada obliga a tratarlos en cada llamada al método que los pueda producir, e ignorarlos puede provocar más adelante en el código comportamientos inesperados de causas difíciles de descubrir.

    Cuando se usan excepciones siempre se asegura que el programador trate toda excepción que pueda producirse o que, si no lo hace, se aborte la ejecución de la aplicación mostrándose un mensaje indicando dónde se ha producido el error.

    Ahora bien, tradicionalmente en lenguajes como C++ el uso de excepciones siempre ha tenido las desventajas respecto al uso de códigos de error de complicar el compilador y dar lugar a códigos más lentos y difíciles de optimizar en los que tras cada instrucción que pudiese producir excepciones el compilador debe introducir las comprobaciones necesarias para detectarlas y tratarlas así como para comprobar que los objetos creados sean correctamente destruidos si se producen.

    Sin embargo, en la plataforma .NET desaparecen los problemas de complicar el compilador y dificultar las optimizaciones ya que es el CLR quien se encarga de detectar y tratar las excepciones y es su recolector de basura quien se encarga asegurar la correcta destrucción de los objetos. Obviamente el código seguirá siendo algo más lento, pero es un pequeño sacrificio que merece la pena hacer en tanto que ello asegura que nunca se producirán problemas difíciles de detectar derivados de errores ignorados.

La clase System.Exception

    Como ya se ha dicho, todas las excepciones derivan de un tipo predefinido en la BCL llamado System.Exception. Los principales miembros que heredan de éste son: 

  • string Message {virtual get;}: Contiene un mensaje descriptivo de las causas de la excepción. Por defecto este mensaje es una cadena vacía (“”)

  • Exception InnerException {virtual get;}: Si una excepción fue causada como consecuencia de otra, esta propiedad contiene el objeto System.Exception que representa a la excepción que la causó. Así se pueden formar cadenas de excepciones de cualquier longitud. Si se desea obtener la última excepción de la cadena es mejor usar el método virtual Exception GetBaseException()

  • string StackTrace {virtual get;}: Contiene la pila de llamadas a métodos que se tenía en el momento en que se produjo la excepción. Esta pila es una cadena con información sobre cuál es el método en que se produjo la excepción, cuál es el método que llamó a este, cuál es el que llamó a ese otro, etc.

  • string Source {virtual get; virtual set;}: Almacena información sobre cuál fue la aplicación u objeto que causó la excepción.

  • MethodBase TargetSite {virtual get;}: Almacena cuál fue el método donde se produjo la excepción en forma de objeto System.Reflection.MethodBase. Puede consultar la documentación del SDK si desea cómo obtener información sobre las características del método a través del objeto MethodBase.

  • string HelpLink {virtual get;}: Contiene una cadena con información sobre cuál es la URI donde se puede encontrar información sobre la excepción. El valor de esta cadena puede establecerse con virtual Exception SetHelpLink (string URI), que devuelve la excepción sobre la que se aplica pero con la URI ya actualizada.

    Para crear objetos de clase System.Exception se puede usar los constructores:

            Exception()

            Exception(string msg)

            Exception(string msg, Exception causante)

    El primer constructor crea una excepción cuyo valor para Message será “” y no causada por ninguna otra excepción (InnerException valdrá null) El segundo la crea con el valor indicado para Message, y el último la crea con además la excepción causante indicada.

    En la práctica, cuando se crean nuevos tipos derivados de System.Exception no se suele redefinir sus miembros ni añadirles nuevos, sino que sólo se hace la derivación para distinguir una excepción de otra por el nombre del tipo al que pertenecen. Ahora bien, es conveniente respetar el convenio de darles un nombre acabado en Exception y redefinir los tres constructores antes comentados.

Excepciones predefinidas comunes

    En el espacio de nombres System de la BCL hay predefinidas múltiples excepciones derivadas de System.Exception que se corresponden con los errores más comunes que pueden surgir durante la ejecución de una aplicación. En la Tabla se recogen algunas:

Tipo de la excepción

Causa de que se produzca la excepción

ArgumentException

Pasado argumento no válido (base de excepciones de argumentos)

ArgumentNullException

Pasado argumento nulo

ArgumentOutOfRangeException

Pasado argumento fuera de rango

ArrayTypeMistmatchException

Asignación a tabla de elemento que no es de su tipo

COMException

Excepción de objeto COM

DivideByZeroException

División por cero

IndexOutOfRangeException

Índice de acceso a elemento de tabla fuera del rango válido (menor que cero o mayor que el tamaño de la tabla)

InvalidCastException

Conversión explícita entre tipos no válida

InvalidOperationException

Operación inválida en estado actual del objeto

InteropException

Base de excepciones producidas en comunicación con código inseguro

NullReferenceException

Acceso a miembro de objeto que vale null

OverflowException

Desbordamiento dentro de contexto donde se ha de comprobar los desbordamientos (expresión constante, instrucción checked, operación checked u opción del compilador /checked)

OutOfMemoryException

Falta de memoria para crear un objeto con new

SEHException

Excepción SHE del API Win32

StackOverflowException

Desbordamiento de la pila, generalmente debido a un excesivo número de llamadas recurrentes.

TypeInizializationException

Ha ocurrido alguna excepción al inicializar los campos estáticos o el constructor estático de un tipo. En InnerException se indica cuál es.

Excepciones predefinidas de uso frecuente 

    Obviamente, es conveniente que si las aplicaciones que escribamos necesiten lanzar excepciones relativas a errores de los tipos especificados en la Tabla 8 08D0C9EA79F9BACE118C8200AA004BA90B02000000080000000E0000005F005200650066003400380036003600350034003900330030000000FF , lancen precisamente las excepciones indicadas en esa tabla y no cualquier otra –ya sea definida por nosotros mismos o predefinida en la BCL con otro significado.

Lanzamiento de excepciones. Instrucción throw

    Para informar de un error no basta con crear un objeto del tipo de excepción apropiado, sino que también hay pasárselo al mecanismo de propagación de excepciones del CLR. A esto se le llama lanzar la excepción, y para hacerlo se usa la siguiente instrucción:


throw <objetoExcepciónALanzar>;

    Por ejemplo, para lanzar una excepción de tipo DivideByZeroException se podría hacer:


throw new DivideByZeroException();

    Si el objeto a lanzar vale null, entonces se producirá una NullReferenceException que será lanzada en vez de la excepción indicada en la instrucción throw.

Captura de excepciones. Instrucción try

    Una vez lanzada una excepción es posible escribir código que es encargue de tratarla. Por defecto, si este código no se escribe la excepción provoca que la aplicación aborte mostrando un mensaje de error en el que se describe la excepción producida (información de su propiedad Message) y dónde se ha producido (información de su propiedad StackTrace) Así, dado el siguiente código fuente de ejemplo:


 using System;
 
 class PruebaExcepciones
 {
  static void Main()
  {
   A obj1 = new A();
   obj1.F();
  }
 }
 
 class A
 {
  public void F()
  {
   G();
  }
  
  static public void G()
  {
   int c = 0;
   int d = 2/c;
  }
 }

    Al compilarlo no se detectará ningún error ya que al compilador no le merece la pena calcular el valor de c en tanto que es una variable, por lo que no detectará que dividir 2/c no es válido. Sin embargo, al ejecutarlo se intentará dividir por cero en esa instrucción y ello provocará que aborte la aplicación mostrando el siguiente mensaje:


 Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero  
 at  PruebaExcepciones.Main()

    Como se ve, en este mensaje se indica que no se ha tratado una excepción de división por cero (tipo DivideByZeroException) dentro del código del método Main() del tipo PruebaExcepciones. Si al compilar el fuente hubiésemos utilizado la opción /debug, el compilador habría creado un fichero .pdb con información extra sobre las instrucciones del ejecutable generado que permitiría que al ejecutarlo se mostrase un mensaje mucho más detallado con información sobre la instrucción exacta que provocó la excepción, la  cadena de llamadas a métodos que llevaron a su ejecución y el número de línea que cada una ocupa en el fuente:

 Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.

   at A.G() in E:\c#\Ej\ej.cs:line 22

   at A.F() in E:\c#\Ej\ej.cs:line 16

   at PruebaExcepciones.Main() in E:\c#\Ej\ej.cs:line 8

    Si se desea tratar la excepción hay que encerrar la división dentro de una instrucción try con la siguiente sintaxis:

 
 try
  <instrucciones>
 catch (<excepción1>)
  <tratamiento1>
 catch (<excepción2>)
  <tratamiento2>
 ...
 finally
  <instruccionesFinally>

    El significado de try es el siguiente: si durante la ejecución de las <instrucciones> se lanza una excepción de tipo <excepción1> (o alguna subclase suya) se ejecutan las instrucciones <tratamiento1>, si fuese de tipo <excepción2> se ejecutaría <tratamiento2>, y así hasta que se encuentre una cláusula catch que pueda tratar la excepción producida. Si no se encontrase ninguna y la instrucción try estuviese anidada dentro de otra, se miraría en los catch de su try padre y se repetiría el proceso. Si al final se recorren todos los try padres y no se encuentra ningún catch compatible, entonces se buscaría en el código desde el que se llamó al método que produjo la excepción. Si así se termina llegando al método que inició el hilo donde se produjo la excepción y tampoco allí se encuentra un tratamiento apropiado se aborta dicho hilo; y si ese hilo es el principal (el que contiene el punto de entrada) se aborta el programa y se muestra el mensaje de error con información sobre la excepción lanzada ya visto.

    Así, para tratar la excepción del ejemplo anterior de modo que una división por cero provoque que a d se le asigne el valor 0, se podría rescribir G() de esta otra forma:


 static public void G()
 {
  try
  {
   int c = 0;
   int d = 2/c;
  } 
  catch (DivideByZeroException)
  { d=0; }
 }

    Para simplificar tanto el compilador como el código generado y favorecer la legibilidad del fuente, en los catchs se busca siempre orden de aparición textual, por lo que para evitar catchs absurdos no se permite definir catchs que puedan capturar excepciones capturables por catchs posteriores a ellos en su misma instrucción try.

    También hay que señalar que cuando en <instrucciones> se lance una excepción que sea tratada por un catch de algún try -ya sea de la que contiene las <instrucciones>, de algún try padre suyo o de alguno de los métodos que provocaron la llamada al que produjo la excepción- se seguirá ejecutando a partir de las instrucciones siguientes a ese try.

    El bloque finally es opcional, y si se incluye ha de hacerlo tras todas los bloques catch. Las <instruccionesFinally> de este bloque se ejecutarán tanto si se producen excepciones en <instrucciones> como si no. En el segundo caso sus instrucciones se ejecutarán tras las <instrucciones>, mientras que en el primero lo harán después de tratar la excepción pero antes de seguirse ejecutando por la instrucción siguiente al try que la trató. Si en un try no se encuentra un catch compatible, antes de pasar a buscar en su try padre o en su método llamante padre se ejecutarán las <instruccionesFinally>.

    Sólo si dentro de un bloque finally se lanzase una excepción se aborta la ejecución del mismo. Dicha excepción sería propagada al try padre o al método llamante padre del try que contuviese el finally.

    Aunque los bloques catch y finally son opcionales, toda instrucción try ha de incluir al menos un bloque catch o un bloque finally.

    El siguiente ejemplo resume cómo funciona la propagación de excepciones:


 using System;
 
 class MiException:Exception {}
 
 class Excepciones
 {
  public static void Main()
  {
   try
   {
    Console.WriteLine("En el try de Main()");
    Método();
    Console.WriteLine("Al final del try de Main()");
   }
   catch (MiException)
   {
    Console.WriteLine("En el catch de Main()");
   }
   finally
   {
    Console.WriteLine("finally de Main()");
   }                                                                                
  }
  
  public static void Método()
  {
   try
   {
    Console.WriteLine("En el try de Método()");
    Método2();
    Console.WriteLine("Al final del try de Método()");
   }
   catch (OverflowException)
   {
    Console.WriteLine("En el catch de Método()");
   }
   finally
   {
    Console.WriteLine("finally de Método()");
   }                                                                                
  }
  
  public static void Método2()
  {
   try
   {
    Console.WriteLine("En el try de Método2()");
    throw new MiException();
    Console.WriteLine("Al final del try de Método2()");
   }
   catch (DivideByZeroException)
   {
    Console.WriteLine("En el catch de Método2()");
   }
   finally
   {
    Console.WriteLine("finally de Método2()");
   }
  }
 }

    Nótese que en este código lo único que se hace es definir un tipo nuevo de excepción llamado MiException y llamarse en el Main() a un método llamado Método() que llama a otro de nombre Método2() que lanza una excepción de ese tipo. Viendo la salida de este código es fácil ver el recorrido seguido durante la propagación de la excepción:


 En try de Main()

 En try de Método()

 En try de Método2()

 finally de Método2

 finally de Método

 En catch de Main()

 finally de Main()

    Como se puede observar, hay muchos WriteLine() que nunca se ejecutan ya que en cuanto se lanza una excepción se sigue ejecutando tras la instrucción siguiente al try que la trató (aunque ejecutando antes los finally pendientes, como se deduce de la salida del ejemplo) De hecho, el compilador se dará cuenta que la instrucción siguiente al throw nunca se ejecutará e informará de ello con un mensaje de aviso.

    La idea tras este mecanismo de excepciones es evitar mezclar código normal con código de tratamiento de errores. En <instrucciones> se escribiría el código como si no pudiesen producirse errores, en las cláusulas catch se tratarían los posibles errores, y en el finally se incluiría el código a ejecutar tanto si producen errores como si no (suele usarse para liberar recursos ocupados, como ficheros o conexiones de red abiertas)

    En realidad, también es posible escribir cada cláusula catch definiendo una variable que se podrá usar dentro del código de tratamiento de la misma para hacer referencia a la excepción capturada. Esto se hace con la sintaxis:

 
 catch (<tipoExcepción> <nombreVariable>)
 {
  <tratamiento>
 }

    Nótese que en tanto que todas las excepciones derivan de System.Exception, para definir una cláusula catch que pueda capturar cualquier tipo de excepción basta usar:

 
 catch(System.Exception <nombreObjeto>)
 {
  <tratamiento>
 }

    En realidad la sintaxis anterior sólo permite capturar las excepciones propias de la  plataforma .NET, que derivan de System.Exception. Sin embargo, lenguajes como  C++ permiten lanzar excepciones no derivadas de dicha clase, y para esos casos se ha incluido en C# una variante de catch que sí que realmente puede capturar excepciones de cualquier tipo, tanto si derivan de System.Exception como si no. Su sintaxis es:


 catch
 {
  <tratamiento>
 }

    Como puede deducirse de su sintaxis, el problema que presenta esta última variante de catch es que no proporciona información sobre cuál es la excepción capturada, por lo que a veces puede resultar poco útil y si sólo se desea capturar cualquier excepción derivada de System.Exception es mejor usar la sintaxis previamente explicada.

    En cualquier caso, ambos tipos de cláusulas catch sólo pueden ser escritas como la última cláusula catch del try, ya que si no las cláusulas catch que le siguiesen nunca llegarían a ejecutarse debido a que las primeras capturarían antes cualquier excepción derivada de System.Exception.

    Respecto al uso de throw, hay que señalar que hay una forma extra de usarlo que sólo es válida dentro de códigos de tratamiento de excepciones (códigos <tratamientoi> de las cláusulas catch) Esta forma de uso consiste en seguir simplemente esta sintaxis:


throw;

    En este caso lo que se hace es relanzar la misma excepción que se capturó en el bloque catch dentro de cuyo de código de tratamiento se usa el throw; Hay que precisar que la excepción relanzada es precisamente la capturada, y aunque en el bloque catch se la modifique a través de la variable que la representa, la versión relanzada será la versión original de la misma y no la modificada.

    Además, cuando se relance una excepción en un try con cláusula finally, antes de pasar a reprocesar la excepción en el try padre del que la relanzó se ejecutará dicha cláusula.

Instrucción throw

    La instrucción throw ya se ha visto que se usa para lanzar excepciones de este modo:


throw
<objetoExcepciónALanzar>;

    En caso de que no se indique ningún <objetoExcepciónALanzar> se relanzará el que se estuviese tratando en ese momento, aunque esto sólo es posible si el throw se ha escrito dentro del código de tratamiento asociado a alguna cláusula catch.

    Como esta instrucción ya ha sido explicada a fondo en este mismo tema, para más información sobre ella puede consultarse el epígrafe Excepciones del mismo.

Excepciones
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:13/11/2006
Última actualizacion:13/11/2006
Visitas totales:44914
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com