En god tommelfingerregel siger, at man skal rydde op efter sig selv.
Således gælder det i C++, at den, som kalder new også skal kalder delete, og man skal holde sig fra at kalde delete på andres objekter.
I COM er reglen i princippet den samme men i en anden form. Kalder man AddRef på en en COM reference, skal man også kalde Release, og man må ikke kalde Release på andres objekter.
I .NET skulle jeg mene, at reglen er det samme. Dispose skal kaldes af den, der opretter objektet. Men kigger man i .NET frameworket lader det til, at billedet er lidt mudret, hvilket nedenstående eksempler illustrerer.
StreamWriter kan initialiseres med en Stream reference, og kalder man Dispose på sin StreamWriter, vil den delegere Dispose kaldet videre til den underliggende Stream. Nedenstående kode illustrerer dette.
1: static void Main(string[] args)
2: {
3: var fs = new FileStream(@"c:\temp\test.txt", FileMode.Create, FileAccess.Write);
4: WriteStringAsBytes(fs, "1");
5:
6: using (var writer = new StreamWriter(fs))
7: {
8: writer.Write("2");
9: }
10:
11: // Følgende linje kaster exception, fordi StreamWriter allerede
12: // har lukket filen.
13: WriteStringAsBytes(fs, "3");
14: }
15:
16: private static void WriteStringAsBytes(FileStream fs, string s)
17: {
18: var bytes = Encoding.Default.GetBytes(s);
19: fs.Write(bytes, 0, bytes.Length);
20: }
I linje 3 opretter vi en FileStream, som i linje 6 benyttes som den underliggende stream til en StreamWriter. I linje 9 kaldes Dispose på vores StreamWriter, hvilket på hemmelig vis også lukker den underliggende FileStream. Det viser sig i linje 13, hvor vi får en exception, fordi vi forsøger at skrive til en lukket fil.
I mine øjne er det en bug.
I .NET 4.5 har StreamWriter heldigvis fået en ny constructor overload, hvor man har mulighed for at angive, at den underliggende stream ikke skal lukkes ved Dispose. Vores kodeeksempel fra før kommer så til at se lidt anderledes ud, for nu skal vi selv huske at lukke vores FileStream.
1: using (var fs = new FileStream(@"c:\temp\test.txt", FileMode.Create, FileAccess.Write))
2: {
3: WriteStringAsBytes(fs, "1");
4:
5: using (var writer = new StreamWriter(fs, Encoding.Default, 512, true))
6: {
7: writer.Write("2");
8: }
9:
10: WriteStringAsBytes(fs, "3");
11: }
For en god ordens skyld beholder jeg using-blokken omkring StreamWriter.
Implementeringen af SqlConnection og SqlCommand er ligeledes en blanding, men her er tingene vendt om ift. StreamWriter. SqlCommand vil nemlig som udgangspunkt ikke kalde Dispose på den tilhørende SqlConnection, medmindre man beder om det. Denne opførsel kan programmøren vælge at ændre på, i det der findes en overload til SqlCommand.ExecuteReader, der tager en parameter af typen System.Data.CommandBehavior. Ved at anvende CommandBehavior.CloseConnection kan man bede om at få den underliggende SqlConnection lukket automatisk, når man også lukker sin DataReader.
På trods af ovenstående bør man generelt kunne forvente, at andre ikke finder på at kalde Dispose på ens objekter. I samme boldgade bør man også kraftigt overveje at lade være med at lade sine interfaces nedarve IDisposable. Gør man det, har man samtidig sagt til brugerne af ens API, at man regner med at kalde Dispose på de objekter, de skyder ind i ens kode. Man kan tale om en leaky abstraction.