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.

Jeg kan huske, at jeg for en del år siden hørte Don Box til afslutningssessionen ved TechEd i Barcelona fortælle om, at noget nær den største sikkerhedsrisiko i .NET var, at alle klasser pr. default ikke er “sealed”.  Jeg ved ikke, om det er en kæmpe sikkerhedsrisiko, men manden havde vel fat i noget (som han som regel har).  Nedarvning er ikke svaret på alles problemer, så det burde vel egentlig være sådan, at man aktivt skal åbne for nedarvning af ens klasse i stedet for som nu, hvor man aktivt skal lukke for det.

Men også ganske små detaljer kan give bugs, hvis man ikke tager højde for potentiel nedarvning.

Jeg stødte selv ind i en lille bug forleden, fordi jeg i min naivitet valgte at nedarve fra en klasse, der ikke var designet til nedarvning.  Der opstod heldigvis ikke noget stort brud på sikkerheden i virksomheden af den grund – i stedet fik jeg bare en almindelig null-reference exception.

Klassen, jeg nedarvede fra, indeholdt bl.a. følgende linjer:

   1:  Assembly assem = this.GetType().Assembly;
   2:  var resourceStream = assem.GetManifestResourceStream(this.GetType(), "ResourceName");

Problemet er kaldene til this.GetType().  Hvis klassen er nedarvet, vil GetType() returnere typen for den nedarvede klasse, og hvis de to klasser ikke ligger i samme assembly, vil .NET kigge i det forkerte assembly efter den angivne resource.  Så lektien må være, at man skal tænke sig nøje om for ikke at putte sealed på sin klasse.