Archive for the ‘Exceptions’ Category

Exception eller ej

Monday, February 15th, 2010

Mark Seemann stillede for nylig et spørgsmål på Twitter, der lød noget i retning af ”Skal Delete(int id) kaste en exception hvis det element, id repræsenterer, ikke eksisterer, eller skal den bare undlade at gøre noget?”.

Ræsonnementet for ikke at gøre noget er naturligvis, at hvis vi beder om at få slettet noget, og det ikke er der, så er sluttilstanden jo den ønskede, uanset om vi slettede noget eller ej.

Mit svar var, at med den signatur skulle metoden kaste en exception, og i dette indlæg vil jeg forsøge at uddybe lidt. Der er to grunde til, at jeg synes, en exception er på sin plads i dette tilfælde.

For det første er vi vant til, at Delete() på framework-typer som f.eks. File, Directory og DataRow alle smider passende exceptions, hvis de kaldes med et element, der ikke findes. Så hvis vi vil holde os til Principle of least surprise, er en exception det rette valg.

For det andet, og måske vigtigere, vil det være umuligt for den kaldende kode at afgøre, hvorvidt operationen var en succes eller ej, hvis Delete() ignorerer eventuelle fejl. Da vi ikke kan sige noget om, hvilken kontekst Delete() vil blive kaldt i, er det ikke rimeligt, at tage beslutninger på den kaldende kodes vegne. Vi ved med andre ord ikke, om koden har brug for at skelne mellem succes og fejl.

Hvis koden kalder Delete(), må vi antage, at det er fordi applikationens tilstand af en eller anden grund får det til at give mening. Koden antager altså, at det kan lade sig gøre. Ved at ignorere en eventuel fejlsituation, fratager vi den kaldende kode muligheden for at opdage, at denne antagelse ikke holdt stik. Vi risikerer altså at skjule en fejl i koden.

Mark fulgte desuden op med følgende spørgsmål: Gør det nogen forskel om Delete() kaldes i et miljø med flere tråde? Nej, ikke i min optik.

File.Delete() er i høj grad underlagt denne problemstilling. Uanset at vi kan undersøge, om en given fil eksisterer lige inden, vi kalder Delete(), er denne operation altid underlagt en race condition. Filen kan forsvinde mellem vores kald til Exists() og Delete(), og den eneste måde, vi kan opdage, om dette er tilfældet, er, hvis Delete(), kaster en exception. Derefter må det være op til den kaldende kode, om den vil reagere på dette eller ej. Ved at undlade at reportere fejlen, fratager vi den kaldende kode muligheden for at reagere.

Derfor synes jeg, en exception er det rette valg i denne situation.

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().

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.

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.

Håndtering af exceptions

Tuesday, May 29th, 2007

Exceptions er tilsyneladende ikke lette at forstå. Skønt de giver langt bedre muligheder for ensartet, central håndtering af fejl end de gammeldags returkoder, støder man ofte på misforståelser omkring brug af exceptions. F.eks. faldt jeg for nylig over nedenstående konstruktion under et code review:

try {
   // her kan exceptions opstå
} catch (Exception) {
}

Koden kan beskrives således: Man forsøger at gøre et eller andet og uanset hvad, der end kan gå galt, ignorerer man det og fortsætter som intet er hændt! Det er aldrig en god ide, da programmet fortsætter i en ikke veldefineret tilstand.

Et af de argumenter, jeg har hørt i forsvaret af ovenståender går noget i retning af, ”hvis jeg ikke gør det, fejler min kode”. Nej, koden er allerede stødt ind i en fejl, men ovenstående ignorerer bare det faktum. Exceptions giver som nævnt mulighed for central håndtering af fejl, så det er ikke meningen at al kode skal kunne håndtere alle fejl. Typisk vil uhåndterede exceptions generere en fejlrapport, så den egentlige fejl kan findes og udbedres. Ved at lade catch sluge fejlen, får udviklerne aldrig at vide, at noget gik helt galt. At ignorere problemet er ikke det samme som at løse det.

Jeg tror, at situationen opstår som følge af manglende forståelse for, hvad catch egentlig betyder. Med en catch konstruktion siger man, jeg ved, hvad der kan gå galt her og jeg ved, hvad jeg i så fald skal gøre. Lad os sige, at vi forsøger at sende en fil via en netværksforbindelse. Her er det rimeligt at forvente problemer i forhold til transmissionen og behandlingen af filen, og applikationen vil med al sandsynlighed kunne håndtere disse tilfredsstillende ved at prøve igen et antal gange. Derfor bør der opstilles et par specifikke catch-blokke, der hver især tager sig af forventede exception-situationer. Ovenstående fanger godt nok de forventede exceptions, men konstruktionen fanger også alt andet. Kan applikationen virkelig håndtere en null reference, manglende hukommelse og et signal om at slå den kørende tråd ihjel ved blot at forsøge at sende filen igen? Næppe.

Så under de fleste omstændigheder er det ikke i orden at fange alle exceptions. Fang kun dem, der kan behandles lokalt. Der er så vidt jeg kan se kun to tilfælde, hvor det er i orden at fange alle exceptions og i begge tilfælde gælder det, at man fanger dem for at kaste dem videre. I alle andre tilfælde skal man kun behandle de exceptions, man faktisk kan gøre noget ved og lade andre om resten.

Det ene tilfælde er, at vi ønsker at gøre et eller andet inden håndklædet bliver kastet i ringen. Det kan f.eks. være, at vi ønsker at skrive en besked i en log. Skønt det måske er nærliggende at tro, er oprydning ikke noget, der bør foretages i dette tilfælde. Oprydning skal placeres i finally-blokken, så vi får ryddet op efter os uanset om vi løber ind i fejl eller ej.

Det andet tilfælde er, at vi ønsker at fange vores exception fra en tråd og kaste den videre på en anden. Afvikler man eksempelvis kode asynkront via BeginInvoke(), vil en eventuel exception blive opfanget af afviklingsmiljøet og først blive returneret til brugeren ved kald af EndInvoke(). I det tilfælde ville det være oplagt at inkludere den oprindelige exception som del af en ny, passende exception. F.eks.

public delegate void SomeDelegate();

static void Main(string[] args) {
   SomeDelegate d = delegate() { throw new NotImplementedException(); };

   IAsyncResult res = d.BeginInvoke(null, null);
   System.Threading.Thread.Sleep(100);

   try {
      d.EndInvoke(res);
   } catch (Exception e) {
      throw new ApplicationException("ups!", e);
   }
}

Dette er de to eneste acceptable tilfælde, jeg kan komme i tanke om. I alle andre tilfælde skal man bruge catch til at behandle specifikke exceptions og lade alt andet gå videre, hvilket i sidste instans vil sige til den globale exception handler, der giver mulighed for at rapportere fejlen til udviklerne.

NotImplementedException Not Implemented

Wednesday, March 14th, 2007

Visual Studio har en fin lille feature, man kan få glæde af hver gang en klasse skal implementere et interface: Højreklik på interface-navnet og vælg Implement interface og voila, Visual Studio opretter fluks stubs til de relevante metoder.

Så hvis vi er ved at implementere IComparer, tilføjer Visual Studio følgende:

#region IComparer<string> Members

public int Compare(string x, string y) {
   throw new Exception("The method or operation is not implemented.");
}

#endregion

På den måde får man hurtigt oprettet nok til, at man med compilerens øjne har implementeret et givent interface og så kan man gå i gang med det reelle arbejde som det passer en. Smart!

Men, men, hvis man som jeg er lidt pernittengryn, kan man jo kun undre sig over hvorfor pokker den genererede kode bruger Exception til at smide en ”not implemented exception”, når nu NotImplementedException synes at være skræddersyet til netop den opgave.

I det hele taget er det noget snavs at smide Exceptions. Brug nogle af de mange exceptions i frameworket eller udvid med egne (og brug ApplicationException som baseklasse).

Exceptions og kaldestakken i C#

Sunday, March 11th, 2007

I C# nedarver alle exceptions fra den fælles baseklasse Exception. Denne indeholder den basale funktionalitet som f.eks. en komplet oversigt over kaldestakken. Så givet en Exception e, er kaldestakken tilgængelig via e.StackTrace.

Da StackTrace er en property på Exception, er det nærliggende at tro, at den enkelte Exception holder styr på sin aktuelle kaldestak, men det er en sandhed med modifikationer. Har man brug for at kaste en exception videre, er det nemlig afgørende, hvordan dette gøres.

Betragt nedenstående klasse. Metoden ThrowTheException kaster blot en exception af typen Exception. Der er to par af metoder, der begge griber den oprindelige exception og kaster den videre. I det ene tilfælde er der komplet kaldestak tilbage til ThrowTheException, mens det andet tilfælde får nulstillet kaldestakken, så det ser ud som om at den aktuelle exception kommer fra CatchAndLoseHistory.

public class TheClass {
   private void ThrowTheException() {
      throw new Exception();
   }

   private void CatchAndKeepHistory() {
      try {
         ThrowTheException();
      } catch (Exception) {
         throw; // ingen parameter, e er implicit
      }
   }

   private void CatchAndLoseHistory() {
      try {
         ThrowTheException();
      } catch (Exception e) {
         throw e; // dette nulstiller kaldestakken i e
      }
   }

   public void CatchWithHistory() {
      try {
         CatchAndKeepHistory();
      } catch (Exception e) {
         Console.WriteLine(e.StackTrace);
      }
   }

   public void CatchWithoutHistory() {
      try {
         CatchAndLoseHistory();
      } catch (Exception e) {
         Console.WriteLine(e.StackTrace);
      }
   }
}

Det afgørende er således at bruge throw korrekt til at kaste en exception videre. Hvis throw kaldes med den exception, der er indfanget med catch-blokken, nulstilles kaldestakken. Derfor skal throw bruges uden yderligere argumenter, hvis man blot ønsker at kaste den aktuelle exception videre. Altså:

try {
   ThrowTheException();
} catch (Exception) {
   throw;
}

er korrekt.

try {
   ThrowTheException();
} catch (Exception e) {
   throw e; // dette nulstiller kaldestakken
}

er forkert (med mindre man naturligvis er interesseret i at smide den oprindelige kilde til fejlen bort). Lille forskel, stor effekt.