003
30.06.2003, 11:54 Uhr
ao
(Operator)
|
Es gibt verschiedene "Arten" von Funktionen, und es gibt verschiedene Modelle, wie man ein Ergebnis transportiert:
1. Gruppe: Funktionen, deren Hauptaufgabe es ist, ein Ergebnis zu ermitteln und zurückzugeben. Diese Gruppe teile ich noch in zwei Untergruppen:
1.1. Funktionen, die immer ein sinnvolles Ergebnis haben, z.B.
C++: |
int Add (int a, int b); // zwei ganze Zahlen addieren; Ueberlauf lassen wir mal weg.
|
1.2. Funktionen die auch fehlschlagen können z.B.
C++: |
float log2 (float x); // Zweierlogarithmus, nur fuer x > 0 definiert.
|
Hierzu gehören auch die Funktionen, die Objekte erzeugen (also intern malloc oder new machen), denn das kann auch fehlschlagen.
2. Gruppe: Funktionen, deren Hauptaufgabe es ist, eine Aktion durchzuführen, z.B. Daten irgendwo zu speichern oder zu übertragen. Die Rückgabe eines Statuswerts, der Erfolg oder Misserfolg anzeigt, kann (je nach Zusammenhang) wichtig oder unwichtig sein.
Die Funktionen aus der Gruppe 1.1 würde ich tatsächlich so definieren wie im Beispiel. Hier gibts keinen Grund, sich die Sache kompliziert zu machen.
Bei der Gruppe 1.2 gibt es zwei Ansätze:
A. so wie oben; Das funktioniert allerdings nur, wenn es sogenannte "Magic Values" gibt, mit denen ein Fehlschlagen angezeigt werden kann. Die log-Funktionen der C-Runtime-Library geben in diesem Falle z.B. NaN (Not a Number) zurück. Funktionen wie malloc erledigen das mit NULL.
Das Problem ist, daß die Funktion für den Benutzer ganz einfach aussieht (Argumente reinstecken, Ergebnis abholen und fertig). Erst auf den zweiten Blick merkt man, daß man das Ergebnis vor der Verwendung auf Gültigkeit testen muß; es sei denn, man weiß, daß das Ergebnis gültig sein wird, weil die Argumente gültig waren, aber auch das muß man sicherstellen (oder aus dem Kontext wissen).
Im allgemeinen Fall ist also der Vorteil für den Anwender (bequem aufrufbare Funktion, Ergebnis sofort verwendbar) entfallen; es muß zwischengespeichert und auf Magic Value getestet werden. Und (schlimmer): Was Magic Values sind und was sie bedeuten, ist von Funktion zu Funktion verschieden.
B. Eine Alternative:
C++: |
float log2 (float x, int *pnStatus);
|
Es gelte die Vereinbarung, daß die Gültigkeit des Ergebnisses in der Variablen *pnStatus abgefragt werden kann. Falls das nicht nötig ist, weil der Anwender weiß, daß es gültig sein wird, kann er NULL hereinreichen; die Funktion log2 prüft das und legt in diesem Fall in *pnStatus keinen Statuswert ab.
Das heißt, der Anwender kann, muß aber nicht, den Statuswert abholen und prüfen. Wenn er ihn nicht prüft, ist er von dem Overhead befreit, eine Variable für den Statuswert zu definieren und auszuwerten, und die Funktion ist genauso einfach zu handhaben wie unter A.
Wenn er ihn prüfen will, bekommt er ihn in einem Format, das projektweit oder sogar unternehmensweit standardisiert werden kann. Es kann z.B. festgelegt werden, daß Status == 0 Success bedeutet und Status == 1 OutOfMemory, Status == 2 DivisionByZero, Status == 3 FileNotFound oder was auch immer. Es ist nicht mehr von der Semantik der Funktion abhängig, was gültige Werte sind und was Magic Values, die irgendeinen Fehler anzeigen. D.h. die Auswertung des Statuswerts kann durch Makros o.ä. weitgehend automatisiert werden. Dadurch entfallen viel lästige Tipparbeit und vor allem Fehlerquellen.
Funktionen der Gruppe 2 (Aktion ausführen) würde ich so definieren:
C++: |
int DoSomething ( /* hier alle Ein- und Ausgabeparameter */);
|
wobei der Rückgabewert genau so ein Statuswert ist wie oben. Der Anwender kann sich hier aussuchen, ob er ihn auswertet oder in seinem Programm einfach ignoriert.
Übrigens würde ich den Statuswert nicht als bool zurückgeben, sondern tatsächlich als int. bool sind für mich nur die echten Bool-sche Werte, also Ergebnisse von logischen Ausdrücken (Vergleichen usw.). Statuswerte, die Erfolg oder Misserfolg anzeigen, sind für mich nicht Bool-sch.
Noch eins zu Exceptions: Mach nicht den Fehler, Exceptions als die billige Lösung aller Probleme anzusehen und wild mit irgendwelchen Exceptions um dich zu werfen. Exceptions sind im Kern nichts anderes als eine nette Verklausulierung von "goto NixWieRausHier;". Der Exception-Werfer muß sich Gedanken machen über den Zustand, in dem er sein Objekt verläßt; der Exception-Fänger muß auf alles gefaßt sein. Ein schlechtes Exception-Design kann ein Objekt praktisch unbrauchbar machen.
ao |