viernes, 20 de junio de 2014

Excepciones en C# - Parte 1: Introducción a las Excepciones

Tabla de Contenido

0. Introducción
1. Introducción a las Excepciones
2. Excepciones en C#
2.1 Omisión del control de excepciones
2.2 Control de excepciones
3. Construcciones de Manejo de Excepciones
3.1 try-catch
3.1.1 Prueba de la CLR cuando se genera una excepción
3.1.2 Captura de excepciones con System.Exception
3.1.3 Omisión de tipo de excepción y variable
3.2 El bloque finally
4. Sentencia using
5. Excepciones en Métodos Asincrónicos
6. Conclusiones
7. Glosario
8. Literatura & Enlaces

0. Introducción

Con esta entrada doy inicio a la serie de artículos de Excepciones en C#. Sin lugar a dudas, este mecanismo de gestión o control de excepciones que provee el lenguaje de programación representa el medio para lograr la escritura de programas más robustos y tolerantes a fallas. El propósito de la serie es que usted aprenda los esenciales de la manipulación, creación, y control de excepciones (parte 1). A medida que avancemos (parte 2), también comprenderemos cómo usar excepciones con más precaución (efecto secundario sobre el rendimiento global de la aplicación), descubrimiento del patrón de métodos TryXXX, y las alternativas propuestas al uso de excepciones. Inclusive (parte 3), usted aprenderá a crear sus propias excepciones para casos muy específicos requeridos por el diseño del sistema. En la parte 4, exploraremos las propiedades interesantes de la clase System.Exception. Finalmente, en la parte 5, introduciré varios ejemplos de las excepciones más comunes en el Framework .NET.


Serie compuesta por los siguientes artículos:

Parte 1: Introducción a las Excepciones
Parte 2: Uso de Excepciones
Parte 3: Diseño de Excepciones Personalizadas
Parte 4: Propiedades de System.Exception
Parte 5: Ejemplo de Excepciones Comunes

1. Introducción a las Excepciones

Una excepción ocurre en tiempo de ejecución (runtime) cuando la aplicación, en una operación en particular, ha transcurrido a un estado anómalo o excepcional. Ese estado anómalo lo podemos ejemplificar con la operación matemática de intento de división por cero (que términos formales se categoriza como una operación indefinida [sin significado]):

Este es el ejemplo más clásico de una operación aritmética que genera un estado anómalo. Un ejemplo particular del error generado por el intento de dividir por cero lo encontramos en la calculadora de Bing [3]:
Intento de dividir 7 entre 0 en la calculadora de Bing.
Figura 1. Intento de dividir 7 entre 0 en la calculadora de Bing.

El mismo efecto en una calculadora de bolsillo TI-nspire CX [19]:
Intento de dividir 7 entre 0 en una calculadora TI-nspire CX.
Figura 2. Intento de dividir 7 entre 0 en una calculadora TI-nspire CX.
Estos programas y dispositivos vienen preparados para responder a esas situaciones excepcionales a través de mensajes que nos advierten sobre el intento de ejecutar una operación que a razón matemática no está definida.

Este mismo concepto lo podemos aplicar en la programación de un artefacto o elemento de programa (e.g., método, propiedad, clase, enumeración) para responder a intentos de ejecución de operaciones inválidas.

Más específicamente una operación inválida la definimos, en el contexto de C#, como:
  • División entre cero.
  • Acceso a un elemento de un arreglo (o colección) por debajo del límite inferior (0), o por encima del límite superior (capacidad).
  • Intento de escritura de un archivo inexistente en el sistema de archivos o en una unidad de red.
  • Conexión fallida a un servidor de base de datos.
  • Argumento de método no inicializado (null).
  • Superación del límite de memoria de trabajo.
  • Entre muchas más...
Como ha sido mencionado, el lenguaje de programación C# provee las construcciones sintácticas y semánticas para manejar o controlar las excepciones que se pudieran generar en una sección o bloque de código específicos. Se tratan de las siguientes palabras claves:
  • try
  • catch
  • finally
  • throw
Describiré cada una de estas en las siguientes secciones.

2. Excepciones en C#

El lenguaje de programación C# permite controlar la generación de una excepción a través de las construcciones try, catch, finally, y throw. La primera de estas -try-, encierra el bloque de código que pudiera generar una excepción, si ocurriera una excepción en el bloque try, el o los bloques catch se encargan de capturar la excepción para un tratamiento o manejo específico a la excepción generada. Con finally, se ejecutan operaciones una vez se finaliza el conjunto de sentencias encerradas en el bloque try, o después de haber sido la excepción en un bloque catch. (En las secciones posteriores nos detendremos en cada una de estas construcciones para mostrar varios detalles importantes a la manejar una excepción.)

2.1 Omisión del control de excepciones

La máquina virtual CLR se encarga administrar las excepciones generadas por el código propenso a generar situaciones inesperadas o anómalas: división entre cero, superación de los límites de un arreglo, apertura de un archivo inexistente, &c.


Empecemos por explorar un ejemplo en donde no controlamos la división entre cero y conocer los resultados:

Archivo de código fuente SinControlExcepciones.cs [enlace alternativo]:

En la línea 11 la expresión a / b es propensa a generar la excepción de intento de división entre cero. Y es lo que intencionalmente queremos que ocurra al pasar como valores de argumentos, en particular, como divisor 0 en la línea 19. Al Intentar dividir 7 / 0, la CLR genera el siguiente mensaje de excepción:


Unhandled Exception: System.DivideByZeroException: Division by zero
  at Articulos.Cap04.SinControlExcepciones.Main ()


Como resulta evidente, el mensaje coincide con la evaluación de una expresión que incluye una división entre cero. El mensaje de excepción también nos indica que la excepción no ha sido controlada o manejada (Unhandled Exception); además, de la locación en donde se ha generado: Articulos.Cap04.SincronExcepciones.Main. Finalmente, el programa finaliza su ejecución de forma abrupta, debido a que la excepción no ha sido manejada.

> Prueba de ejecución.

2.2 Control de excepciones

Construyamos el código fuente necesario para controlar la excepción a través de las construcciones mencionadas al principio de esta sección. Esto nos va a servir para destacar las principales diferencias con el ejemplo presentando en la sección 2.1, y empezar a concebir los esenciales del manejo de excepciones en C#.

Archivo de código fuente ControlDeExcepciones.cs [enlace alternativo]:

En el método Dividir validamos que el divisor sea igual a cero, de ser así, entonces lanzamos la excepción DividyByZeroException [4] a través de la sentencia:

throw new DivideByZeroException();

En caso que el divisor sea distinto de cero, entonces se realiza la operación de división de manera normal.


En la línea 25 invocamos al método Dividir con los valores 7 (dividendo), y 0 (divisor), para motivar la generación de la excepción. Cuando la excepción se genera, se interrumpe la ejecución de las sentencias del bloque try (líneas 23-27), y flujo de ejecución pasa al bloque catch. El tipo de la excepción de este bloque coincide con la clase DivideByZeroException. Es aquí donde se maneja la excepción, en el caso particular, se muestra un mensaje (línea 30) relacionado con el tipo de operación inválida:


Intento de división entre 0

> Prueba de ejecución.

3. Construcciones de Manejo de Excepciones

Esta es la estructura general del manejo de excepciones en C#:

try
{
... // sección de código propensa a generar excepciones
}
catch (ExceptionA ex)
{
// Manejo de la excepción de tipo ExceptionA
}
catch (ExceptionA ex)
{
// Manejo de la excepción de tipo ExceptionB
}
finally
{
// código de liberación de recursos
}

y continuación profundizaremos en detalles de cada elemento integral de este bloque de código.

3.1 try-catch

La sentencia try-catch [5] está compuesta por un bloque de sentencias proclives a generar situaciones inesperadas o excepcionales; este bloque (try) se encierra entre corchetes ({, y }), y va seguido por una o más clausulas catch. Cada una de las clausulas corresponde con un tipo de excepción. Debido a que las excepciones integradas en la biblioteca base de clases y las creadas por el propio programador derivan de la clase base System.Exception [6] , el orden (arriba-abajo) en que deben organizarse las excepciones ha de empezar por el tipo de excepción más específico (o con el orden jerarquía menor).


Continuando, el bloque try contiene la sentencia o el conjunto de sentencias proclives a generar una excepción. En este bloque pueden ocurrir dos situaciones:
  1. El bloque genera una excepción.
  2. Finaliza satisfactoriamente y pasa al bloque finally (si ha sido declarado) o la sentencia inmediata después del bloque try.
Asumamos el siguiente fragmento de código:

object o = null;

try
{
int i = (int) 0; // Error
}


El intento de la operación unboxing en la sentencia encerrada por el bloque try, generará la excepción NullReferenceException [7] debido al intento de realizar una operación sobre una referencia null. Esta excepción detendrá abruptamente la ejecución de la aplicación; para evitar esto, se debe atrapar la excepción en un bloque catch. Esta captura debe coincidir con un tipo específico o general de la jerarquía de herencia. Así:


object o = null;

try
{
int i = (int) 0; // Error
}
catch (InvalidCastException e)
{
Console.WriteLine ("Intento de conversión inválido.");
}


La clausula catch captura una excepción de tipo InvalidCastException [8]. Esta captura es tratada en el bloque subsiguiente; en donde podemos mostrar mensajes de error y/o crear un archivo de registro de los problemas generados en el flujo de ejecución de la aplicación.


Un bloque try, puede estar seguido por una o más clausulas y deben seguir el orden (arriba-abajo) de tipos de excepciones específicos a más generales. El compilador de C#, generará un error al declarar una clausula catch con un tipo general y más adelante uno específico. Con un ejemplo queda más claro:


Archivo de código fuente OrdenCatch.cs [enlace alternativo]:

En la línea 19 especificamos el tipo específico de excepción que generará la sentencia de la línea 13 (InvalidCastException). Sin embargo, en la línea 15 la clausula catch con el tipo de excepción System.Exception, capturará cualquier tipo de excepción que se pudiera generar en el bloque try, por lo tanto la clausula catch de la línea 19 es superflua, y genera el error CS0160 [9]:


error CS0160: A previous catch clause already catches all exceptions of this or a super type `System.Exception'

> Prueba de ejecución.

3.1.1 Prueba de la CLR cuando se genera una excepción

Cuando una excepción se genera y es lanzada desde cualquier lugar, la CLR lleva a cabo la siguiente validación:


¿El flujo de ejecución se haya dentro de una sentencia try es proclive a lanzar una excepción?
  • En caso de ser así, el tratamiento de la excepción será llevado por la clausula catch compatible con la excepción. Cuando la clausula catch finaliza satisfactoriamente, el flujo de ejecución transita a la siguiente sentencia después de la sentencia try-catch, o de haber un bloque finally se ha de ejecutar este primero.
  • En caso de no ser así, el flujo de ejecución pasa (esto sin antes ejecutar el bloque finally) a la sección que invocó la función (e.g., propiedad, método) y la prueba se repite de nuevo.

3.1.2 Captura de excepciones con System.Exception

Con el tipo de excepción System.Exception se captura cualquier tipo de excepción que produzca dentro de la sentencia try. En [1] indican que esto es útil en las siguientes situaciones:
  • Recuperación de la ejecución del programa independiente del tipo específico que se haya generado.
  • Volver a lanzar la excepción, lo anterior sin antes crear un registro.
  • Último recurso del manejador de errores antes de la terminación del programa.

3.1.3 Omisión de tipo de excepción y variable

Es posible prescindir de la variable dentro la clausula catch. Por ejemplo:

catch (DivideByZero)
{
// ...
}


Esto resulta útil cuando es necesario acceder a la información de la excepción (e.g., mensaje de error, ubicación exacta donde se generó la excepción). E inclusive se puede prescindir también del tipo de la excepción. Así:


catch ()
{
// ...
}


Este tipo de clausula catch, atrapará cualquier tipo de excepción generada en el bloque try subyacente.

3.2 El bloque finally

Esta sección opcional de la sentencia try-catch se ejecutará independiente de si se ha completado satisfactoriamente el bloque try (es decir, sin generar ninguna excepción), como no (es decir cuando alguno de los bloques catch ha atrapado (capturado) una excepción.


En general, el bloque finally se ejecutan en cualquier de las siguientes situaciones:
  • Al finalizar el bloque try.
  • Uso de las sentencias de salto (e.g., goto o return) localizadas en el bloque try.
  • Después de finalizar un bloque catch.
Además, como dicen en [1], las situaciones que pueden impedir el paso al bloque finally son un ciclo infinito, o la terminación abrupta (e.g., desbordamiento de memoria) del programa.


El bloque finally ayuda a agregar determinismo [10, 11] al programa, es decir, que las causas o razones producidas en la sentencia try-catch, luego sus efectos sean tratados en el bloque finally.


En el siguiente ejemplo, aseguramos el cierre del archivo al finalizar la ejecución del conjunto de sentencias del bloque try, o la generación de una excepción (i.e., IOException [12]), o si se alcanza el final del archivo o está vacío (EndOfStream):

public void LeerArchivo()
{
  StreamReader sr = null;

try
{
sr = File.OpenText ("archivo.txt");

if (sr.EndOfStream)
{
return;
}

Console.WriteLine (sr.ReadToEnd());
}
finally
{
if (reader != null )
{
if (reader != null )
}
}
}

4. Sentencia using

Aquellas clases que implementan la interfaz System.IDisposable [12] encapsulan recursos no administrador por la CLR, como: archivos, gráficos, o bases de datos. Estas clases implementan al método abstracto Dispose [14] para la liberación de los recursos (e.g., memoria) ocupados en su manejo. Para este tipo de clases, la sentencia using provee una sintaxis más cómoda y versatil que usar la sentencia try-catch-finally.

En lugar de

StreamReader sr = File.OpenText ("archivo.txt");

try
{
// ...
}
finally
{
if (sr != null)
{
((IDisposable)sr).Dispose();
}
}

podemos usar la sentencia using:

using (StreamReader sr = File.Open ("archivo.txt"));
{
// ...
};


Es evidente que este último modo es más cómodo, pues nos ahorra la liberación de recursos por parte nuestra.

5. Excepciones en Métodos Asincrónicos

De cuerdo con [5] un método marcado el modificador async [16] puede contener uno o más operadores await; en cuanto a una expresión que contenga este último operador no puede ser parte de un bloque catch o finally. En este ejemplo se demuestra cómo se puede utilizar otras propiedades de la clase Task en los casos en que un método asincrónico genere una excepción:

Archivo de código fuente MetodoAsincronicoExcepciones.cs [enlace alternativo]:

Con las propiedades IsCanceled y IsFaulted (líneas 22, y 23) se puede comprobar si el método asincrónico RetrasoAsincronico ha sido cancelado o si ha fallado, respectivamente.


> Resultado prueba de ejecución no. 1 (versión mostrada en el archivo MetodoAsincronicoExcepciones.cs):
Ejecución normal del método asincrónico
Figura 3. Ejecución normal del método asincrónico.


> Resultado prueba de ejecución no. 2 (remoción del comentario de la línea 40 y la agregación del comentario en la línea 43 sobre el archivo original):
Ejecución con excepción en el método asincrónico
Figura 4. Ejecución con excepción OperationCanceledException en el método asincrónico.


> Resultado prueba de ejecución no. 3 (remoción del comentario de la línea 41 y la agregación del comentario en la línea 43 sobre el archivo original):
Ejecución con excepción Excepcion en el método asincrónico
Figura 5. Ejecución con excepción Exception en el método asincrónico.

6. Resumen

Las excepciones cuentan con las siguientes características o propiedades [18]:
  • Todas las excepciones en el Framework .NET derivan de la clase System.Exception.
  • Use la sentencia try con la(s) sentencia(s) que sean proclives a generar excepciones.
  • Cuando ocurre una excepción dentro de un bloque try, esta es capturada por la primera sentencia catch que es capaz de manejar la excepción.
  • En caso de carecer de una sentencia capaz de manejar la excepción, el programa terminará abruptamente arrojando un mensaje de error.
  • Si en un bloque catch se define una variable del tipo de la excepción capturada (atrapada), el programador puede utilizar esa información para dar detalles de la excepción ocurrida.
  • Para generar una excepción de forma explícita, use la palabra clave throw.
  • El código que hace parte de un bloque finally se ejecuta independiente de si el bloque try se ha completado satisfactoriamente o no.
  • Las excepciones administradas en el Framework .NET se implementan bajo el mecanismo de manipulación de excepciones Win32.
Por otro lado, una nota del menoscabo del rendimiento con el uso de excepciones en [1]:

7. Conclusiones

Ahora conocemos los esenciales de excepciones en C#. El control o manejo de excepciones permite crear programas más tolerantes a fallas. Conocimos cómo declarar una excepción y utilizar los bloques o sentencias try, catch, finally, throw. Además, para las clases que implementen la interfaz IDisposable, el uso de la sentencia using simplifica la liberación de recursos en contraste con try-catch-finally. Al final exploramos cómo se compartan las excepciones en métodos asincrónicos, y varias propiedades de las excepciones.

8. Glosario

  • Excepción
  • Exception
  • CLR
  • Método asincrónico
  • Referencia
  • Runtime
  • Tiempo de ejecución
  • using

9. Literatura & Enlaces

[1]: C# 5.0 in a Nutshell by Joseph Albahari and Ben Albahari. Copyright 2012 Joseph Albahari and Ben Albahari, 978-1-449-32010-2.
[2]: Division by zero - Wikipedia, the free encyclopedia - http://en.wikipedia.org/wiki/Division_by_zero
[3]: Bing - http://www.bing.com
[4]: DivideByZeroException Class (System) - http://msdn.microsoft.com/en-us/library/system.dividebyzeroexception(v=vs.110).aspx
[5]: try-catch (C# Reference) - http://msdn.microsoft.com/en-us/library/0yd65esw.aspx
[6]: Exception Class (System) - http://msdn.microsoft.com/en-us/library/System.Exception(v=vs.110).aspx
[7]: NullReferenceException Class (System) - http://msdn.microsoft.com/en-us/library/system.nullreferenceexception(v=vs.110).aspx
[8]: InvalidCastException Class (System) - http://msdn.microsoft.com/en-us/library/system.invalidcastexception.aspx
[9]: Compiler Error CS0160 - http://msdn.microsoft.com/en-us/library/hxs37t55(v=vs.90).aspx
[10]: determinism - definition of determinism by the Free Online Dictionary, Thesaurus and Encyclopedia. - http://www.thefreedictionary.com/determinism
[11]: Determinismo - Wikipedia, la enciclopedia libre - http://es.wikipedia.org/wiki/Determinismo
[12]: IOException Class (System.IO) - http://msdn.microsoft.com/en-us/library/system.io.ioexception(v=vs.110).aspx
[13]: IDisposable Interface (System) - http://msdn.microsoft.com/en-us/library/system.idisposable(v=vs.110).aspx
[14]: IDisposable.Dispose Method (System) - http://msdn.microsoft.com/en-us/library/system.idisposable(v=vs.110).aspx
[15]: using Statement (C# Reference) - http://msdn.microsoft.com/en-us/library/yh598w02.aspx
[16]: async (C# Reference) - http://msdn.microsoft.com/en-us/library/hh156513.aspx
[17]: await (C# Reference) - http://msdn.microsoft.com/en-us/library/hh156528.aspx
[18]: Exceptions and Exception Handling (C# Programming Guide) - http://msdn.microsoft.com/en-us/library/vstudio/ms173160(v=vs.100).aspx
[19]: TI-Nspire™ CX CAS Handheld - Math. Science. Algebraic Precision. All on One Handheld. by Texas Instruments - US and Canada - http://education.ti.com/en/us/products/calculators/graphing-calculators/ti-nspire-cx-cas-handheld/tabs/overview


J

2 comentarios:

  1. Muy buena explicación, gracias por compartir conocimiento.

    ResponderEliminar
  2. Muy buena, ahora puedo controlar mejor las execpciones.

    ResponderEliminar

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.