Risponderò dal punto di vista del C ++. Sono abbastanza sicuro che tutti i concetti chiave siano trasferibili su C #.
Sembra che il tuo stile preferito sia "getta sempre eccezioni":
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
Questo può essere un problema per il codice C ++ perché la gestione delle eccezioni è pesante : fa sì che il caso di errore venga eseguito lentamente e il caso di errore alloca memoria (che a volte non è nemmeno disponibile) e generalmente rende le cose meno prevedibili. La pesantezza di EH è uno dei motivi per cui senti persone dire cose come "Non usare eccezioni per il flusso di controllo".
Quindi alcune librerie (come <filesystem>
) usano ciò che C ++ chiama una "doppia API" o ciò che C # chiama il Try-Parse
modello (grazie Peter per il suggerimento!)
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
bool TryCalculateArea(int x, int y, int& result) {
if (x < 0 || y < 0) {
return false;
}
result = x * y;
return true;
}
int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
// use a2
}
Puoi vedere subito il problema con le "doppie API": un sacco di duplicazione del codice, nessuna guida per gli utenti su quale API sia quella "giusta" da usare e l'utente deve fare una scelta difficile tra utili messaggi di errore ( CalculateArea
) e speed ( TryCalculateArea
) perché la versione più veloce prende la nostra utile "negative side lengths"
eccezione e la appiattisce in un inutile false
- "qualcosa è andato storto, non chiedermi cosa o dove". (Alcune API doppie utilizzano un tipo di errore più espressivo, come int errno
o C ++ std::error_code
, ma ciò non ti dice ancora dove si è verificato l'errore - solo che si è verificato da qualche parte.)
Se non riesci a decidere come dovrebbe comportarsi il tuo codice, puoi sempre dare una decisione al chiamante!
template<class F>
int CalculateArea(int x, int y, F errorCallback) {
if (x < 0 || y < 0) {
return errorCallback(x, y, "negative side lengths");
}
return x * y;
}
int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });
Questo è essenzialmente ciò che il tuo collega sta facendo; tranne che sta prendendo in considerazione il "gestore degli errori" in una variabile globale:
std::function<int(const char *)> g_errorCallback;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorCallback("negative side lengths");
}
return x * y;
}
g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);
Spostare parametri importanti da parametri di funzioni esplicite nello stato globale è quasi sempre una cattiva idea. Non te lo consiglio. (Il fatto che non sia uno stato globale nel tuo caso, ma semplicemente uno stato membro a livello di istanza mitiga un po 'la cattiveria, ma non molto.)
Inoltre, il collega limita inutilmente il numero di possibili comportamenti di gestione degli errori. Invece di consentire qualsiasi lambda di gestione degli errori, ha deciso solo due:
bool g_errorViaException;
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
return g_errorViaException ? throw Exception("negative side lengths") : 0;
}
return x * y;
}
g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);
Questo è probabilmente il "punto debole" di una di queste possibili strategie. Hai tolto tutta la flessibilità all'utente finale costringendolo a utilizzare esattamente uno dei due callback di gestione degli errori esattamente ; e hai tutti i problemi di stato globale condiviso; e stai ancora pagando per quel ramo condizionale ovunque.
Infine, una soluzione comune in C ++ (o in qualsiasi linguaggio con compilazione condizionale) sarebbe quella di costringere l'utente a prendere la decisione per l'intero programma, a livello globale, al momento della compilazione, in modo che il codepath non preso possa essere completamente ottimizzato:
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
return 0;
#else
throw Exception("negative side lengths");
#endif
}
return x * y;
}
// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);
Un esempio di qualcosa che funziona in questo modo è la assert
macro in C e C ++, che condiziona il suo comportamento sulla macro del preprocessore NDEBUG
.