Ho incontrato queste domande e risposte diverse volte e volevo contribuire con una risposta più completa. Penso che il modo migliore di pensare a questo sia come restituire errori al chiamante e cosa restituisci.
Come
Esistono 3 modi per restituire informazioni da una funzione:
- Valore di ritorno
- Argomenti fuori sede
- Out of Band, che include goto non locale (setjmp / longjmp), file o variabili con ambito globale, file system ecc.
Valore di ritorno
È possibile restituire solo il valore è un singolo oggetto, tuttavia, può essere un complesso arbitrario. Ecco un esempio di un errore che restituisce la funzione:
enum error hold_my_beer();
Un vantaggio dei valori di ritorno è che consente il concatenamento delle chiamate per una gestione degli errori meno invasiva:
!hold_my_beer() &&
!hold_my_cigarette() &&
!hold_my_pants() ||
abort();
Ciò non riguarda solo la leggibilità, ma può anche consentire l'elaborazione di una matrice di tali puntatori a funzioni in modo uniforme.
Argomenti fuori sede
Puoi restituire di più tramite più di un oggetto tramite argomenti, ma le migliori pratiche suggeriscono di mantenere basso il numero totale di argomenti (diciamo <= 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);
}
Fuori banda
Con setjmp () si definisce un luogo e come si desidera gestire un valore int e si trasferisce il controllo a tale posizione tramite un longjmp (). Vedi l'utilizzo pratico di setjmp e longjmp in C .
Che cosa
- Indicatore
- Codice
- Oggetto
- Richiama
Indicatore
Un indicatore di errore indica solo che esiste un problema, ma nulla sulla natura di tale problema:
struct foo *f = foo_init();
if(!f) {
/// handle the absence of foo
}
Questo è il modo meno potente per una funzione di comunicare lo stato di errore, tuttavia, perfetto se il chiamante non può comunque rispondere all'errore in modo graduato.
Codice
Un codice di errore indica al chiamante la natura del problema e può consentire una risposta adeguata (da quanto sopra). Può essere un valore restituito o simile all'esempio look_ma () sopra un argomento di errore.
Oggetto
Con un oggetto errore, il chiamante può essere informato su problemi complicati arbitrari. Ad esempio, un codice di errore e un messaggio leggibile umano adatto. Può anche informare il chiamante che più cose sono andate male o un errore per articolo durante l'elaborazione di una raccolta:
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]);
}
Invece di pre-allocare l'array di errori, puoi anche (ri) allocarlo dinamicamente secondo necessità, ovviamente.
Richiama
Il callback è il modo più potente per gestire gli errori, in quanto puoi dire alla funzione quale comportamento vorresti vedere accadere quando qualcosa va storto. Un argomento di callback può essere aggiunto a ciascuna funzione o se la personalizzazione è richiesta solo per istanza di una struttura come questa:
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();
Un vantaggio interessante di un callback è che può essere invocato più volte, o in nessun caso in assenza di errori in cui non ci sono spese generali sulla via felice.
Vi è, tuttavia, un'inversione di controllo. Il codice chiamante non sa se il callback è stato invocato. Pertanto, può essere logico utilizzare anche un indicatore.