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

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.

2 Responses to “Hvis man implementerer IDisposable, skal man gøre det korrekt”

  1. Thomas says:

    Hej Brian,

    Du har en potentiel bug/mangel i din implementering af Dispose().

    Naar disposing er false, saa skal man huske clean up any unmanaged ressourcer. Det kan man ikke umiddelbart se af din impl.

    (Naar disposing er true, er der sikkert at clean up baade managed og unmanaged ressourcer. Dette viser du ogsaa i din impl.)

    Desuden er implementeringen ikke threadsafe.

    Jeg ville nok tilfoeje dette:

    
            private readonly object m_syncRoot = new object();
            private bool m_disposed = false;
            public bool Disposed
            {
                get
                {
                    lock (m_syncRoot)
                    {
                        return m_disposed;
                    }
                }
            }
    
            protected virtual void Dispose(bool disposing)
            {
                lock (m_syncRoot)
                {
                    if (!this.Disposed)
                    {
                        if (disposing)
                        {
                            if (m_resource != null)
                            {
                                //dispose ALL managed AND unmanaged resources.
                                m_resource.Dispose();
                            }
                        }
    
                        //cleanup ONLY unmanaged resources here.
    
                        this.m_disposed = true;
                    }
                }
            }
    
  2. Hej Thomas – tak for din grundige kommentar. Det er helt rigtig, at implementeringen ikke er thread safe, men det er Microsofts anbefalede måde at gøre det på. Man bliver jo nødt til at tage stilling til, om man har brug for at en given type er thread safe eller ej. Har man brug for det, skal man gøre, som du beskriver. Hvis ikke er ovenstående nok (og så er der ingen grund til at betale det overhead lock giver). Det er en god pointe at få det understreget.

    Mht. fejl/mangel i eksemplet kan jeg ikke se din pointe. Vores kode er identisk (hvis vi ser bort fra lock) og clean-up af unmanaged ressourcer illustreres kun som kommentar i vores eksempler. Forskellen er, at du skriver i din kommentar, at både managed og unmanged ressourcer skal frigives i tilfælde af at disposing er true (din kommentar burde måske også være flyttet over m_resource != null, men det er en anden sag), men det er jo implicit, eftersom at vi i alle tilfælde havner uden for if(disposing)-delen, og dermed får frigivet begge dele, når disposing er true.

Leave a Reply