Uso de excepciones

En C#, los errores del programa en tiempo de ejecución se propagan a través del programa mediante un mecanismo denominado excepciones. Las excepciones las inicia el código que encuentra un error y las detecta el código que puede corregir dicho error. El entorno de ejecución .NET o el código de un programa pueden producir excepciones. Una vez iniciada, una excepción se propaga hasta la pila de llamadas hasta que encuentra una instrucción catch para la excepción. Las excepciones no detectadas se controlan mediante un controlador de excepciones que ofrece el sistema y muestra un cuadro de diálogo.

Las excepciones están representadas por clases derivadas de Exception. Esta clase identifica el tipo de excepción y contiene propiedades que tienen los detalles sobre la excepción. Iniciar una excepción implica crear una instancia de una clase derivada de excepción, configurar opcionalmente las propiedades de la excepción y luego producir el objeto con la palabra clave throw. Por ejemplo:

class CustomException : Exception
{
    public CustomException(string message)
    {
    }
}
private static void TestThrow()
{
    throw new CustomException("Custom exception in TestThrow()");
}

Cuando se inicia una excepción, el entorno runtime comprueba la instrucción actual para ver si se encuentra dentro de un bloque try. Si es así, se comprueban los bloques catch asociados al bloque try para ver si pueden detectar la excepción. Los bloques Catch suelen especificar tipos de excepción; si el tipo del bloque catch es el mismo de la excepción, o una clase base de la excepción, el bloque catch puede controlar el método. Por ejemplo:

try
{
    TestThrow();
}
catch (CustomException ex)
{
    System.Console.WriteLine(ex.ToString());
}

Si la instrucción que inicia una excepción no está en un bloque try, o si el bloque try que la encierra no tiene un elemento catch coincidente, el entorno de ejecución busca una instrucción try y bloques catch en el método de llamada. El entorno runtime sigue hasta la pila de llamadas para buscar un bloque catch compatible. Después de encontrar el bloque catch y ejecutarlo, el control pasa a la siguiente instrucción después de dicho bloque catch.

Una instrucción try puede contener más de un bloque catch. Se ejecuta la primera instrucción catch que pueda controlar la excepción; las instrucciones catch siguientes se omiten, aunque sean compatibles. Ordene los bloques catch de más específicos (o más derivados) a menos específicos. Por ejemplo:

using System;
using System.IO;

namespace Exceptions
{
    public class CatchOrder
    {
        public static void Main()
        {
            try
            {
                using (var sw = new StreamWriter("./test.txt"))
                {
                    sw.WriteLine("Hello");
                }
            }
            // Put the more specific exceptions first.
            catch (DirectoryNotFoundException ex)
            {
                Console.WriteLine(ex);
            }
            catch (FileNotFoundException ex)
            {
                Console.WriteLine(ex);
            }
            // Put the least specific exception last.
            catch (IOException ex)
            {
                Console.WriteLine(ex);
            }
            Console.WriteLine("Done");
        }
    }
}

Para que el bloque catch se ejecute, el entorno runtime busca bloques finally. Los bloques Finally permiten al programador limpiar cualquier estado ambiguo que pudiera haber quedado tras la anulación de un bloque try o liberar los recursos externos (como identificadores de gráficos, conexiones de base de datos o flujos de archivos) sin tener que esperar a que el recolector de elementos no utilizados en el entorno de ejecución finalice los objetos. Por ejemplo:

static void TestFinally()
{
    FileStream? file = null;
    //Change the path to something that works on your machine.
    FileInfo fileInfo = new System.IO.FileInfo("./file.txt");

    try
    {
        file = fileInfo.OpenWrite();
        file.WriteByte(0xF);
    }
    finally
    {
        // Closing the file allows you to reopen it immediately - otherwise IOException is thrown.
        file?.Close();
    }

    try
    {
        file = fileInfo.OpenWrite();
        Console.WriteLine("OpenWrite() succeeded");
    }
    catch (IOException)
    {
        Console.WriteLine("OpenWrite() failed");
    }
}

Si WriteByte() ha iniciado una excepción, el código del segundo bloque try que intente reabrir el archivo generaría un error si no se llama a file.Close(), y el archivo permanecería bloqueado. Como los bloques finally se ejecutan aunque se inicie una excepción, el bloque finally del ejemplo anterior permite que el archivo se cierre correctamente y ayuda a evitar un error.

Si no se encuentra ningún bloque catch compatible en la pila de llamadas después de iniciar una excepción, sucede una de estas tres acciones:

  • Si la excepción se encuentra en un finalizador, este se anula y, si procede, se llama al finalizador base.
  • Si la pila de llamadas contiene un constructor estático o un inicializador de campo estático, se inicia una excepción TypeInitializationException, y la excepción original se asigna a la propiedad InnerException de la nueva excepción.
  • Si se llega al comienzo del subproceso, este finaliza.