Sophie: Falsche Parametertypen

Hallo zusammen,

ich überlege gerade, in welcher Weise man die Parameter innerhalb von Funktionen behandeln sollte.
Nach meinen Überlegungen wäre es nur in öffentlich zugänglichen Methoden (--> Revealing Module Pattern) sinnvoll die Parameter zu prüfen, da das mMn der einzige Punkt ist, an dem falsche Parametertypen vorkommen könnten (es steht euch frei mich eines Besseren zu belehren) – ich selbst werde ja schließlich meine Funktionen schon richtig aufrufen. Also:

Sollte man prüfen, ob im Parameter das Richtige drin ist, bevor man anfängt damit rumzuspielen?
z.B.: if (typeof args0 !== 'string') oder if (!(args0 instanceof MyClass))
(Möglicherweise kann mir auch jemand erklären, warum man bei typeof ständig den Identitätsoperator (===) sieht, obwohl ein einfacher Gleichheitsoperator (==) völlig ausreicht? Schließlich gibt typeof immer einen String zurück und case-sensitive sind beide Operatoren ...)

Und wenn was Falsches drin ist, was wäre dann sinnvoll zu tun?
Fehler in die Konsole schreiben, TypeError werfen, ... ?

Oder auch gar nicht prüfen? Nach dem Motto: Wenn nicht das übergeben wird, was erwartet wird, dann knallts eben.

Ob der Inhalt des Parameters überhaupt sinnvoll ist, ist eine andere Geschichte.

Liebe Grüße,
Sophie

  1. Solange alles mein eigener Code ist, würde ich falsche Parametertypen durch Unit-Tests entdecken wollen. Steckst Du in eine Funktion falsche Typen hinein, dürften die Unit-Tests vor die Pumpe fahren. Wenn Du eine Bibliothek schreibst, sieht die Sache natürlich anders aus. Da können Prüfungen in der öffentlichen Schnittstelle durchaus sinnvoll sein. Wenn Du mit dem falschen Typ nicht weitermachen kannst, solltest Du auch eine Exception werfen. Es ist ja schließlich ein Programmierfehler, da darf es gerne laut scheppern. Loggen auf die Konsole ist mit Vorsicht zu genießen, die ist bekanntlich nicht immer verfügbar.

    Da "==" dazu gemacht ist, eine type coercion anzustoßen, und "===" das nicht tut, kann man davon ausgehen, dass "===" eine Spur fixer ist.

    Rolf

    1. Hallo Rolf,

      Solange alles mein eigener Code ist, würde ich falsche Parametertypen durch Unit-Tests entdecken wollen. Steckst Du in eine Funktion falsche Typen hinein, dürften die Unit-Tests vor die Pumpe fahren.

      Ich habe mich gerade ein wenig in Unit-Tests eingelesen und bin dabei unter anderem auf QUnit gestoßen. Aber ich verstehe nicht, wie ich „falsche Parametertypen [damit] entdecken [sollte]“. QUnit ist doch dazu gedacht Codeabschnitte (Funktionen, Units) auf ein erwartetes Ergebnis zu prüfen. So interessant ich Unit-Test auch finde (kannte ich bis gerade eben nicht), ich denke nicht, dass sie mich auf meine Frage hin weiterbringen, oder? Und wie definierst du „mein eigener Code“? »Private Variablen, Funktionen und Sowas«, »Code, den eh kein Anderer zu Gesicht bekommt«, ... ?

      Wenn Du eine Bibliothek schreibst, sieht die Sache natürlich anders aus. Da können Prüfungen in der öffentlichen Schnittstelle durchaus sinnvoll sein. Wenn Du mit dem falschen Typ nicht weitermachen kannst, solltest Du auch eine Exception werfen. Es ist ja schließlich ein Programmierfehler, da darf es gerne laut scheppern.

      Ich denke das ist der Punkt, zu erkennen wann man mit einem falschen Typ nicht weitermachen kann.
      Angenommen meine Funktion erwartet einen Parameter vom Typ Number und ich prüfe mit typeof und jemand kommt ernsthaft auf die Idee new Number(10) als Parameter zu übergeben. Kann mir das egal sein (--> TypeError werfen) und wirklich nur auf Primitives setzen?
      Oder meine Funktion erwartet einen Parameter vom Typ Number und jemand übergibt einen String der a) nur eine Zahl enthält ('42') oder b) eine Zahl am Anfang hat und Nicht-Ziffern danach ('42foo') oder c) (am Anfang) nur aus Nicht-Ziffern besteht ('foo42', 'foo'). Ich könnte nun versuchen, den String mit Number() oder window.parseInt() nach Number umzuwandeln und mit window.isNaN() schauen, ob ich nach der Umwandlung immer noch Mist habe, aber wo endet die Toleranzgrenze?

      Aber man sollte schon selber prüfen und ein TypeError werfen, wenn was nicht stimmt, anstatt stur zu versuchen seine Funktion abzuarbeiten und möglicherweise ein automatisches TypeError in der Konsole zu produzieren, oder?

      Mal nebenbei: Mache ich mir einfach zu viele Gedanken über solche „Kleinigkeiten“ oder darf ich wegen der positiven Bewertung davon ausgehen, dass mein Problem doch gar nicht so doof ist? Gut, doof ist es schon, denn mir zumindest geht es ziemlich auf die Nerven. Ihr wisst schon, wie das gemeint ist...

      Da "==" dazu gemacht ist, eine type coercion anzustoßen, und "===" das nicht tut, kann man davon ausgehen, dass "===" eine Spur fixer ist.

      Okay, danke.

      Liebe Grüße,
      Sophie

      1. Tach!

        Solange alles mein eigener Code ist, würde ich falsche Parametertypen durch Unit-Tests entdecken wollen. Steckst Du in eine Funktion falsche Typen hinein, dürften die Unit-Tests vor die Pumpe fahren.

        Ich habe mich gerade ein wenig in Unit-Tests eingelesen und bin dabei unter anderem auf QUnit gestoßen. Aber ich verstehe nicht, wie ich „falsche Parametertypen [damit] entdecken [sollte]“. QUnit ist doch dazu gedacht Codeabschnitte (Funktionen, Units) auf ein erwartetes Ergebnis zu prüfen.

        Unit-Tests funktionieren nur dann, wenn man sich auf Ergebnisse für alle möglichen Fälle festgelegt hat. Im Gutfall muss das Ergebnis ebenjenes sein, dass man eigentlich haben will. Für den Fehlerfall (oder auch mehrere unterschiedliche) müssen ebenfalls Reaktionen festgelegt sein, und sei es das Werfen einer Exception. Unit-Tests stellen sicher, dass immer eines dieser erwartbaren Ergebnisse entsteht. Voraussetzung ist, dass sie alle möglichen Fälle abdecken. Natürlich können sie keine Fehler in besonderen Grenzsituationen finden, für die man keinen Test geschieben hat. Sowas passiert auch. Aber man kann dafür sorgen, dass beim Bekanntwerden mit dem Nachtragen eines entsprechenden Tests dieser Fall künftig nicht unentdeckt bleibt.

        So interessant ich Unit-Test auch finde (kannte ich bis gerade eben nicht), ich denke nicht, dass sie mich auf meine Frage hin weiterbringen, oder?

        Vermutlich nicht. Was innerhalb einer Funktion geschieht, ist dem Unit-Test egal. Der sieht genauso eine Blackbox, wie sie ein Anwender vor sich hat, der nicht in den Code schaut.

        Und wie definierst du „mein eigener Code“? »Private Variablen, Funktionen und Sowas«, »Code, den eh kein Anderer zu Gesicht bekommt«, ... ?

        Ich nehme an, er meint Code, den man selbst geschrieben und nicht von anderswo bezogen hat. Mit dem zitierten Satz hat er sicher auch von außen geschaut und nicht aus der Sicht des Innenlebens.

        Wenn Du eine Bibliothek schreibst, sieht die Sache natürlich anders aus. Da können Prüfungen in der öffentlichen Schnittstelle durchaus sinnvoll sein. Wenn Du mit dem falschen Typ nicht weitermachen kannst, solltest Du auch eine Exception werfen. Es ist ja schließlich ein Programmierfehler, da darf es gerne laut scheppern. Ich denke das ist der Punkt, zu erkennen wann man mit einem falschen Typ nicht weitermachen kann.

        Ja, und den kann man nicht pauschal und für alle Fälle gültig beantworten. Es gibt solche und solche und obendrein auch noch solche Situationen.

        Angenommen meine Funktion erwartet einen Parameter vom Typ Number und ich prüfe mit typeof und jemand kommt ernsthaft auf die Idee new Number(10) als Parameter zu übergeben. Kann mir das egal sein (--> TypeError werfen) und wirklich nur auf Primitives setzen?

        Garbage in, Garbage out. Das ist eine Strategie. Man definiert und dokumentiert, für welche Fälle welches Ergebnis zu erwarten ist und für die anderen erklärt man undefiniertes Verhalten. Das ist sicher nicht die netteste Strategie, denn sie überlässt dem Verwender die Prüfung auf den gültigen Wertebereich. Andererseits müsste der das sowieso machen, um die von dir geworfene Exception zu vermeiden. Aber er hat bei Exceptions auch die Möglichkeit, einfach nur einen Catch-Handler zu implementieren und kann eine Vorfilterung weglassen. Wie schon im vorigen Absatz erwähnt, kann man nicht pauschal sagen, was die beste Lösung ist. Es läuft hinaus auf ein: Kenne die Möglichkeiten und entscheide dich im konkreten Fall für eine bestimmte.

        Ich könnte nun versuchen, [...] aber wo endet die Toleranzgrenze?

        Die muss man für jeden Fall neu definieren. Da hilft nur Erfahrung, um den Entscheidungsprozess abzukürzen. Auch ein Betrachten aus der Sicht des potentiellen Verwenders kann dabei helfen. Vielen fällt es jedoch schwer, die eigene Gedankenblase zu verlassen und mit anderen Augen auf die Lösung zu schauen.

        Aber man sollte schon selber prüfen und ein TypeError werfen, wenn was nicht stimmt, anstatt stur zu versuchen seine Funktion abzuarbeiten und möglicherweise ein automatisches TypeError in der Konsole zu produzieren, oder?

        Auf Konsolenausgaben kann man im Prinzip gar nicht reagieren. Eine Exception oder ein definiertes Ergebnis im Fehlerfall kann man hingegen sehr leicht auswerten. Die Konsolenausgabe kann man trotzdem noch einfügen. Die Frage ist dann aber, ob die dann nicht mehr Verwirrung beim Endanwender (so dieser die Konsole beobachtet) verursacht, als sie Nutzen bringt. Vor allem, wenn der verwendende Programmierer auf den Fehlerfall ordnungsgemäß reagiert hat.

        Mal nebenbei: Mache ich mir einfach zu viele Gedanken über solche „Kleinigkeiten“ oder darf ich wegen der positiven Bewertung davon ausgehen, dass mein Problem doch gar nicht so doof ist?

        Gerade die Beachtung dieser Kleinigkeiten unterscheidet den guten Programmierer von demjenigen, der nur den Geradeausweg für schönes Wetter runtergeschrieben hat.

        Da "==" dazu gemacht ist, eine type coercion anzustoßen, und "===" das nicht tut, kann man davon ausgehen, dass "===" eine Spur fixer ist.

        Irr-Elefant. Der Unterschied fällt unter die Nichtigkeitengrenze. Die Verständlichkeit des Codes ist in der Regel deutlich höher zu gewichten als solche vermuteten Laufzeitunterschiede[1], besonders bei Sprachen, die per se nicht auf Höchstgeschwindigkeit ausgelegt sind. Es gibt die Verfechter, dass man bei === weniger nachdenken müsse. Arbeitserleichterungen sind ja schön und gut, aber das darf nicht dazu führen, dass man sich damit auf ein trügerisches Ruhekissen bettet.

        dedlfix.


        1. Vermutet, weil je nach Engine und deren Optimierer das Ergebnis auch durchaus anders aussehen kann. ↩︎

        1. Hallo,

          mit den Unit Tests bei eigenem Code meinte ich folgendes: Wenn ich ein Programm schreibe, dann erzeuge ich Unit-Tests für alle öffentlichen Schnittstellen der von mir erstellten Klassen (bzw Funktionen) und befeuere sie so mit Testdaten. Wenn mein Programmcode korrekt ist, wird es bei den daraus folgenden internen Aufrufen nicht zu Typfehlern kommen. Wenn doch, ist zu erwarten, dass der Unit-Test fehlschlägt.

          Wenn ich Unit-Tests für eine Klasse schreibe, die Abhängigkeiten zu einer anderen Klasse hat, dann mocke ich diese Abhängigkeit und prüfe im Unit-Test, ob meine Mocks wie erwartet aufgerufen wurden. Wenn die getestete Klasse falsche Typen übergibt, merke ich das an dieser Stelle. Seinen Code so zu schreiben, dass das überhaupt möglich ist, ist die hohe Kunst des TDD.

          Solange die zuvor erwähnten Klassen und Funktionen so aufgerufen werden, dass ich die Kontrolle über den Aufruf habe (sprich: der Aufruf durch meinen eigenen Code erfolgt), brauche ich keine Typvalidierung. Denn ich unit-teste ja meinen eigenen Code und weiß, dass er keine falschen Typen ausspuckt. Falsche Wertebereiche vielleicht schon, ggf. sind dagegen Prüfungen erforderlich. Das ist eine Designfrage und nicht allgemein zu beantworten.

          Funktionen, die ihre Aufrufparameter auf Typrichtigkeit prüfen müssen, sind solche, die von Stellen aufgerufen werden können, die nicht meiner Kontrolle unterliegen. Also dann, wenn ich von irgendwoher ein JSON Objekt vor die Füße gekübelt bekomme. Oder wenn ich nicht das Hauptprogramm schreibe, sondern eine allgemeine Bibliothek veröffentlichen will wie jQuery, Knockout oder Angular. Zumindest muss ich dann eine Debug-Version bereitstellen, die direkt beim Einstieg in eine öffentliche Schnittstelle genauer prüft und dem Entwickler während der Testphase direkt beim Aufruf "Du Depp" sagt, statt mit den unbrauchbaren Daten weiterzumachen, sie über 17 Stufen weiterzureichen, vielleicht sogar 3x in Closures zu verpacken und sich drei Mausklicks später unvermittelt darüber zu erbrechen. Oder schlimmer noch, den Code im Bunker auszuführen (try-catch, der Exceptions verwirft) und den Nutzer raten lassen, woher in der finsteren Höhle das urk kommt.

          Wenn die Prüfungen dazu führen, dass der Code zu langsam wird, kann man immer noch eine Release-Version mit reduzierten Prüfungen bereitstellen.

          Dein Hinweis zu primitiven und gewrappten Typen ist durchaus interessant. Du hast recht, dass man die bei einer Typprüfung nicht über einen Kamm scheren kann. Aber Du kannst Dir bei Number zumindest damit das Leben erleichtern, dass Du Number als Funktion nutzt. Number(3) (ohne new!) ergibt die primitive 3. Number("3") ergibt AUCH die primitive 3. Number(" 3") ebenfalls. Auch Number(new Number(3)). Number("3a") ergibt dagegen NaN.

          Ein weiterer Grund für Typprüfungen sind Funktionen, die so tun als wären sie überladen. Die schauen sich ihre Parameter an, um herauszufinden, welche Aufrufvariante der Nutzer wohl gemeint haben könnte (jQuery ist ein prima Beispiel für Spekulatius dieser Art). Aber das ist wohl nicht das, wonach Du gefragt hast.

          Zu dedlfixens Irr-Elefant: Ich habe ja nur versucht, einen Grund zu finden, warum man an der Stelle eine Präferenz für === haben könnte. Ob bei einem Typvergleich "==" oder "===" verständlicher ist, tja, keine Ahnung. Ich WEISS, dass ich auf beiden Seiten strings haben werde. Insofern ist's semantisch an dieser Stelle Wurscht und nur die hauchzarte Wurstpelle der Performance blieb für mich als Grund übrig.

          Rolf

  2. Sollte man prüfen, ob im Parameter das Richtige drin ist, bevor man anfängt damit rumzuspielen?
    z.B.: if (typeof args0 !== 'string') oder if (!(args0 instanceof MyClass))

    Vor allem ist es sehr gut, dass du dir Gedanken um Typen machst, sie sind eine wirkungsvolle Hilfe beim Aufbau eines mentalen Modells von Programmcode. Ungeachtet, ob du die Typen jedesmal ausschreibst oder du sie dir nur vorstellst, diesen mentalen Trick kannst du immer anwenden. Er hilft mir zum Beispiel dabei klarere Schnittstellen zu designen.

    Die konkrete Erscheinungsform und Ausprägung des Typsystems ist natürlich auch interessant. Ich empfehle dir, ein paar Alternativen durchzuprobieren. JavaScript ist eine dynamisch typisierte Sprache (das ist nicht ganz korrekt), das heißt dass Typ-Zugehörigkeiten zur Laufzeit ermittelt werden und nicht von einem Compilerschritt im Voraus sichergestellt werden, sowie es zum Beispiel bei Java gemacht wird. Dynamische Typisierung ist im Allgemeinen flexibler, aber wie ich finde, führt diese zusätzliche Flexibilität nicht dazu, dass man bessere Abstraktionen findet – mit anderen Worten ich nutze diese Flexibilität sowieso nicht aus. Für mich sind deshalb statische Typsysteme der bessere Fit. Für JS gibt es zum Beispiel den statischen Typchecker: Flow. Er arbeitet mit Typeinference und kann dir schon vor der Laufzeit deines Programms sagen, ob Typfehler vorhanden sind. Ich persönlich gehe noch einen Schritt weiter und arbeite gerne mit TypeScript - das ist JavaScript gepaart mit einem sehr hochentwickelten statischem Typsystem. Das stärkste Typsystem im JavaScript-Ökosystem hat zur Zeit PureScript zu bieten - eine funktionale Programmiersprache, die genau wie TypeScript zu JavaScript kompiliert. Sie unterstützt sogenannte "Dependent Types", man spricht auch von proof-carrying Code, also Code der seinen eigenen Korrektheitsbeweis im Typsystem mitführt.