Menu

C# using-Anweisung: IDisposable, Dispose und using var

Die using-Anweisung garantiert, dass Dispose für Dateien, Streams, Verbindungen und andere IDisposable-Objekte aufgerufen wird, auch wenn eine Exception geworfen wird. Lerne, wozu sie kompiliert wird, in welcher Reihenfolge Objekte freigegeben werden, die using-Deklaration aus C# 8 und wie du IDisposable in deiner eigenen Klasse implementierst.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Manche Objekte halten Ressourcen, die der Garbage Collector nicht verwaltet: offene Dateien, Netzwerk-Sockets, Datenbankverbindungen, Handles des Betriebssystems. Sie implementieren IDisposable, und du gibst die Ressource frei, indem du Dispose() aufrufst. Die using-Anweisung ruft es an einer festen Stelle für dich auf, auch wenn eine Exception geworfen wird.

Diese Seite behandelt die using-Anweisung. Die Zeilen using System; am Anfang einer Datei sind ein anderes Feature, die using-Direktive, die einen Namespace importiert.

Ein using-Block

using (declaration) { body } erzeugt das Objekt, führt den Rumpf aus und ruft dann Dispose auf. Eine kleine Klasse, die in Dispose etwas ausgibt, zeigt genau, wann das passiert:

Ausgabe:

open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled

Der zweite Block wirft, und „close api“ wird trotzdem ausgegeben, bevor das catch die Exception behandelt. Diese Garantie ist der Grund, using zu verwenden, statt Dispose() selbst in der letzten Zeile aufzurufen: Ein manueller Aufruf wird von jeder Exception übersprungen, die davor geworfen wird.

Wozu using kompiliert wird

Der Compiler macht aus einem using-Block ein try/finally. Diese beiden sind gleichwertig:

using (var writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("started");
}

// is compiled as:
{
    var writer = new StreamWriter("log.txt");
    try
    {
        writer.WriteLine("started");
    }
    finally
    {
        if (writer != null) ((IDisposable)writer).Dispose();
    }
}

Aus dieser Umsetzung folgen drei Details. Die Variable gilt nur im Block und ist darin schreibgeschützt (du kannst writer nicht neu zuweisen). Das Objekt muss IDisposable implementieren, sonst kompiliert der Code nicht. Und ein null-Wert ist erlaubt: Dispose wird dann einfach übersprungen, was praktisch ist, wenn eine Fabrik möglicherweise nichts zurückgibt.

Dateien und Streams

Datei- und Stream-Klassen sind der Alltagsfall. Ein StreamWriter puffert Text im Speicher, und Dispose schreibt den Puffer auf die Festplatte und schließt das Dateihandle:

Ausgabe:

id,total
1001,59.90
1002,12.50

Ohne Dispose liegen die letzten Zeilen des Writers vielleicht noch in seinem Puffer, und unter Windows hindert das offene Handle andere Programme (und dein eigenes nächstes File.Delete) daran, die Datei anzufassen. Hilfsmethoden wie File.WriteAllText und File.ReadAllLines öffnen und geben intern frei und brauchen daher kein using. Mehr dazu unter Dateien.

Mehrere Ressourcen: Reihenfolge der Freigabe

Stapele using-Anweisungen ohne geschweifte Klammern dazwischen, um mehrere Ressourcen in einem Block zu öffnen. Sie werden in umgekehrter Reihenfolge ihrer Erzeugung freigegeben:

Ausgabe:

acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a

Die umgekehrte Reihenfolge ist genau das, was geschichtete Ressourcen brauchen: Ein Writer umschließt einen Puffer, der eine Datei umschließt, der Writer muss also in den Puffer schreiben, bevor der Puffer in die Datei schreibt, bevor die Datei geschlossen wird.

Die using-Deklaration (C# 8)

C# 8 hat eine Form ohne geschweifte Klammern hinzugefügt. Die Variable wird freigegeben, wenn der umgebende Gültigkeitsbereich endet, meist am Ende der Methode:

static void ExportReport(string path, IEnumerable<string> rows)
{
    using var writer = new StreamWriter(path);
    writer.WriteLine("Report");

    foreach (string row in rows)
    {
        writer.WriteLine(row);
    }
} // writer.Dispose() runs here, at the end of the method

Sie entfernt eine Einrückungsebene, was sich summiert, wenn eine Methode zwei oder drei Ressourcen verwendet. Die Regel zur Reihenfolge der Freigabe ist dieselbe: Deklarationen in einem Gültigkeitsbereich werden in umgekehrter Reihenfolge freigegeben. Der Preis ist, dass die Ressource offen bleibt, bis der Gültigkeitsbereich endet. Wenn eine Datei geschlossen sein soll, bevor die Methode weiterarbeitet, behalte die Blockform oder setze die Deklaration in einen eigenen { }-Block.

Für Objekte, deren Aufräumen asynchron ist (IAsyncDisposable, etwa viele Datenbankverbindungen und Streams), hat C# 8 außerdem await using, das an derselben Stelle DisposeAsync() abwartet.

IDisposable in deiner eigenen Klasse implementieren

Implementiere IDisposable, wenn deine Klasse etwas Freizugebendes besitzt (sie hat einen Stream, einen Timer, eine Verbindung erzeugt) und es daher ihrerseits freigeben muss. Im üblichen Fall ist das eine kurze Methode:

Ausgabe:

1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException

Die Regeln, denen das folgt: Dispose gibt alles frei, was das Objekt besitzt; es mehrmals aufzurufen bewirkt beim zweiten Mal nichts; und das Objekt nach der Freigabe zu verwenden wirft ObjectDisposedException. using (log) zeigt außerdem, dass der Block statt einer Deklaration auch eine vorhandene Variable nehmen kann.

Das längere „Dispose Pattern“ mit einer Methode protected virtual void Dispose(bool disposing) und einem Finalizer brauchst du nur, wenn eine Klasse direkt ein nicht verwaltetes Handle hält (einen rohen Zeiger aus nativem Code). In modernem .NET kapselst du solche Handles stattdessen in einer Unterklasse von SafeHandle, und die einfache Version oben ist alles, was deine Klasse braucht.

Häufige Fehler

  • Ein in einem using-Block erzeugtes Objekt zurückgeben. Es wird freigegeben, wenn der Block endet, der Aufrufer bekommt also einen geschlossenen Stream. Gib die Daten zurück oder lass den Aufrufer das using besitzen.
  • using bei Streams und Writern vergessen. Gepufferte Daten können verloren gehen, und Dateien bleiben gesperrt.
  • Ein geteiltes Objekt freigeben. Gib nur frei, was dir gehört. Ein langlebiger HttpClient zum Beispiel soll geteilt und wiederverwendet werden, nicht pro Anfrage erzeugt und freigegeben.
  • Ein Objekt nach dem Block verwenden. Methoden eines freigegebenen Objekts werfen ObjectDisposedException.
  • Sich auf den Garbage Collector verlassen. Er gibt Speicher frei, keine Dateien oder Sockets, und ruft nie Dispose auf.

Häufig gestellte Fragen

Was macht die using-Anweisung in C#?

using (var x = ...) { ... } ruft x.Dispose() auf, wenn der Block endet, egal ob er normal endet, über return oder weil eine Exception geworfen wurde. Der Compiler macht daraus ein try/finally mit dem Dispose-Aufruf im finally, die Ressource wird also an einer bekannten Stelle freigegeben statt dann, wenn der Garbage Collector läuft.

Was ist IDisposable in C#?

IDisposable ist ein Interface mit einer Methode, void Dispose(). Eine Klasse implementiert es, wenn sie etwas hält, das explizit freigegeben werden muss: ein Dateihandle, einen Netzwerk-Socket, eine Datenbankverbindung, einen Timer. Jedes Objekt, dessen Typ IDisposable implementiert, sollte freigegeben werden, wenn du fertig bist, meist mit using.

Was ist der Unterschied zwischen using und using var?

using (var x = ...) { } gibt am Ende seines Blocks frei. Die using-Deklaration using var x = ...; (ab C# 8) hat keinen eigenen Block: Sie gibt frei, wenn der umgebende Gültigkeitsbereich endet, typischerweise am Ende der Methode. Das spart Verschachtelung, wenn eine Ressource für den Rest der Methode leben soll.

In welcher Reihenfolge werden mehrere using-Objekte freigegeben?

In umgekehrter Reihenfolge ihrer Erzeugung. Bei using (var a = ...) using (var b = ...) { } wird zuerst b und dann a freigegeben. Das ist die Reihenfolge, die du willst, wenn b von a abhängt, etwa ein StreamWriter, der einen FileStream umschließt.

Ruft der Garbage Collector Dispose auf?

Nein. Der Garbage Collector gibt Speicher frei und führt vielleicht zu einem unvorhersehbaren späteren Zeitpunkt einen Finalizer aus, aber er ruft nie Dispose auf. Eine nicht freigegebene Datei kann gesperrt bleiben, und ihre gepufferten Daten werden womöglich nie geschrieben. Deshalb gibt es using.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S