Advanced Java Services | close, dispose und using |
Man öffnet eine Datei (zum Lesen und/oder Schreiben), man bearbeitet sie, und am Ende schließt man sie. An diesen drei Schritten hat sich seit ihrer Einführung nichts mehr geändert. Bei jedem der drei Schritte können Fehler auftreten, die man mit einem try-catch-Konstrukt abfangen kann. Es empfiehlt sich hierzu über Exceptions Bescheid zu wissen. In C# gibt es darüber hinaus das using-Konstrukt, welches sich als eine Kurzform eines spezielles try-catch-finally-Konstrukts herausstellen wird. Wir beginnen mit dem klassischen try-catch Konstrukt.
Wir wählen den StreamWriter. Mit ihm kann man etwa Text in eine Datei fast genauso einfach schreiben wie auf die Console. Ohne Fehlerbehandlung sähe das so aus.
using System; using System.IO; namespace io { class StreamWriterDemo { static void Main() { StreamWriter sw = new StreamWriter("test.txt"); // Öffnen // UnauthorizedAccessException // ArgumentException // ArgumentNullException // DirectoryNotFoundException // PathTooLongException // IOException // SecurityException sw.Write("foo"); // Bearbeiten // ObjectDisposedException // NotSupportedException // IOException sw.Close(); // Schließen // EncoderFallbackException } } }
Hier ist natürlich eine Fehlerbehandlung Pflicht. Die möglichen Exceptions sind bereits als Kommentar aufgelistet
using System; using System.IO; namespace io { class StreamWriterDemo { static void Main() { StreamWriter sw = null; try { sw = new StreamWriter("test.txt"); // Öffnen sw.Write("foo"); // Bearbeiten } catch(Exception ex) { Console.WriteLine(ex.Message); } finally { try { if(sw!=null) sw.Close(); // Schließen } catch(Exception ex) { } } } // end Main } }
sw.Close() muß im finally-teil stehen, damit es auf alle Fälle ausgeführt wird. Da auch Close() schiefgehen kann brauchen wir nochmal ein try-catch.
Diese Variante führt uns zur Verwendung des using-Konstrukts. Neben der Close()-Methode gibt es bei allen Streamklassen in der IO-Hierarchie die Methode Dispose(). Sie wird im Interface IDisposable vereinbart. Wie man der Graphik entnehmen kann wird dieses Interface von allen Dateihandlingklassen implementiert. Die Methode Dispose() macht das gleiche wie Close(), wirft aber keine Exception (laut API), so daß in finally nicht erneut ein try-catch stehen muß. Das sieht folgendermaßen aus.
using System; using System.IO; namespace io { class StreamWriterDemo { static void Main() { StreamWriter sw = new StreamWriter("test.txt"); try { sw.Write("foo"); // Bearbeiten } finally { if(sw!=null) ((IDisposable)sw).Dispose(); // Schließen // Dispose() von Object wird gerufen, Dispose() wirft keine Exception } } // end Main } }
Hier wird in finally explizit die Methode Dispose() aus der Klasse Object gerufen. Das obige Konstrukt ist äquivalent zu der folgenden using-Anweisung.
using System; using System.IO; namespace io4 { class StreamWriterDemo { static void Main() { using(StreamWriter sw = new StreamWriter("test.txt") ) // Öffnen { sw.Write("foo"); // Bearbeiten } } } }
Tatsächlich übersetzt der Compiler die using-Anweisung exakt in das vorhergehende try-finally Konstrukt. (siehe etwas Understanding the 'using' statement in C#) Welche Schreibweise man wählt ist letzten Endes Ansichtssache. Ein Vorteil ist jedoch, daß die durch die Verwendung von using automatisch aufgerufene Methode Dispose() keine Exception wirft, Close() jedoch schon.
Für die Klassen aus dem Namespace IO haben die Methoden Dispose() und Close() die gleiche Wirkung, siehe dazu den Artikel von Kim Hamilton im MSDN Blog. Bei Datenbankverbindungen kann es jedoch Unterschiede geben, vgl. hierzu die beiden Artikel auf stackoverflow.com Artikel 1 und Artikel 2 und auch Artikel 3.
Der letzte Artikel zeigt insbesondere, daß die Methoden Close() von StreamReader und StreamWriter Dispose() aufrufen.
Bei der Einführung der using-Anweisung ging es eben nicht um Fehlerbehandlung, sondern man wollte ein Konstrukt, das durch den automatischen Aufruf von Dispose() kein finally mehr braucht.
Braucht man eine Fehlerbehandlung, so gibt es mehrere Möglichkeiten
try { using (StreamReader sr = new StreamReader("test.txt")) // Öffnen { string line = sw.ReadLine(); // Einlesen } } catch (Exception ex) { // Fehlerbehandlung }
Beim Öffnen eines StreamReaders können die folgenden 5 Exceptions auftreten, auf die man in catch() reagieren kann.
ArgumentException (path is an empty string).
ArgumentNullException (path is null)
FileNotFoundException (The file cannot be found)
DirectoryNotFoundException (The specified path is invalid, such as being on an unmapped drive)
IOException (path includes an incorrect or invalid syntax for file name, directory name, or volume label)
Eine andere Möglichkeit ist eine Überprüfung mit den Utilityklassen File bzw. Directory.
... if (filename != null && filename != "" && Directory.Exists(filename) && File.Exists(filename)) using (StreamReader sr = new StreamReader(filename)) { // kleine Textdateien auf einmal lesen string text = sr.ReadToEnd(); Console.WriteLine(text); }
StreamWriter sw = null; try { sw = new StreamWriter("test.txt"); // Öffnen sw.Write("foo"); // Bearbeiten } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { if(sw!=null) ((IDisposable)sw).Dispose(); }
In diesem Fall muß man selbst darauf achten, daß in finally die Dispose()-Methode von Objekt aufgerufen wird.
Interessant zu diesem Thema ist auch die Diskussion auf
social.msdn.microsoft.com