Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 8 Min.

Try, catch und wie es nicht geht

Die wenigen Sprachmittel von C# zur Ausnahmebehandlung sind ausgefeilt. Beim Einsatz ist aber Vorsicht geboten.
© dotnetpro
Was das Erzeugen und Behandeln von Fehlern angeht, herrschen die unterschiedlichsten Meinungen in der Entwicklerwelt. Die vorangegangene Episode von Davids Deep Dive­ hat gezeigt, dass es hierfür aber nur einen richtigen Weg geben kann: das in .NET eingebaute Ausnahmekonzept [1]. Nur dieses erfüllt die fünf Regeln der Ausnahmebehandlung, die für ein reibungsloses Fehlermanagement zu befolgen sind.Der aktuelle Teil von Davids Deep Dive geht weg von der Theorie und befasst sich damit, wie mit Fehlern im Code umgegangen wird. Neben den notwendigen Grundlagen der Fehlerbehandlung soll es aber besonders um die Anti-Pattern der Fehlerbehandlung in C# gehen. Denn auch wenn das Sprachkonstrukt try/catch/finally nicht besonders kompliziert ist, kann es falsch angewendet werden und dabei die Ausnahmesituation teilweise sogar verschlimmern.

Try, catch und finally

Zunächst sollen die Grundlagen der Ausnahmen in C# erläutert werden; erfahrene Entwickler können diesen Abschnitt ohne Weiteres überspringen. Im Prinzip kann nahezu jede Zeile Quellcode in C# eine Ausnahme auslösen, egal ob es eine simple Division ist (die zu einer DivideByZeroException führt) oder eine korrupte Verbindung zu einem SQL-Datenbankserver (SqlConnectionException). Folglich sollte der Quellcode immer in dem Bewusstsein geschrieben werden, dass eine gerade geschriebene Zeile Quellcode nicht erfolgreich ausgeführt werden kann.Tritt eine solche Ausnahme während des Programmablaufs auf, wird die Ausführung der aktuellen Methode auf der ­Stelle unterbrochen und die aufgetretene Ausnahme an die Stelle „weitergeleitet“, an der die fehlerhafte Methode aufgerufen wurde. Dort wird diese dann erneut ausgelöst, womit auch die aufrufende Methode unmittelbar unterbrochen wird. Dieser Prozess setzt sich so lange fort, bis die Ausnahme an der Startmethode (zum Beispiel Main()) angekommen ist. Dort sorgt der Fehler dann für das Terminieren der AppDomain und damit der laufenden Anwendung. Dies ist der übliche Kontrollfluss einer Ausnahme. Soll dieser Kontrollfluss „gesteuert“ werden, kann dies mithilfe des try/catch/finally-Statements geschehen.In Listing 1 wird ein Stream mithilfe der Methode File.OpenRead() aufgerufen, bei der mehrere Ausnahmen auftreten können, weil die Datei nicht vorhanden oder gesperrt ist, weil keine Dateiberechtigung vorhanden ist, weil sie bereits geöffnet ist und so weiter. Ist der Aufruf von OpenRead() jedoch innerhalb eines try-Blocks enthalten, so sorgt die auftretende Ausnahme nicht für einen direkten Abbruch der Methode, sondern es wird zunächst der Inhalt des catch-Blocks ausgeführt, in dem der Fehler behandelt werden kann, Protokollinforma­tionen geschrieben werden können und so weiter. Der optional vorhandene finally-Block wird ­sowohl im Ausnahme- als auch im Erfolgsfall ausgeführt.
Listing 1: Grundlagen von try/catch/finally
Stream strm = null; <br/><span class="hljs-keyword">try</span> <br/>{ <br/>  stream = File.OpenRead(<span class="hljs-string">"data.csv"</span>); <br/>  // ... <br/>} <br/><span class="hljs-keyword">catch</span> <br/>{ <br/>  Console.WriteLine(<span class="hljs-string">"Fehler"</span>); <br/>} <br/>finally <br/>{ <br/>  stream?.Close(); <br/>}  

Der catch-Block

Der catch-Block kann auf mehrere unterschiedliche Arten be­ginnen:
  • catch { … }
  • catch(ExceptionTyp) {…}
  • catch(ExceptionTyp e) { … }
Bei der ersten Variante wird der Kontrollfluss bei allen Arten von Ausnahmen übernommen, unabhängig davon, welchen Typ die Ausnahme hat.Bei Variante zwei wird der catch-Block nur dann ausgeführt, wenn die ausgelöste Ausnahme zum angegebenen Ausnahmetyp passt, und bei Variante drei gewährt die Variable e Zugriff auf das Ausnahmeobjekt.Bei Variante zwei und drei können mehrere catch-Blöcke nacheinander stehen, wobei jeder Ausnahmetyp einen speziellen Ausnahmetyp behandelt. Wichtig dabei ist, dass immer der spezifischere Ausnahmetyp vor dem weniger spezifischen geprüft wird, wie es beispielsweise hier zu sehen ist:
catch(FileNotFoundException) { } 
catch(UnauthorizedAccessException) { } 
catch(Exception) { } 
Da der Fokus dieser Deep-Dive-Episode nicht auf den Grundlagen liegen soll, wollen wir es bei diesen allgemeinen Bemerkungen belassen und auf eine weitere Beschreibung verzichten; stattdessen sei auf die Ausnahmebehandlung im C#-Programmierhandluch von Microsoft verwiesen [2].

Anti-Pattern 1: Leerer catch-Block

Gäbe es eine Hitliste der Exception-Antimuster, wäre die unangefochtene Nummer eins wohl der leere catch-Block. Bleibt ein solcher Block komplett leer, so wird die aufgetretene Ausnahme einfach verschluckt und die Anwendung setzt ihre Ausführung fort. Leider ist dies besonders bei Einsteigern einer der häufigsten Fehler im Umgang mit Ausnahmen, daher muss auch dieser Artikel hier davor warnen: Ausnahmen treten nicht ohne Grund auf, daher sollte es niemals einen­ leeren catch-Block innerhalb einer Anwendung geben; vielmehr sollte die Ausnahme erst gar nicht entstehen. Daher sollte in jeder Codierrichtlinie die Verwendung von leeren catch-Blöcken verboten werden und in Tools wie ReSharper sollte eine entsprechende Verletzung zu einem fehlerhaften Build führen. Zu finden ist diese Einstellung in dem Programm unter Options | Code Inspection | Inspection Severity | Empty general catch clause(Bild 1).
Das ToolReSharper kann leere catch-Blöcke verbieten(Bild 1) © Autor
Nur in absoluten Ausnahmefällen, in denen beispielsweise ein API trotz Erfolg eine Ausnahme auslöst, ist ein leerer catch-Block erlaubt, sollte dann aber trotzdem um einen entsprechenden Kommentar erweitert werden, warum und wieso an dieser Stelle seine Verwendung nicht verhindert werden kann.

Anti-Pattern 2: Alles „catchen“

Der Verzicht auf try/catch/finally ist natürlich ein No-Go, aber der massive Einsatz ist auch nicht viel besser. Von Zeit zu Zeit sehe ich in Kundenprojekten besonders eifrige Entwickler, die nahezu alle Quellcodezeilen mit einem try-Block umschließen, um jede aufkommende Ausnahme zu erwischen.­ Das macht den Code sehr unleserlich und bietet darüber hin­aus meist keine Stabilität innerhalb der Software, denn der Einsatz von try/catch ist nur dann sinnvoll, wenn die aufgetretene Ausnahme auch tatsächlich behandelt werden kann oder sonstige Maßnahmen wie eine Protokollierung nötig sind. Wenn beides nicht erforderlich ist, muss auch kein try/catch eingesetzt werden, denn die aufgetretene Ausnahme wird ganz von allein zur aufrufenden Methode weitergeleitet.Neben der Unsinnigkeit und der schlechten Lesbarkeit des Codes gilt es aber noch etwas viel Wichtigeres zu beachten: Quellcode, der innerhalb eines try-Blocks steht, wird nicht durch den Just-in-time-Compiler der Common Language Runtime optimiert [3]. Folglich führt ein massiver Einsatz von try/catch zu einer verminderten Anwendungsgeschwindigkeit.

Anti-Pattern 3: Alles ins Log schreiben

Zusammen mit dem „Catching everything“-Anti-Pattern ist oft das unnötige und massive Protokollieren von Ausnahmeinformationen zu sehen – was meist in bester Absicht geschieht. Dabei wird ein sehr großer Teil des Quellcodes mit try/catch-Blöcken zugepflastert, um in Letzterem die Ausnahmeinformationen in eine Log-Datei zu schreiben. Wird nun aber eine Methode C() von einer Methode B() aufgerufen, die wiederum von einer Methode A() aufgerufen wird, so wird die Ausnahme in jeder Methode in der Log-Datei protokolliert, das heißt, die Exception wird drei Mal mit unterschiedlichen Stack-Traces in die Log-Datei geschrieben. Je nachdem, wie lang die Aufrufkette der Methoden ist, erschwert dies die Auswertung der Log-Datei massiv. Dabei sollte eine Ausnahme nur in folgenden Fällen protokolliert werden:
  • Die Ausnahme wurde behandelt (Log-Stufe Warning).
  • Die Ausnahme verlässt die Anwendungsgrenze, zum Beispiel weil dem Nutzer ein Fehlerdialog angezeigt wird (Log-Stufe Error).
  • Die Anwendung stürzt wegen einer unbehandelten Ausnahme ab (Log-Stufe Fatal).
Um letzteren Fall einfach implementieren zu können, bietet die Klasse AppDomain das Ereignis UnhandledException an, das wie hier verwendet werden kann:
class Program 
{ 
  static void Main() 
  { 
    AppDomain.CurrentDomain.UnhandledException
      += LogUnhandledException; 
    App.Run(); 
  } 
  private static void LogUnhandledException(
    object sender, UnhandledExceptionEventArgs e) 
  { 
    File.WriteAllText("log.txt", e.ToString()); 
  } 
} 

Anti-Pattern 4: Ausnahme neu erzeugen

Wenn eine Ausnahme in einem catch-Block abgefangen wurde, lassen sich dort geeignete Maßnahmen einleiten. Manchmal kann eine Gegenmaßnahme die Ausnahme kompensieren; oft aber muss am Ende die Ausnahme an den Aufrufer der Methode weitergegeben werden. Dazu bietet C# mehrere Möglichkeiten, wie das folgende Codebeispiel zeigt:
catch (ArgumentNullException e) 
{ 
  throw e; // Falsch! 
} 
catch (ArgumentOutOfRangeException e) 
{ 
  throw; // Richtig! 
} 
Die als richtig gekennzeichnete Weiterleitung (throw;) gibt das unveränderte Ausnahmeobjekt an den Aufrufer weiter. Die als falsch gekennzeichnete Variante leitet die Ausnahme zwar ebenfalls weiter, jedoch wird der Call-Stack der Ausnahme verändert, denn nun zeigt dieser nicht mehr auf die Stelle, an der die Ausnahme ursprünglich ausgelöst wurde, sondern auf die Stelle throw e; – das Auffinden des Fehlers wird also wesentlich schwieriger.Da dieses Verhalten in der Praxis so gut wie nie gewünscht wird, zeigen Tools wie ReSharper oder CodeRush dieses als Warnung an, nicht jedoch als Fehler. Da besonders Einsteiger diesen Fehler oft machen – und es bei Unachtsamkeit auch einem erfahrenen Entwickler passieren kann –, rate ich hier ebenfalls, durch ReSharper oder CodeRush den Build fehlschlagen zu lassen.

Anti-Pattern 5: Null-Referenz in finally

Der finally-Block ist für Arbeitsschritte gedacht, die auf jeden Fall erfolgen sollen, sowohl im Erfolgs- als auch im Miss­erfolgsfall, etwa Aufräumarbeiten. Dabei ist aber zu berücksichtigen, wo genau eine Ausnahme aufgetreten sein kann. In Listing 1 wird ein FileStream-Objekt durch Aufruf der Methode File.OpenRead() erzeugt und anschließend der Referenz stream zugewiesen. Sollte die Ausnahme beim Aufruf dieser Methode ausgelöst werden, so ist die Referenz natürlich leer, hat also den Wert null. Wenn der Stream nun im finally-Block geschlossen werden soll, so muss vor dem Zugriff überprüft werden, ob die Referenz tatsächlich nicht null ist, sondern ihr ein Objekt zugewiesen wurde. Seit C# 6.0 gibt es dazu mit dem Null-Conditional-Operator [4] eine sehr elegante Lösung, ohne eine Verzweigung einbauen zu müssen.

Anti-Pattern 6: Unvollständige eigene Exceptions

Wenn Sie eine sorgfältige Ausnahmebehandlung umsetzen möchten, werden Sie nicht um die Implementierung von ­eigenen Ausnahmen herumkommen. Wichtig ist dabei, dass eigene Ausnahmen einige sehr wichtige Informationen an die Basis-Exception über verschiedene Konstruktoren weiterleiten und darüber hinaus versioniert serialisierbar sind, damit Ausnahmen sich auch über Prozessgrenzen hinweg übertragen lassen. Daher sollte eine Ausnahme folgende Eigenschaften haben:
  • den Konstruktor ctor()
  • den Konstruktor ctor(string msg) : base(msg)
  • den Konstruktor ctor(string msg, Exception ie) : base(msg, ie)
  • einen Serialisierungskonstruktor
  • ein [Serializable]-Attribut
Um Zeit zu sparen und damit nicht aus Versehen einer der angesprochenen Punkte verloren geht, bietet sich die Verwendung des in Visual Studio integrierten Code-Snippets für das Erstellen einer eigenen Exception an; dieses erscheint in IntelliSense nach Eingabe der Zeichenkette „exc“ automatisch und kann mit Drücken der [Enter]-Taste ausgeführt werden.

Fazit

Das Sprachmittel try/catch/finally in C# ist schnell erklärt und jeder Entwickler sollte es beherrschen. Das Problem ist jedoch, dass es in zahlreichen Antimustern verwendet werden kann. Diese Episode von Davids Deep Dive­ hat die wichtigsten davon gezeigt, die es zu vermeiden gilt.Eines wurde hier aber noch nicht berücksichtigt. Dieses Anti-Pattern wirkt sich sehr schlecht auf die Architektur einer­ Anwendung aus, da es durch eine Ausnahme zu einer Verletzung eines Kontrakts und des Geheimnisprinzips führt. Da dies jedoch eine ganze Episode füllen würde, wird es auf die nächste Ausgabe verschoben. Bis dahin wünsche ich Ihnen viel Spaß bei der Behandlung von Ausnahmen!

Fussnoten

  1. David Tielke, Was is’n das genau? Exceptions, Teil 2, ­dotnetpro 2/2019, Seite 36 ff., http://www.dotnetpro.de/A1902DDD
  2. Ausnahmen und Ausnahmebehandlung (C#-Programmierhandbuch), http://www.dotnetpro.de/SL1903DDD1
  3. Peter Ritchie, Performance implications of try/catch/finally, http://www.dotnetpro.de/SL1903DDD2
  4. David Tielke, Webcast „C# 6.0 – Teil 5: Null-Conditional-Operator“, http://www.dotnetpro.de/SL1903DDD3

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige