Archive for the ‘IDisposable’ Category

Når exceptions bliver væk

Friday, March 28th, 2008

I den klassiske More Effective C++ diskuterer Scott Meyers et væld af komplikationer ved exceptions i C++. Et af emnerne drejer sig om, hvad der sker, hvis der bliver smidt en exception under behandlingen af en exception. Inspireret af Meyers’ mange overvejelser, tænkte jeg, at det kunne være interessant at se på nogle af detaljerne omkring exceptions i C#.

I denne sammenhæng er using-konstruktionen bemærkelsesværdig, da den jo som bekendt genererer kode, der svarer til en try/finally-block. Så hvad sker der, hvis både koden i try og finally smider en exception?

Betragt nedenstående klasse, der illustrerer problemet. SomeType implementerer IDisposable og har derfor en Dispose()-metode. Desværre smider Dispose() en ExceptionInDispose exception, og Foo() smider en ExceptionInMethod exception.

public class SomeType : IDisposable {
   public void Dispose() {
      throw new ExceptionInDispose();
   }

   public void Foo() {
      throw new ExceptionInMethod();
   }
}

public class ExceptionInDispose : Exception { }
public class ExceptionInMethod : Exception { }

Hvis vi bruger vores type i en using-blok, vil Dispose() blive kaldt ved udgangen af using-blokken, og spørgsmålet er nu: Hvilken exception fanges af vores catch-blok?

try {
   using (SomeType st = new SomeType()) {
      st.Foo();
   }
} catch (Exception e) {
   Console.WriteLine(e);
}

Svaret er, at catch-blokken fanger en ExceptionInDispose, og der er ingen spor af den oprindelige ExceptionInMethod. Den ligger ikke som InnerException, og hvis vi har sat Visual Studio til at stoppe på unhandled exceptions, sker der heller ikke noget, når vi kører ovenstående. Vores oprindelige exception er for alle praktiske formål væk. (Visual Studio giver os dog lov at reagere, hvis vi sætter debuggeren til at stoppe ved thrown exception).

using genererer som bekendt kode svarende til nedenstående:

SomeType st = new SomeType();
try {
   st.Foo();
} finally {
   if (st != null)
      st.Dispose();
}

og problemet er således, at hvis vi smider en exception i finally-delen, mister vi den oprindelige exception. finally bliver kørt, uanset om der er en exception eller ej, og alle indlejrede finally-blokke bliver afviklet inden en omsluttende catch-blok.

Specifikationen for C# er da også meget præcis på dette punkt. Hvis en exception undslipper en finally-blok, vil en eventuel eksisterende exception blive tabt som illustreret ovenfor.

Problemet er, at using ikke giver os mulighed for at definere en catch-blok, og vi kan heller ikke påvirke den genererede finally-blok. Har vi brug for at kunne reagere på ExceptionInMethod, er vi derfor nødt til at udskifte using-blokken med en try/catch/finally-blok som vist nedenfor.

try {
   SomeType st = new SomeType();
   try {
      st.Foo();
   } catch (Exception e) {
      Console.WriteLine(e);
   } finally {
      if (st != null)
         st.Dispose();
   }
} catch (Exception e) {
   Console.WriteLine(e);
}

I det tilfælde kan vi få lov at reagere på begge exceptions.

Med andre ord: hvis der er en risiko for, at Dispose() kan smide en exception, er using ikke særlig anvendelig, idet eventuelle fejl fra try-bloken vil blive tabt i tilfælde af en exception fra Dispose().

Det var en fejl

Friday, February 15th, 2008

For knap et år siden beskrev jeg, hvordan instanser af Control-klassen ikke blev ryddet ordentlig op. Jeg konkluderede, at den opførsel, vi kunne konstatere ved et lille testprogram og en tur i WinDbg, gav anledning til en del undren, men jeg kunne ikke forklare, hvorfor det forholdt sig som tilfældet var.

På det tidspunkt var jeg tilbageholdende med at kalde fænomenet for en fejl, og det var min antagelse, at i praksis ville det ikke betyde det store, eftersom at oprydningen faktisk blev udført, hvis vi blot huskede at kalde CreateControl() på instansen. WinForms er ret funky, og der foregår en masse bag facaden, så min antagelse var, at det nok var mere sandsynligt, at der skulle være et eller andet jeg havde overset, end at der faktisk skulle være en fejl i noget så basalt som Control-klassen. På det tidspunkt var min teori, at problemet var relateret til brugen af CreateControl() og Dispose(). Min ide var, at Controls (eller rettere baseklassen Component) fik en eller anden speciel behandling af CLRen.

Jeg tog fejl. Ser vi listen af rettelser i Service Pack 1 til .NET Framework 2.0 igennem, finder vi nemlig følgende: FIX: A memory leak may occur when you create and then delete Windows form control objects in an application that is built on the .NET Framework 2.0. Der var altså blot tale om en banal fejl. Opretter man instanser af Control, bliver de ikke ryddet op. Jeg kunne altså have påpeget problemet helt uden at se på Dispose() og CreateControl(). Jeg kan dog stadig ikke forklare, hvorfor jeg kunne konstatere, at et kald til CreateControl() kunne gøre en forskel, men det forhold har sikkert være årsag til at fejlen ikke har skabt større opmærksomhed.

Når CLRen giver op

Tuesday, July 24th, 2007

Fejlhåndtering i .NET er skruet sammen omkring exceptions. Hvis noget går galt, smider den kørende kodestump en passende exception, og så må den kaldende kode tage stilling til, hvad der skal gøres. Som bekendt løber CLRen kaldestakken igennem for at finde en catch-blok, der kan håndtere den aktuelle exception. Hvis en sådan mod forventning ikke findes, afsluttes programmet med en unhandled exception-fejl, men ellers bliver fejlen håndteret, og applikationen kan køre videre. CLRen sørger også for, at de relevante finally-blokke afvikles, som de skal. Ingen overraskelser her, men der er faktisk fejlsituationer, hvor dette ikke er den anvendte fremgangsmåde.

Der er situationer, der er så grelle, at CLRen bare vil af med den fejlende applikation så hurtig som mulig. I disse tilfælde afvikles applikationen på samme måde, som hvis der var kaldt System.Environment.FailFast(), hvilket betyder, at eventuelle manglende finalize-metoder og finally-blokke vil blive sprunget over!

Fra og med .NET 2.0 får følgende to situationer CLRen til at smide håndklædet i ringen:

  1. Stack overflow
  2. Exception under kørsel af en finalize-metode

Begge dele kan let eftervises. Et stack overflow er trivielt at generere med en rekursiv funktion – især hvis den som Foo() nedenfor ikke har nogen slutbetingelse. Den vil blive ved med at køre og hurtig fylde stakken med en exception til følge, men som eksemplet illustrerer, er der ikke tale om en almindelig exception.

static void Main(string[] args) {
   try {
      Foo(234.6m);
   } catch {
      Console.WriteLine("catch");
   } finally {
      Console.WriteLine("finally");
   }
}

public static decimal Foo(decimal d) {
   return Foo(d + 1);
}

Under normale omstændigheder vil såvel catch som finally-delen blive kørt, uanset hvad Foo() så end må finde på af ulykker, men da en stack exception fører til en FailFast-situation, bliver ingen af delene kørt her.

Den anden situation er heller ikke svær at genskabe, den kræver bare lidt flere knæbøjninger.

static void Main(string[] args) {
   try {
      DisposableType d = new DisposableType();
      d.Dispose();
      d = null;
      GC.Collect();
      GC.WaitForPendingFinalizers();
   } catch {
      Console.WriteLine("catch");
   } finally {
      Console.WriteLine("finally");
   }
}

public class DisposableType : IDisposable {
   private Component c = new Component();
   public void Dispose() {
      c.Dispose();
   }

   ~DisposableType() {
      throw new NotImplementedException();
   }
}

I ovenstående implementerer DisposableType IDisposable samt en finalize-metode. Sidstnævnte smider dog en exception. For at fremprovokere fejlsituationen i Main(), opretter vi en instans af DisposableType, kalder Dispose() på den for god ordens skyld (det er dog ikke nødvendigt i denne sammenhæng), og sætter referencen d til null. Da det fjerner den eneste reference til instansen, burde garbage collectoren kunne markere vores DisposableType-instans som værende klar til at blive ryddet af vejen, men da typen også implementerer en finalize-metode, er instansen faktisk refereret af den såkaldte Finalize-kø. Vores kald til GC.Collect() sørger derfor for at instansen bliver flyttet fra Finalize-køen til den såkaldte freachable-kø, hvorfra garbage collectoren på et eller andet tidspunkt vil køre instansens finalize-metode. Med et kald til GC.WaitForPendingFinalizers() sikrer vi os, at dette sker, og dermed får vi kørt vores problematiske finalize-metode. Også i dette tilfælde bliver såvel catch- som finally-blokken sprunget over.

Jeg skal ikke kunne sige, om der er flere situationer, hvor disse forhold gør sig gældende, men sikkert er det i hvert fald, at det er klogt at være opmærksom på ovenstående, idet begge disse tilfælde sætter de normale fejlhåndteringsmekanismer i .NET ud af spil, hvilket let kan resulterer i korrumpering af data og andre ubehagelige overraskelser. Man bør således altid være ekstra varsom, når man har med rekursive funktioner at gøre, og man bør ligeledes være meget påpasselig med, hvad man laver i finalize-metoder.

Svar fra forfatteren

Wednesday, July 18th, 2007

Jeg har fået svar fra Francesco Balena på mine spørgsmål vedrørende hans bog Visual C# 2005: The Base Class Library. Han skriver, at han ikke har undersøgt forholdene ved brug af using via WinDbg. Han fortsætter med at sige, at et objekt, der ikke længere er refereret, vil blive taget af Garbage Collectoren, og det er jo rigtigt. Men pointen ved using er netop, at der er en reference til objektet, idet compileren ikke genererer kode, der nulstiller referencen efter kaldet til Dispose().

Derudover sår han en smule tvivl om, hvorvidt man kan bruge WinDbg til at undersøge disse forhold. Den betragtning er jeg slet ikke enig med ham i. I rigtige applikationer kan det være svært at få taget et dump på det rette sted, men i testapplikationer er det trivielt. Der er derfor meget gode muligheder for at sætte en præcis situation op til formålet, og dermed kan man få et eksakt dump af heap, stak og så videre, som man kan undersøge i WinDbg.

Han overser desværre, at jeg også skriver, at det slet ikke er nødvendigt at anvende WinDbg for at konstatere, at compileren ikke nulstiller reference. En inspektion af IL-koden er tilstrækkelig. Nedenstående er IL-koden for en using-konstruktion som den ser ud i Release-mode:

.method public hidebysig static void  UsingDispose() cil managed
{
  // Code size       19 (0x13)
  .maxstack  1
  .locals init ([0] class ConsoleApplication.DisposableType d)
  IL_0000:  newobj     instance void ConsoleApplication.DisposableType::.ctor()
  IL_0005:  stloc.0
  .try
  {
    IL_0006:  leave.s    IL_0012
  }  // end .try
  finally
  {
    IL_0008:  ldloc.0
    IL_0009:  brfalse.s  IL_0011
    IL_000b:  ldloc.0
    IL_000c:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0011:  endfinally
  }  // end handler
  IL_0012:  ret
} // end of method Program::UsingDispose

Læg mærke til, at lokalvariable 0 ikke sættes til null (der mangler ldnull efterfulgt af stloc.0). Dispose() kaldes i instruktion IL_000c, og derefter returnerer metoden, hvilket naturligvis nedlægger den lokale stak og dermed referencen til DisposableType, men selve using-konstruktionen nulstiller altså stadig ikke referencen, som Balena hævder.

Læg også mærke til, at DisposableType allokeres udenfor try/finally-blokken. Det betyder, at en eventuel exception under kørsel af DisposableTypes constructor medfører, at finally-blokken ikke afvikles. Det er dog ikke noget stort problem, eftersom at en sådan fejl også medfører, at referencen ikke initialiseres, og dermed kan finally-blokken alligevel ikke gøre yderligere.

Mit andet spørgsmål vedrørende JIT compileren optimering har jeg desværre ikke fået udtrykt klart nok, for Balena har i hvert fald ikke helt forstået, hvad jeg mener.

Han forklarer desuden, at hans teknik til undersøgelse af disse konstruktioner bygger på Finalize-metoder. Ved at trace hvornår Finalize-metoder bliver kaldt, har han kunne følge med i objekternes levetid. Han indrømmer selv, at denne metode ikke er helt perfekt, da tilstedeværelse af en Finalize-metode på en type, forlænger instanser af denne types levetid.

Balena har lovet at se nærmere på ovenstående, når han får bedre tid, så det kan være, at dialogen fortsætter. Så stay tuned …

Do og undo med IDisposable

Wednesday, July 11th, 2007

Jeg har skrevet til forfatteren til Visual C# 2005: The Base Class Library, Francesco Balena vedrørende JIT compilerens optimeringer og compilerens opførsel ved brug af using. Balena påstår, at using sætter variablen til null (side 56). Jeg hævder, at dette ikke er tilfældet. Mens jeg venter på svar, vil jeg underholde med en anden observation fra Balenas bog.

Han foreslår nemlig, at kombinationen af constructor og Dispose() kan bruges til en do/undo-mekanisme, der f.eks. kan anvendes til at sætte en cursor for en handling og derefter få ryddet op eller som i nedenstående at sætte det aktuelle directory for et sæt operationer.

using (new CurrentDirectory(@"c:\temp")) {
   // Lav noget i c:temp
} // Implicit Dipose sætter til oprindelig directory

Ovenstående kræver naturligvis, at Dispose() sørger for at rydde op på passende vis, men det er der jo heller ingen ben i her.

Bemærk i øvrigt den sindrige konstruktion, hvor ressourcen end ikke anvendes i selve using-blokken. Det kan måske undre, hvordan using sørger for at få kaldt Dispose() på den allokerede type, men som det fremgår af IL-koden, er det ikke et problem. Som bekendt har den lokale stak referencer til lokale variable, og blot fordi der ikke eksplicit erklæres en reference, ændrer det ikke på den generede kode på det punkt.

  .locals init ([0] class [TestApp]TestApp.DisposableType CS$3$0000)
  IL_0000:  newobj     instance void [TestApp]TestApp.DisposableType::.ctor()
  IL_0005:  stloc.0
  .try
  {
    IL_0006:  leave.s    IL_0012
  }  // end .try
  finally
  {
    IL_0008:  ldloc.0
    IL_0009:  brfalse.s  IL_0011
    IL_000b:  ldloc.0
    IL_000c:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0011:  endfinally
}

Jeg må indrømme, jeg godt kan se det smarte i konstruktionen. Jeg er dog ikke meget for den, men lad os begynde med fordelene. For det første giver det en dejlig kompakt kode. Al logikken omkring håndteringen den midlertidige tilstand er indkapslet i constructoren og det implicitte kald til Dispose(), og i og med at Dispose() kaldes i en finally-blok, er man tilmed sikker på at oprydningen sker efter planen (men se mit indlæg om afbrudte constructors).

Min bekymring ved ovenstående er mest af alt principiel. Da IDisposable bruges til at indikere at typen anvender en eller flere begrænsede ressourcer, synes jeg at man bevæger sig på kanten af det acceptable ved at bruge Dispose() som et smart trick til oprydning af helt almindelige ressourcer. Typer, der implementer IDisposable, får allerede positiv særbehandling af compileren, og givet de problemer der er med at implementere Dispose() og finalize korrekt, kan man sagtens forestille sig, at denne særbehandling udvides. Hvis det sker, risikerer man, at det smarte trick pludselig ikke er så smart mere.

En anden relateret bekymring går på, at det er svært nok at få folk til at forstå IDisposable, og derfor er det en god ide, at have meget eksakt vejledning i hvordan dette gøres korrekt. I den sammenhæng er det kun til besvær, at udvide denne i forvejen komplekse problemstilling med finurlige tricks og undtagelser. Det kan være, at jeg maler fanden på væggen her, men jeg synes ikke gevinsten modsvarer den øgede forvirring og risikoen for problemer hvis behandlingen af disposable-typer ændres.

Afbrudte constructors og IDisposable

Thursday, June 28th, 2007

Jeg påstod i mit forrige indlæg, at hvis en constructor bliver afbrudt, kan ressourcer være ikke-initialiserede, hvilket gør, at man naturligvis ikke kan kalde en eventuel Dispose()-metode på disse. Min løsning var at udvide Microsofts anbefalinger ved at tilføje et check for om de relevante variable er null. Nedenstående eksempel illustrerer den oprindelige problemstilling. Bemærk, at dette bestemt ikke er den rigtige måde at skrive sin Dispose()-metode.

public class DisposableType : IDisposable {
   private Component component; // Disposable ressource
   public DisposableType() {
      // Øger sandsynligheden for at vi får afbrudt constructoren
      System.Threading.Thread.Sleep(1);
      component = new Component();
   }

   ~DisposableType() {
      Dispose(false);
   }

   public void Dispose() {
      Dispose(true);
      GC.SuppressFinalize(this);
   }

   protected virtual void Dispose(bool disposing) {
      // Dette er *ikke* en korrekt implementering af Dispose(),
      // men sådan så den ud i vores GUI-bibliotek.
      component.Dispose();
   }
}

public class WorkerClass {
   public static void DoWork() {
      for (int i = 0; i < 5000; i++) {
         using (DisposableType d = new DisposableType()) {
            // simuler at vi laver noget ...
            System.Threading.Thread.Sleep(1);
         }
      }
   }
}

static void Main(string[] args) {
   Thread t = new Thread(new ThreadStart(WorkerClass.DoWork));
   t.Start();
   // Giv vores tråd en chance for at komme i gang
   System.Threading.Thread.Sleep(10);
   t.Abort();
}

DisposableType allokerer en disposable ressource og implementerer derfor IDisposable, så brugeren kan iværksætte en oprydning og på den måde undgå at vente på at Finalize-metoden kommer til på et eller andet tidspunkt.

Jeg har lagt en kunstig pause ind i DisposableTypes constructor. Det vil man naturligvis ikke gøre under normale omstændigheder, men det hjælper os til at gøre problemet reproducerbart. Bemærk også at der kun er en ressource i eksemplet, men man kan sagtens forestille sig situationer hvor nogle af ressourcerne er initialiserede, mens andre ikke er det. Den aktuelle tilstand kommer helt an på, hvornår constructoren bliver afbrudt.

I Main() starter vi en tråd, der kalder DoWork(), hvis arbejde består i at lave en masse instanser af DisposableType. Bemærk at disse laves i en using-konstruktion, så vi gør, som man skal og rydder pænt op efter os her. Når DoWork() først er i gang, slår vi den ihjel med et kald til Abort(). Det resulterer i en ThreadAbortException, hvilket slår vores tråd ihjel. Applikationen dør under kørsel af Finalize-metoden på DisposableType, hvilket i dette tilfælde højst sandsynligt sker som del af nedlæggelsen af den aktuelle AppDomain (der har næppe været behov for oprydning på et tidligere tidspunkt).

Da DisposableType implementerer en Finalize-metode, registreres instanser af typen på Finalize-listen, når de oprettes. Det vil sige, før constructoren bliver kørt. Det betyder også, at uanset om constructoren kører til ende eller ej, vil hver instans være registreret på listen, og Finalize-tråden vil kalde deres respektive Finalize-metode, med mindre GC.SuppressFinalize() kaldes inden. De instanser, der bliver lavet uden problemer, får kaldt deres Dispose()-metode som del af using-konstruktionen, og dermed bliver de fjernet fra Finalize-listen.

Bliver DisposableTypes constructor imidlertid afbrudt, får erklæringen i using ikke en reference til instansen og dermed bliver Dispose() heller ikke kaldt (referencen er null, men using genererer som bekendt en finally-blok, der checker om referencen er gyldig inden Dispose() kaldes, så ingen problemer her). Da Finalize-listen under alle omstændigheder har en reference til instansen, betyder det, at den aktuelle Finalize-metode og dermed Dispose() vil blive kaldt. Da Dispose() ikke tager højde for, at component kan være uinitialiseret, dør koden med en NullReferenceException på dette tidspunkt. Det var det problem, vi så i vores GUI-bibliotek.

Så for at opsummere: Enten får constructoren lov til at køre færdig eller også bliver den afbrudt. Hvis det første er tilfældet, er alle typens egne referencer i orden, og vi får en reference, vi kan bruge til at kalde Dispose(). Er det modsatte tilfældet, kan vi ikke regne med, at typens referencer er blevet initialiseret, men eftersom vi ikke har en reference til objektet, kan vi heller ikke kalde Dispose() på instansen. Har typen derimod en Finalize-metode, har Finalize-listen en reference til objektet, da denne oprettes inden constructoren kører. Det vil sige, at Finalize-tråden kan kalde Finalize på instansen, og eftersom den bør kalde Dispose(), kan vi få kaldt Dispose() på et ikke initialiseret objekt. Men – og her kommer jeg til at modsige mit tidligere indlæg – eftersom at Finalize-metoden kalder Dispose(false), bør det være tilstrækkeligt at checke på disposing-flaget.

Det vil sige, at Microsofts dokumentation som udgangspunkt ikke er så tosset endda (men flere detaljer ville stadig være på sin plads). Derfor er det også interessant, at Microsoft i mange tilfælde faktisk laver null-check i deres kode, ligesom det er værd at bemærke, at en masse kloge hoveder også anbefaler denne praksis. Skønt jeg vil mene, at der strengt taget ikke er behov for det, eftersom disposing-flaget har samme effekt under normale omstændigheder, kan der være andre gode grunde til at gøre det alligevel.

For det første er det nødvendigt, hvis man i tillæg til at kalde Dispose() også nulstiller referencer. Da Dispose() skal kunne tåle at blive kaldt et vilkårligt antal gange for en instans, er et null-check obligatorisk i denne situation.

Derudover er det fornuftigt at lave sin Finalize-kode så stabil som mulig. Da Finalize-metoderne afvikles i en separat tråd, vil en exception, der undslipper en Finalize-metode, ramme selve Finalize-tråden, med det resultat, at den kaster håndklædet i ringen, hvilket som dokumentationen siger, betyder at Finalize-tråden afbrydes, eventuelle resterende Finalize-metoder bliver sprunget over og applikationen lukkes. Så skønt et null-check kan se overflødigt ud, er det en god sikkerhedsforanstaltning mod, at nogen på et senere tidspunkt kommer til at nulstille de referencer, der bruges til at kalde Dispose() på ressourcerne.

En sidste grund er naturligvis i tilfældet hvor klassens constructor er mocked (med TypeMock.NET) som del af en test. I det tilfælde kører constructoren ikke, og dermed bliver referencerne heller ikke initialiseret. Her er disposing-flaget tilmed ikke tilstrækkeligt, da det ikke beskytter mod den manglende reference ved et eksplicit kald til typens Dispose()-metode. Bruger man TypeMock.NET i sit testmiljø skal alle Dispose()-metoder således checke disposing-flaget og validere samtlige referencer i Dispose().

Skønt min argumentation er ændret en anelse, er konklusionen stadig den samme. Når det kommer til Finalize-metoder (og Dispose()-metoder, der kaldes fra disse), er det bedre at gå med livrem og seler end at forsøge at være lidt smart, så brug disposing-flaget og check for null for en sikkerheds skyld.

Hvis man implementerer IDisposable, skal man gøre det korrekt

Monday, June 18th, 2007

På mit arbejde bruger vi et fancy grafikbibliotek, der implementerer en række smarte GUI-komponenter. Vi bruger også TypeMock.NET i forbindelse med vores unit test. TypeMock er lidt af et power tool, når det kommer til mocking. Det anvender CLR Profiler APIet til at opsnappe og omdirigere kald til de typer, man mocker som en del af testen. Det vil sige, at det kan mocke stort set alt, men det betyder også, at man kan komme i nogle situationer, mange udviklere nok ikke har forestillet sig mulige, og netop det har givet anledning til det meget spøjse problem, vi løb ind i.

Når man mocker en type, kan man bestemme om man vil mocke constructoren eller ej. Hvis man mocker constructoren, betyder det, at hverken typens egen constructor eller eventuelle baseklassers constructorer bliver kørt. Det eneste, der sker ved oprettelse af en mocked type, er således, at der bliver afsat plads til den, så referencen og typeobjektet er på plads. Og så er der lige en enkelt vigtig detalje mere: Hvis typen eller en af dens baseklasser implementerer en Finalize-metode, tilføjer afviklingsmiljøet en reference til instansen på den såkaldte Finalize-kø.

Det er anbefalet praksis, at hvis man implementerer en Finalize-metode, skal man også implementere IDisposable, så brugeren har mulighed for at rydde op på et veldefineret tidspunkt, og derved ikke behøver at vente på, at Finalize-metoden bliver kaldt som en del af den almindelige garbage collector oprydning. IDisposable skal ifølge Microsofts dokumentation implementeres således:

public class MicrosoftsRecommendedImplementation : IDisposable {
   private bool Disposed = false;
   private SomeResource TheResource;

   public MicrosoftsRecommendedImplementation() {
   // Appropriate constructor code
}

   ~MicrosoftsRecommendedImplementation() {
      Dispose(false);
   }

   public void Dispose() {
      Dispose(true);
      GC.SuppressFinalize(this);
   }

   protected virtual void Dispose(bool disposing) {
      if (!Disposed) {
         if (disposing) {
            TheResource.Dispose(); // managed resource
         }
         // Dispose any unmanaged resources here
         Disposed = true;
      }
   }
}

Ideen med de to Dispose()-metoder er at kunne se forskel på, om Dispose() kaldes eksplicit eller som en del af oprydningen i forbindelse med Finalize-metoden. Kaldes Dispose() direkte sørger kaldet til GC.SuppressFinalize() for, at instansen fjernes fra Finalize-køen, hvilket sikrer hurtigere nedlæggelse af instansen.

Læg mærke til at det ikke specificeres, hvor TheResource initialiseres. Det kan gøres direkte i erklæringen eller i constructoren. Læg også mærke til at anbefalingen ikke foreskriver, at man skal checke for om TheResource er null! En bedre måde at implementere Dispose(bool) er derfor:

protected virtual void Dispose(bool disposing) {
   if (!Disposed) {
      if (disposing) {
         if (TheResource != null) {
            TheResource.Dispose();
         }
         // clean up unmanaged resources
      }
      Disposed = true;
   }
}

Det var lige præcis et manglende null-check, der var årsagen til det problem, vi stødte på. Det forholder sig nemlig sådan, så snart en instans af en type med en Finalize-metode oprettes placeres instansen på Finalize-køen, men hvis constructoren ikke får love at køre til ende, kan en eller flere af typens ressourcer være uinitialiserede. Det får naturligvis et kald til Dispose() på en sådan til at fejle med en NullReferenceException.

Eftersom vi mocker constructoren, får den ikke lov til at køre og dermed fik vi en NullReferenceException. Ikke alene checkede vores indkøbte GUI-komponenter ikke disposing-flaget som Microsoft ellers foreskriver, de checkede heller ikke, om deres interne ressourcer faktisk var allokeret som forventet.

Ser man Microsofts kode igennem med Reflector, vil man se, at de er meget påpasselige med altid at lave null-checks, inden de tilgår ressourcer i Dispose()-metoder. Hvorfor det ikke er en del af anbefalingen, kan jeg kun gisne om, for det kan nemlig godt ske, at en constructor ikke får lov at gøre til ende, og så sprænger Dispose()-kaldet i luften, hvis man ikke checker for null. Eksempelvis kan en constructor blive afbrudt af en ThreadAbortException, hvilket kan være en helt gyldig operation. I det tilfælde vil alle typer, der antager, at de selv har styr på deres ressourcer få problemer. Desværre er vores indkøbte GUI-bibliotek fyldt med den slags antagelser.

Så for lige at opsummere:

  1. Har en type en Finalize-metode, skal den også implementere IDisposable.
  2. Implementerer en type IDisposable bør den også have en Finalize-metode (der er undtagelser som jeg vil komme ind på senere).
  3. IDisposable skal implementeres som Microsofts anbefalinger foreskriver, men man skal desuden tilføje check for at ressourcer faktisk er tilgængelig.

Og til Microsoft: Få lige opdateret den dokumentation tak.