Verwenden von Objekten, die IDisposable implementieren

Der Garbage Collector (GC) der Common Language Runtime beansprucht den von verwalteten Objekten verwendeten Arbeitsspeicher wieder. In der Regel implementieren Typen, die nicht verwaltete Ressourcen verwenden, die IDisposable- oder IAsyncDisposable-Schnittstelle, damit die nicht verwalteten Ressourcen wieder beansprucht werden können. Wenn Sie mit der Verwendung eines Objekts fertig sind, das IDisposable implementiert, rufen Sie die Dispose- oder DisposeAsync-Implementierung des Objekts auf, um die Bereinigung explizit durchzuführen. Dazu haben Sie zwei Möglichkeiten:

  • Mithilfe der C#-Anweisung oder Deklaration using (Using in Visual Basic).
  • Durch Implementieren eines try/finally-Blocks und Aufrufen der Dispose- oder DisposeAsync-Methode in finally.

Wichtig

Die GC entsorgt Ihre Objekte nicht, weil sie keine Kenntnis von IDisposable.Dispose() oder IAsyncDisposable.DisposeAsync() hat. Die GC weiß nur, ob ein Objekt finalisierbar ist (d. h., es definiert eine Object.Finalize()-Methode) und wann der Finalizer des Objekts aufgerufen werden muss. Weitere Informationen finden Sie unter Funktionsweise der Finalisierung. Weitere Details zur Implementierung von Dispose und DisposeAsync finden Sie unter:

Objekte, die System.IDisposable oder System.IAsyncDisposable implementieren, sollten immer unabhängig vom Variablenbereich ordnungsgemäß gelöscht werden, sofern nicht ausdrücklich anders angegeben. Typen, die einen Finalizer definieren, um nicht verwaltete Ressourcen freizugeben, rufen in der Regel GC.SuppressFinalize entweder von deren Dispose- oder DisposeAsync-Implementierung auf. Das Aufrufen von SuppressFinalize zeigt der GC an, dass der Finalizer bereits ausgeführt wurde und das Objekt nicht zur Finalisierung höhergestuft werden sollte.

Die using-Anweisung

Die using-Anweisung in C# und die Using-Anweisung in Visual Basic vereinfachen den Code, den Sie schreiben müssen, um ein Objekt zu bereinigen. Die using-Anweisung ruft eine oder mehrere Ressourcen ab, führt die von Ihnen angegebenen Anweisungen aus und verwirft dann das Objekt automatisch. Die using-Anweisung ist jedoch nur hilfreich bei Objekten, die im Bereich der Methode verwendet werden, in der sie erstellt werden.

Im folgenden Beispiel wird die using-Anweisung verwendet, um ein System.IO.StreamReader-Objekt zu erstellen und freizugeben.

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

Eine using-Deklaration ist eine alternative Syntax, die verfügbar ist, wenn die Klammern entfernt werden und die Bereichsdefinition implizit ist.

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.
            //
        }
    }
}

Obwohl die StreamReader-Klasse die IDisposable-Schnittstelle implementiert, die angibt, dass sie eine nicht verwaltete Ressource verwendet, wird in dem Beispiel nicht explizit die StreamReader.Dispose-Methode aufgerufen. Wenn der C#- oder Visual Basic-Compiler die using-Anweisung findet, gibt er Zwischensprache (Intermediate Language, IL) aus, die dem folgenden Code entspricht, der explizit einen try/finally-Block enthält.

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

Mit der using-Anweisung in C# können Sie auch mehrere Ressourcen in einer einzigen Anweisung abrufen. Intern entspricht dies geschachtelten using-Anweisungen. Im folgenden Beispiel werden zwei StreamReader-Objekte instanziiert, um den Inhalt von zwei verschiedenen Dateien zu lesen.

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.
            //
        }
    }
}

Try-/Finally-Block

Anstatt einen try/finally-Block in einer using-Anweisung zu umschließen, haben Sie die Möglichkeit, den try/finally-Block direkt zu implementieren. Dies kann Ihr persönlicher Codierungsstil sein, oder Sie möchten dies eventuell aus einem der folgenden Gründe durchführen:

  • Um einen catch-Block einzufügen, um im try-Block ausgelöste Ausnahmen zu behandeln. Andernfalls werden alle in der using-Anweisung ausgelösten Ausnahmen nicht behandelt.
  • Um ein Objekt zu instanziieren, das IDisposable implementiert, dessen Bereich für den Block, in dem es deklariert ist, nicht lokal ist.

Das folgende Beispiel ähnelt dem vorhergehenden, es wird jedoch ein try/catch/finally-Block verwendet, um ein StreamReader-Objekt zu instanziieren, zu verwenden und zu verwerfen, und um alle Ausnahmen zu behandeln, die von dem StreamReader-Konstruktor und dessen ReadToEnd-Methode ausgelöst werden. Der Code im finally-Block überprüft, ob das Objekt, das IDisposable implementiert, nicht null ist, bevor die Dispose-Methode aufgerufen wird. Wird dies nicht ausgeführt, kann es zu einer NullReferenceException-Ausnahme zur Laufzeit kommen.

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

Sie können dieses grundlegende Muster beibehalten, wenn Sie einen try/finally-Block implementieren möchten oder müssen, da Ihre Programmiersprache eine using-Anweisung nicht unterstützt, jedoch direkte Aufrufe der Dispose-Methode erlaubt.

IDisposable-Instanzmember

Wenn die Klasse ein Instanzfeld oder eine Instanzeigenschaft besitzt und dessen/deren Typ IDisposable implementiert, sollte die Klasse auch IDisposable implementieren. Weitere Informationen finden Sie unter Implementieren eines kaskadierenden Dispose (Löschung).

Siehe auch