Ich bin mehrmals auf diese Fragen und Antworten gestoßen und wollte eine umfassendere Antwort liefern. Ich denke, der beste Weg, darüber nachzudenken, ist, wie man Fehler an den Anrufer zurückgibt und was Sie zurückgeben.
Wie
Es gibt drei Möglichkeiten, Informationen von einer Funktion zurückzugeben:
- Rückgabewert
- Out Argument (s)
- Out of Band, einschließlich nicht lokaler goto (setjmp / longjmp), Datei- oder globaler Gültigkeitsbereichsvariablen, Dateisystem usw.
Rückgabewert
Sie können nur den Wert eines einzelnen Objekts zurückgeben, es kann sich jedoch um einen beliebigen Komplex handeln. Hier ist ein Beispiel für eine Fehlerrückgabefunktion:
enum error hold_my_beer();
Ein Vorteil von Rückgabewerten besteht darin, dass Aufrufe für eine weniger aufdringliche Fehlerbehandlung verkettet werden können:
!hold_my_beer() &&
!hold_my_cigarette() &&
!hold_my_pants() ||
abort();
Dies betrifft nicht nur die Lesbarkeit, sondern kann auch die einheitliche Verarbeitung eines Arrays solcher Funktionszeiger ermöglichen.
Out Argument (s)
Sie können mehr über mehr als ein Objekt über Argumente zurückgeben. Es wird jedoch empfohlen, die Gesamtzahl der Argumente niedrig zu halten (z. B. <= 4):
void look_ma(enum error *e, char *what_broke);
enum error e;
look_ma(e);
if(e == FURNITURE) {
reorder(what_broke);
} else if(e == SELF) {
tell_doctor(what_broke);
}
Außerhalb der Bandbreite
Mit setjmp () definieren Sie einen Ort und wie Sie mit einem int-Wert umgehen möchten, und übertragen die Steuerung über longjmp () an diesen Ort. Siehe Praktische Verwendung von setjmp und longjmp in C. .
Was
- Indikator
- Code
- Objekt
- Zurückrufen
Indikator
Eine Fehleranzeige zeigt Ihnen nur an, dass ein Problem vorliegt, aber nichts über die Art des Problems:
struct foo *f = foo_init();
if(!f) {
/// handle the absence of foo
}
Dies ist die am wenigsten leistungsfähige Methode für eine Funktion, um den Fehlerstatus zu kommunizieren. Sie ist jedoch perfekt, wenn der Anrufer ohnehin nicht schrittweise auf den Fehler reagieren kann.
Code
Ein Fehlercode informiert den Anrufer über die Art des Problems und ermöglicht möglicherweise eine geeignete Antwort (von oben). Dies kann ein Rückgabewert sein oder wie das Beispiel look_ma () über einem Fehlerargument.
Objekt
Mit einem Fehlerobjekt kann der Anrufer über beliebig komplizierte Probleme informiert werden. Zum Beispiel ein Fehlercode und eine geeignete lesbare Nachricht. Es kann den Anrufer auch darüber informieren, dass mehrere Fehler aufgetreten sind oder ein Fehler pro Element bei der Verarbeitung einer Sammlung aufgetreten ist:
struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
if(reason[i] == NOT_FOUND) find(friends[i]);
}
Anstatt das Fehlerarray vorab zuzuweisen, können Sie es natürlich auch nach Bedarf dynamisch (neu) zuweisen.
Zurückrufen
Rückruf ist die leistungsstärkste Methode, um Fehler zu behandeln, da Sie der Funktion mitteilen können, welches Verhalten bei einem Fehler auftreten soll. Jeder Funktion kann ein Rückrufargument hinzugefügt werden, oder wenn eine Anpassung nur pro Instanz einer Struktur wie dieser erforderlich ist:
struct foo {
...
void (error_handler)(char *);
};
void default_error_handler(char *message) {
assert(f);
printf("%s", message);
}
void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
assert(f);
f->error_handler = eh;
}
struct foo *foo_init() {
struct foo *f = malloc(sizeof(struct foo));
foo_set_error_handler(f, default_error_handler);
return f;
}
struct foo *f = foo_init();
foo_something();
Ein interessanter Vorteil eines Rückrufs besteht darin, dass er mehrmals oder gar nicht aufgerufen werden kann, wenn keine Fehler vorliegen, bei denen auf dem glücklichen Pfad kein Overhead entsteht.
Es gibt jedoch eine Umkehrung der Kontrolle. Der aufrufende Code weiß nicht, ob der Rückruf aufgerufen wurde. Daher kann es sinnvoll sein, auch einen Indikator zu verwenden.