Uso de objetos que implementan IDisposable

El recolector de elementos no utilizados (GC) de Common Language Runtime reclama la memoria usada por los objetos administrados. Normalmente, los tipos que usan recursos no administrados implementan la interfaz IDisposable o IAsyncDisposable para permitir que se recuperen los recursos no administrados. Cuando termine de usar un objeto que implemente IDisposable, debe llamar a la implementación Dispose o DisposeAsync del objeto para realizar explícitamente la limpieza. Puede hacerlo de una de las maneras siguientes:

  • Mediante la instrucción o declaración using de C# (Using en Visual Basic).
  • Mediante la implementación de un bloque try/finally y una llamada al método Dispose o DisposeAsync en finally.

Importante

El GC no elimina los objetos, ya que no tiene conocimiento de IDisposable.Dispose() ni de IAsyncDisposable.DisposeAsync(). El GC solo sabe si un objeto es finalizable (es decir, si define un método Object.Finalize()) y cuándo se debe llamar al finalizador del objeto. Para más información, consulte Cómo funciona la finalización. Para más información sobre la implementación de Dispose y de DisposeAsync, consulte:

Los objetos que implementan System.IDisposable o System.IAsyncDisposable siempre deben eliminarse correctamente, independientemente del ámbito de las variables, a menos que se indique lo contrario explícitamente. Los tipos que definen un finalizador para liberar recursos no administrados suelen llamar a GC.SuppressFinalize desde su implementación Dispose o DisposeAsync. La llamada a SuppressFinalize indica al GC que el finalizador ya se ha ejecutado y que el objeto no debe promoverse para la finalización.

La instrucción using

La instrucción using de C# y la instrucción Using de Visual Basic simplifican el código que se debe escribir para limpiar un objeto. La instrucción using obtiene uno o más recursos, ejecuta las instrucciones especificadas y desecha el objeto automáticamente. Pero la instrucción using solo resulta útil para los objetos que se usan dentro del ámbito del método en el que se construyen.

En el ejemplo siguiente se utiliza la instrucción using para crear y liberar un objeto System.IO.StreamReader.

using System.IO;

class UsingStatement
{
    static void Main()
    {
        var buffer = new char[50];
        using (StreamReader streamReader = new("file1.txt"))
        {
            int charsRead = 0;
            while (streamReader.Peek() != -1)
            {
                charsRead = streamReader.Read(buffer, 0, buffer.Length);
                //
                // Process characters read.
                //
            }
        }
    }
}
Imports System.IO

Module UsingStatement
    Public Sub Main()
        Dim buffer(49) As Char
        Using streamReader As New StreamReader("File1.txt")
            Dim charsRead As Integer
            Do While streamReader.Peek() <> -1
                charsRead = streamReader.Read(buffer, 0, buffer.Length)
                ' 
                ' Process characters read.
                '
            Loop
        End Using
    End Sub
End Module

Una usingdeclaración es una sintaxis alternativa disponible donde se quitan las llaves, y cuyo ámbito es implícito.

using System.IO;

class UsingDeclaration
{
    static void Main()
    {
        var buffer = new char[50];
        using StreamReader streamReader = new("file1.txt");

        int charsRead = 0;
        while (streamReader.Peek() != -1)
        {
            charsRead = streamReader.Read(buffer, 0, buffer.Length);
            //
            // Process characters read.
            //
        }
    }
}

Aunque la clase StreamReader implementa la interfaz IDisposable, que indica que usa un recurso no administrado, en el ejemplo no se llama al método StreamReader.Dispose de manera explícita. Cuando el compilador de C# o de Visual Basic encuentra la instrucción using, emite un lenguaje intermedio (IL), que equivale al código siguiente que contiene un bloque try/finally de manera explícita.

using System.IO;

class TryFinallyGenerated
{
    static void Main()
    {
        var buffer = new char[50];
        StreamReader? streamReader = null;
        try
        {
            streamReader = new StreamReader("file1.txt");
            int charsRead = 0;
            while (streamReader.Peek() != -1)
            {
                charsRead = streamReader.Read(buffer, 0, buffer.Length);
                //
                // Process characters read.
                //
            }
        }
        finally
        {
            // If non-null, call the object's Dispose method.
            streamReader?.Dispose();
        }
    }
}
Imports System.IO

Module TryFinallyGenerated
    Public Sub Main()
        Dim buffer(49) As Char
        Dim streamReader As New StreamReader("File1.txt")
        Try
            Dim charsRead As Integer
            Do While streamReader.Peek() <> -1
                charsRead = streamReader.Read(buffer, 0, buffer.Length)
                ' 
                ' Process characters read.
                '
            Loop
        Finally
            If streamReader IsNot Nothing Then DirectCast(streamReader, IDisposable).Dispose()
        End Try
    End Sub
End Module

La instrucción using de C# también permite adquirir varios recursos en una sola instrucción, lo que internamente equivale a instrucciones using anidadas. En el ejemplo siguiente se crean instancias de dos objetos StreamReader para leer el contenido de dos archivos.

using System.IO;

class SingleStatementMultiple
{
    static void Main()
    {
        var buffer1 = new char[50];
        var buffer2 = new char[50];

        using StreamReader version1 = new("file1.txt"),
                           version2 = new("file2.txt");

        int charsRead1, charsRead2 = 0;
        while (version1.Peek() != -1 && version2.Peek() != -1)
        {
            charsRead1 = version1.Read(buffer1, 0, buffer1.Length);
            charsRead2 = version2.Read(buffer2, 0, buffer2.Length);
            //
            // Process characters read.
            //
        }
    }
}

Bloque try/finally

En lugar de ajustar un bloque try/finally en una instrucción using, puede elegir implementar el bloque try/finally directamente. Puede adoptar este estilo como su método de creación de código personal, o bien puede hacer esto por una de las razones siguientes:

  • Para incluir un bloque catch para controlar las excepciones iniciadas en el bloque try. De lo contrario, las excepciones iniciadas en la instrucción using no se controlan.
  • Para crear instancias de un objeto que implementa IDisposable cuyo ámbito no es local en el bloque en el que se declara.

El siguiente ejemplo es similar al anterior, a excepción de que se usa un bloque try/catch/finally para crear instancias, usar y eliminar un objeto StreamReader, y para controlar las excepciones que produce el constructor StreamReader y su método ReadToEnd. El código del bloque finally comprueba que el objeto que implementa IDisposable no es null antes de llamar al método Dispose. Si no se realiza este paso, se puede producir una excepción NullReferenceException en tiempo de ejecución.

using System;
using System.Globalization;
using System.IO;

class TryExplicitCatchFinally
{
    static void Main()
    {
        StreamReader? streamReader = null;
        try
        {
            streamReader = new StreamReader("file1.txt");
            string contents = streamReader.ReadToEnd();
            var info = new StringInfo(contents);
            Console.WriteLine($"The file has {info.LengthInTextElements} text elements.");
        }
        catch (FileNotFoundException)
        {
            Console.WriteLine("The file cannot be found.");
        }
        catch (IOException)
        {
            Console.WriteLine("An I/O error has occurred.");
        }
        catch (OutOfMemoryException)
        {
            Console.WriteLine("There is insufficient memory to read the file.");
        }
        finally
        {
            streamReader?.Dispose();
        }
    }
}
Imports System.Globalization
Imports System.IO

Module TryExplicitCatchFinally
    Sub Main()
        Dim streamReader As StreamReader = Nothing
        Try
            streamReader = New StreamReader("file1.txt")
            Dim contents As String = streamReader.ReadToEnd()
            Dim info As StringInfo = New StringInfo(contents)
            Console.WriteLine($"The file has {info.LengthInTextElements} text elements.")
        Catch e As FileNotFoundException
            Console.WriteLine("The file cannot be found.")
        Catch e As IOException
            Console.WriteLine("An I/O error has occurred.")
        Catch e As OutOfMemoryException
            Console.WriteLine("There is insufficient memory to read the file.")
        Finally
            If streamReader IsNot Nothing Then streamReader.Dispose()
        End Try
    End Sub
End Module

Puede seguir este patrón básico si elige implementar o debe implementar un bloque try/finally, ya que el lenguaje de programación no admite una instrucción using pero sí llamadas directas al método Dispose.

Miembros de instancia de IDisposable

Si una clase posee un campo o una propiedad de instancia y su tipo implementa IDisposable, la clase también debe implementar IDisposable. Para más información, consulte cómo implementar una eliminación en cascada.

Vea también