Warum ich „as“ nicht mag aber Exceptions liebe

Heute widme ich mich mal wieder zwei Bestandteilen von .Net die es schon sehr lange gibt und deren Nutzung als Grundlagenwissen zu verstehen sind. Da mir recht häufig der „Missbrauch“ dieser Dinge unterkommt, will ich hier die Vorteile und Nachteile etwas genauer beleuchten. Dabei geht es mir zunächst um das Schlüsselwort as und im Weiteren um Exceptions allgemein.

as gibt es seit .Net 2.0 und es ermöglicht das Casten von Objekten auf einen bestimmten Typ. Ein Vorteil dabei ist, dass ein solcher Cast wesentlich lesbarer ist, als bei der allgemeinen Methode. Ein weiterer vermeintlicher Vorteil besteht darin, dass keine Exception geschmissen wird wenn der Cast „missglückt“.

Nachfolgender Code zeigt kurz beide Varianten. Am Ende der Ausführung ist str1 null und str2 gar nichts, denn an dieser Stelle fliegt eine InvalidCastException und beendet die Applikation.

object obj = new object();

string str1 = obj as string;
string str2 = (string) obj;

Laut MSDN, kann man das as Schlüsselwort in unserem Beispiel auch mit folgendem Konstrukt gleichsetzen. Es wird also überprüft, ob ein Objekt tatsächlich vom angegebenen Typ ist. Ist dem so, erfolgt ein Cast ansonsten wird null zurückgegeben.

string str1 = obj is string ? (string) obj : (string) null;

Nun gut, wenn es so wahnsinnig toll ist warum mag ich „as“ dann nicht? Eben genau wegen diesen vermeintlichen Vorteilen und eben weil es genau wegen diesen in meinen Augen von einigen Leuten inflationär eingesetzt wird.

Zugegeben über die Lesbarkeit von Code kann man sich viel und lange streiten. Das Nichtwerfen von Exceptions als Vorteil zu deklarieren zeugt für mich aber von einer falschen Arbeitseinstellung. Denn wann immer ein Fehler geschieht der im Produktivcode nicht geschehen darf ist eine Exception zu werfen!

Wie sollen wir sonst darauf aufmerksam werden, dass etwas nicht stimmt? In meinen Augen müssen sich einige Entwickler davon verabschieden, Exceptions als Teufelszeug anzusehen nur weil sie uns die eigenen Fehler vorhalten. Denn genau das ist ihre Aufgabe.

Sie sollen uns auf Bugs aufmerksam machen, bevor diese in ungünstigen Situationen (also zum Beispiel beim Kunden) auftreten und sie sollen uns helfen Fehlsituationen leichter zu lokalisieren. Das dabei ein gewisses Maß bewart werden muss und ab jetzt nicht wild mit Exceptions geschmissen werden darf, versteht sich von selbst. Gänzlich auf sie zu verzichten oder sie in jedem Fall mit Try-Catch mundtot zu machen ist aber einfach falsch.

Nachfolgender Code zeigt dies recht deutlich und entspricht dem Auslöser für dieses Post. Dabei muss man wissen, dass die Klasse B von A abgeleitet ist.

public void Foo()
{
    A a = new A();

     // ...

     DoSomething(a as B);
}

public void DoSomething(B b)
{
    if (b == null)
    {
        throw new ArgumentNullException("b");
    }

     // ...
}

Ungeachtet dessen, dass wir ein A erstellen und es einer Methode übergeben die ein B verlangt, wird der Code kompiliert. Bei der Ausführung erhalten wir jedoch eine ArgumentNullException. Diese Exception ist berechtigt immerhin sollten public Methoden immer prüfen ob ihre Eingangsparameter ungleich null sind. Ansonsten fliegt an späterer Stelle eine NullReferenceException und dann muss erst lange gesucht werden warum die Referenz denn nun keinen Wert hat.

Die Problematik der verschleppten Informationen hat man im Beispiel aber bereits, da ein null-Wert an die Methode übergeben wird statt eine InvalidCastException zu werfen. Durch die Nutzung von as wird die Aufmerksamkeit also weg vom falschen Cast, als Wurzel des Problems, hin zum falschen Parameter gelenkt. Man sucht demnach in der Methode DoSomething nach einem Fehler obwohl jener viel früher aufgetreten ist.

Noch einmal deutlich: Hätte man hier einen echten Cast verwendet, wäre eine InvalidCastException geflogen, man hätte nur in den Stacktrace jener schauen müssen und sofort den Ausgangspunkt des Fehlers gefunden. Das wiederum hätte weniger Zeit gekostet, vor allem wenn man den Fehler nur in einem Log und nicht während einer Debugsession findet.

Aufgrund der NullReferenceException-Problematik habe ich as mittlerweile fast gänzlich aus meinem C#-Wortschatz getilgt. Sollte man sicher gehen wollen, dass kein unzulässiger Cast geschieht, kann man immerhin auch is verwenden. Aber auch da kann man Mist bauen, wie folgendes Beispiel zeigt:

public bool DoSomething(A a)
{
    bool returnValue = false;

    if (a is B)
    {
        B b = (B)a;
        // ...

        returnValue = true;
    }

    return returnValue;
}

Dies basiert auf realem Code der mir Tränen in die Augen trieb als ich ihn sah. Denn hier wird jeglicher Fehler verschleppt. Es gibt Situationen in denen es sich lohnt per Returnwert über den Erfolg oder Misserfolg einer Aktion zu informieren ohne gleich eine Exception zu werfen (siehe hier).

Woher soll aber die aufrufende Methode in diesem Fall wissen, dass sie ein false kassiert weil der Cast nicht möglich ist? Wie soll das false behandelt werden? Welche Meldung zeigt man ggf. dem Nutzer an? Im schlimmsten Fall wird hier der Rückgaberwert nicht geprüft, der Fehler bleibt somit unentdeckt und kann sich fortpflanzen, was dann in irgend welchem wirren Verhalten endet welches niemand mehr nachvollziehen kann.

Im Beispiel, kann man sich das aber getrost schenken wenn man als Parameter gleich den richtigen Typ verlangt. Sollte man verschiedene Casts in der Methode haben, kann man evtl. auch nachfolgendes probieren. Wobei C und B wieder von A abgeleitet sind.

public void DoSomething(A a)
{
    if (a is B)
    {
        // ...
    }
    else if (a is C)
    {
        // ...
    }
    else
    {
        throw new ArgumentException("not supported class type "+a.GetType());
    }
}


Es ist also zunächst immer wichtig die Rahmenbedingungen einer Aktion zu prüfen und eine Exception nur dann auszulösen wenn ein Fehlerfall eintritt durch den jene Aktion nicht kontrolliert beendet werden kann. „Nicht kontrolliert“ heißt demnach: Auf eine vom Programmierer unvorhergesehene Weise. Denn dadurch kann die Richtigkeit des Ergebnisses jener Aktion nicht gewährleistet werden.

17 Kommentare

  1. Die Diskussion ist m.E. so nicht ganz richtig.
    Ein Entwickler, der eine Exception bewusst auslöst, hat seine Programmieraufgabe nicht richtig gelöst. Exceptions habe ich bestensfalls nur da, wo ich es nicht 100% ausschließen kann, dass eine Ausnahme auftritt und dann in der Regel nur über einen try catch Block. Dennoch feuere ich die Exception nicht, sondern protokolliere sie.
    „as“ macht genau das, was es soll. Ich hätte mir allerdings gewünscht, dass eine neue Instanz statt null zurückgegeben wird. So muss ich noch zusätzlich auf null prüfen.

  2. Wahrscheinlich ist es aus dem Text nicht ganz ersichtlich, aber Exceptions sollten meiner Meinung nach im Produktivcode auch nicht mehr unbehandelt ausgelöst werden.

    In letzter Instanz muss immer ein Handler stehen. Beispiel wäre hier eine TimeOutException eines WebRequests. Dies kann man nicht verindern, muss man aber auswerten um dem Nutzer die entsprechende Fehlermeldung zu präsentieren.

    Es ist wie beim Zugriff auf eine Datei. Es gibt Entwickler, die einfach ein Try-Catch um die Open-Methode bauen und dem Nutzer dann sagen: „geht nicht, das ist passiert [ExceptionMessage]“. Eigentlich müssten sie aber vorher prüfen ob die Datei vorhanden ist, ob sie geöffnet werden kann und dem Nutzer dann, ohne ausgelöste Exception, eine entsprechende Mitteilung präsentieren. Alternativ könnte man auch die Exceptions fangen und je nach Art eine andere Fehlermeldung bringen, aber auch das empfinde ich als etwas unsauber.

    Ich werde bei Gelegenheit mal noch einen Post zu gutem Exception-Handling als solches schreiben. Nach 3 Stunden schreiben an diesem hier, war dann irgend wann die Luft raus 😉

  3. Ehrlich gesagt muss ich mich René anschließen, dass die Diskussion am eigentlichen Thema vorbeigeht.

    Natürlich ist das Beispiel mit dem as im Methodenaufruf merkwürdig und verschleppt den Fehler. Das liegt aber nicht an as an sich, sondern an einer falschen Nutzung dessen.

    Es gibt durchaus Szenarien, in denen as ein sehr nützliches Schlüsselwort ist. Beispiel:

    Du hast zwei Interfaces, IPrintable und ISavable, und eine Reihe von Dateiklassen, die keins, eins oder beide Interfaces implementieren – je nachdem, was mit der Datei eben möglich ist.

    Nun hast Du eine Liste von Dateien und willst diese ausdrucken. Du durchläufst also in einer foreach-Schleife die Liste von FileBase-Objekten, und castest jedes mit as nach IPrintable. Wenn dabei null herauskommt, überspringst Du das aktuelle Objekt, ansonsten rufst Du die Print-Methode auf.

    Quasi so:

    foreach(FileBase file in files)
    {
        IPrintable printableFile = file as IPrintable;
    
        if(printableFile == null)
        {
            continue;
        }
    
        printableFile.Print();
    }
    

    Hier jedes Mal eine Exception zu werfen, zu fangen und zu behandeln, hielte ich für sehr schlecht.

    Insofern: as ist nicht „böse“, man muss aber wissen, wie und wo man es einsetzt.

    1. Was hindert dich in deinem Beispiel daran is zu verwenden? Dann könntest du auch folgendes schreiben:

      foreach(FileBase file in files)
      {
         if(file is IPrintable)
         {
            ((IPrintable)file).Print();
         }
      }
      

      Dann spart man sich auch den Sprung durch continue. Eine Exception muss in dem Fall nicht geschmissen werden.

      Alternativ kann man hier auch as für den Cast nutzen, dass sieht dann tatsächlich schöner aus und die Gefahr einer NullReferenceException besteht nicht mehr.

      1. Der scheint hier in den Kommtaren keine spitzen klammern zuzulassen. sollte heissen files.OfTypeSPITZEKLAMMER IPrintable SPITZEKLAMMER().FirstOrDefault((p)=>{p.Print();return false;});

  4. PS: Und übrigens, Dein Beispiel mit der Prüfung, ob eine Datei existiert, bevor man sie öffnet, schützt vor Exceptions nicht.

    Wird die Datei nämlich zwischen der (erfolgreichen) Prüfung und dem Öffnen gelöscht, beispielsweise von einem anderen Thread oder Prozess, fliegt trotzdem eine Exception.

    1. In dem Fall stimmt das, aber dass ist dann auch ein Fehler der vom Programmierer nicht vorher gesehen werden kann. Das in so einem fall ein Try-Catch benötigt wird ist richtig. Aber wenn du vor dem öffnen prüfst ob eine Datei vorhanden ist und den Nutzer informierst falls sie es nicht ist kannst du ihm eine andere Fehlermedung („Datei existiert nicht“) bringen als wenn sie mittendrin verschwindet („Dateizugriff während des Schreibens enzogen.“). Du bekommst also eine feinere Granularität deiner Fehlermeldung wodurch die Problemlösung vereinfacht wird.

  5. Hi,

    schicker Blogeintrag…

    @ Rene, dass „as“ null zurückgibt is schon gut so, sonst könnteste manchmal gar nich wissen, ob der cast nun geklappt hat oder nich. wenn du ne neue instanz willst, dann mach doch einfach A a = obj as A ?? new A(); so hastes kurz und knapp…

    @ Hendrik,
    ich habe ein winziges Problem mit diesem Satz: „Denn wann immer ein Fehler geschieht der im Produktivcode nicht geschehen darf ist eine Exception zu werfen!“. Was ist, wenn der fehlgeschlagene Cast gar kein Fehler ist? Ich migriere aktuell bei einem Kunden alten code, der aber trotzdem noch mit anderem alten sehr unschönen Code zusammenarbeiten muss (auch auf COM-Seite). so habe ich es öfter, dass meine Methoden ein object bekommen und ich gucken muss, isses ein int, dann mach das, ist es ein string, dann mache das und das. ich nutze somit häufig xxx.TryParse, um extra jegliche Exceptions zu vermeiden. Und Aufgrund historischer Entwicklungen kann ich noch nicht mal ne Exception werfen, wenn gar keines dieser Typen funktioniert. Dass das aber mistig is, da sind wir uns alle einig.

    Was ich also eigentlich sagen will, was ein „Fehler“ ist, das ist immer situationsabhängig, und was eine „Ausnahmesituation“ ist, leider auch…

    Ich stimme dir aber zu, dass man versuchen sollte, durch Code jegliche Exceptions zu vermeiden (z.b. Open mit vorher Exists)…

    1. “Denn wann immer ein Fehler geschieht der im Produktivcode nicht geschehen darf ist eine Exception zu werfen!”.

      Sobald du weißt, dass ein Fehlverhalten deiner seits (falsche Cast, falscher Dateizugriff, falscher Parser usw.) geschehen kann, kannst du im vornherein prüfen ob du die Aktion wirklich ausführst. In dem du beispielsweise prüfst ob der Typ tatsächlich dem zu castenden entspricht, die Datei überhaupt vorhanden ist oder in dem du TryParse statt Parse verwendest.

      Die Aussage, sollte nicht implizieren, dass in jedem Fall eine Exception zu werfen ist. Es geht mir mehr darum, dass man mit Augenmaß an mögliche Fehlersituationen geht und deren Rahmenbedingungen prüft. Folgender Code bringt niemandem etwas, wird aber auch gern eingesetzt.

      Try
      {
         File.Open("bla");
      }
      catch(Exception ex)
      {
         MessageBox.Open("Fehler! " + ex.Message);
      }
      
  6. > Ein Entwickler, der eine Exception bewusst auslöst,
    > hat seine Programmieraufgabe nicht richtig gelöst

    Ich würde mich dem Post von Hendrik vollständig anschließen. Wenn ich eine Situation erkenne, die nicht sein dürfte, nützt mir im Produktivcode ein Loggen nichts. Diese Anwendung ist bei Kunden, zu denen ich oft keinen Kontakt habe und ich muss sicherstellen, dass kein Code läuft, der nicht laufen sollte. Ich kassiere lieber einen Anruf wegen einer Exception, als einen Anruf wegen falscher Ergebnisse.

    1. @Mario:
      >Diese Anwendung ist bei Kunden, zu denen
      >ich oft keinen Kontakt habe

      Das kann ich nicht teilen, denn das Loggen von Exceptions ist gerade in der Einführungsphase wichtig. Ist der Entwicklungsprozess im Idealfall abgebildet, mit allem Drum und Dran – dann brauchst es eh nicht. Das ist aber eher seltener der Fall.
      Wenn Du dann keinen Kontakt zum Kunden hast, läuft was schief im Entwicklungsprozess.

      Beispiel:
      Ich erlebe es regelmäßig durch unsere Testabteilung, die die Software ganz klar anders testet (bedient), als von der Entwicklung erwartet wird. Oft werden im Rahmen der Tests Missverständnisse bei der Interpretation der fachlichen Anforderungen deutlich. 98% aller protokollierter Exceptions finden sich in der Testphase. Während der Testphase werten wir selbständig die Logs aus.

      Der Kunde hat keine Exception angezeigt zu bekommen – es ist und bleibt Aufgabe des Entwicklers, hierauf zu reagieren. Dafür sind Protokollierungen unerlässlich. Was nützt dem Kunden eine Exception? Bei mir hinterlässt das den Eindruck: „du wusstest, dass ein Fehler auftritt und reagierst nicht (stellst ihn nicht ab)“

      1. Der Kunde hat keine Exception angezeigt zu bekommen

        Da stimme ich dir voll und ganz zu. Während der Entwicklung reiche ich die Exctions weitestgehend nach oben durch. Im Produktiveinsatz werden sie vom Exceptionhandler aber nur noch ins Log geschrieben bzw. davor noch eine entsprechende Fehlermeldung an den Nutzer weiter gereicht. Im normalen Fall wird dann nur die aktuelle Aktion abgebrochen bzw. kontrolliert zu Ende gebracht.

        98% aller protokollierter Exceptions finden sich in der Testphase.

        Auch hier, volle Zustimmung.

  7. Das „as“ Schlüsselwort ist nichts böses. Sondern teilweise sehr sinnvoll.

    Das Beispiel war doch:

    foreach(FileBase file in files)
    {
       if(file is IPrintable)
       {
          ((IPrintable)file).Print();
       }
    }
    

    Besser lesbar fände ich hier:

    foreach(FileBase file in files)
    {
       if (file == null)
          continue;
    
       var printable = file as IPrintable;
       if(printable != null)
       {
          printable.Print();
          continue;
       }
    
       logl.LogWarning ("Typ " + file.GetType().Name + " kann nicht gedruckt werden.");
    }
    

    Wenn das Ding nicht Printable ist, wird nichts gedruckt. Dann gibt es einen freundlichen Anfruf vom Kunden, der sich vielleicht darüber beschwert, dass der Druck nicht funktioniert. Ich lasse mir das Logfile zusenden und sehe sofort das Problem.

    Bei einer Exception kann, je nachdem wo sie gefangen wird, Daten verloren gehen oder ähnliches. Das kommt meistens schlechter an, als ein nicht ausgedrucktes Formular.

  8. @Rene
    > Wenn Du dann keinen Kontakt zum Kunden hast,
    > läuft was schief im Entwicklungsprozess.
    Wir entwickeln Software für Kunden, mit denen wir im Freigabeprozess natürlich Kontakt haben. Nach der Freigabe wird diese als Verkaufshilfe teilweise durch diese Kunden weiterverschenkt, in Größenordnung. Da ist kein Kontakt mehr machbar und seitens der Kunden-Kunden auch nicht gewünscht.

    Situationen, die ich nicht sein dürfen, produzieren bei mir Exceptions. Das finde ich immer dann in Ordnung, wenn ich einfach nicht mehr weiter rechnen kann. Das Ergebnis darf nie unzuverlässig werden.

    Exceptions, die nicht sein dürfen, erzeugen mir sogar Vorteile: Der Kunde kann mit dem Ergebnis so oder so nichts anfangen. Ich muss den Prozess komplett abbrechen. Die Exception-Meldung vom Net-Framework liefert viele gute Infos, die mir helfen.

    „Früher“ haben wir die Meldungen mit Screenshot automatisch in einer schönen Fehlermail vorbereitet, das Problem berichtet und einen Neustart empfohlen. Ergebnis waren Fehlermails, die nicht eine Zeile Text enthielten oder Kontaktmöglichkeiten ausser der Absender-Email… Auch haben wir nie erfahren, ob der Anwender überhaupt will, dass wir was unternehmen.

    Bei Exceptions melden sich alle die Leute, die Hilfe wollen und liefern noch einen Beitrag zur Fehlerbereinigung.

  9. Hallo zusammen,

    ich bin der Überzeugung, dass die ganze Diskussion viel zu weit oben aufgehängt ist. Ich denke, dass es immer in der Verantwortung des Entwicklers liegt, eine Anforderung umzusetzen. Ob er das mit dem Mittel mit „as“ oder „is“ oder dergleichen realisiert ist nebensächlich.

    Für mich wäre es nur wichtig folgendes festzuhalten (und ich glaub da sind wir alle derselben Meinung):

    1. Programmlogik soll nicht über Exceptions gesteuert werden
    2. Der Endbenutzer darf keine unhandled Exception zu sehen bekommen

    Und mit Programmlogik meine ich jetzt nicht das Abfangen von Fehlern mit anschließendem Fehlerhandling.

    Grüße
    Timo

  10. Ich habe die Hervorhebung im Text jetzt noch einmal geändert, ohne den Text geändert zu haben. Evtl. klärt sich dadurch das entstandene Missverständnis.

    Exceptions sind auch meiner Meinung nach Ausnahmen. Sie sollten nur dann eine Aktion abbrechen wenn deren fehlerfreies Abarbeiten nicht mehr gewährleistet ist. Die Beispiele aus der Diskussion hier in den Kommentaren sind dafür eher ungeeignet.

    Ich denke zum Beispiel an das Mapping von Daten aus einem internen Datenmodell auf ein externes, welches zum Beispiel zum Speichern in einer DB genutzt wird. Wenn ich dort den falschen Mapper verwende bzw. der Mapper den falschen Datentyp erhält kann er die Aktion nicht ordnungsgemäß beenden.

    Loggen wir nun aber den Fehler nur, werden wir erst sehr später oder vielleicht garnichtt erfahren, dass die Aktion nicht ausgeführt wird. Andererseits ist ein Mapper auch keine Logik die Zugang zur GUI haben sollte und dem Nutzer demnach auch keine Fehlermeldung anzeigen kann. Was bleibt also übrig?

    Eine Exception welche von der darüber liegenden Logik behandelt werden sollte. Diese Behandlung sieht im Debugbuild bspw. eine Ausschrift von Stacktrace und Co. in der GUI vor, beim Release aber nur noch eine allgemeine Fehlermeldung und ein log in der entsprechenden Datei.

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

14 − eins =

Bitte folgende Aufgabe lösen um fortzufahren

Wieviel ist 4 + 14 ?
Please leave these two fields as-is: