Je vais répondre du point de vue de C ++. Je suis à peu près sûr que tous les concepts de base sont transférables en C #.
On dirait que votre style préféré est "jetez toujours des exceptions":
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
Cela peut poser problème pour le code C ++, car la gestion des exceptions est lourde : le cas d'échec est exécuté lentement, l'allocation de mémoire par le cas d'échec (qui parfois n'est même pas disponible) et rend généralement les choses moins prévisibles. La lourdeur d'EH est l'une des raisons pour lesquelles vous entendez des gens dire des choses telles que "n'utilisez pas d'exceptions pour contrôler le flux".
Ainsi, certaines bibliothèques (telles que <filesystem>
) utilisent ce que C ++ appelle une "double API" ou ce que C # appelle le Try-Parse
modèle (merci Peter pour le conseil!)
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
}
Vous pouvez voir tout de suite le problème des "doubles API": beaucoup de duplication de code, aucune indication pour les utilisateurs quant à savoir quelle API est la "bonne" à utiliser, et l'utilisateur doit faire un choix difficile entre les messages d'erreur utiles ( CalculateArea
) et speed ( TryCalculateArea
) parce que la version la plus rapide prend notre "negative side lengths"
exception utile et l'aplatit en une inutile false
- "quelque chose s'est mal passé, ne me demandez pas quoi ou où". (Certaines API doubles utilisent un type d'erreur plus expressif, tel que int errno
ou C ++ std::error_code
, mais cela ne vous dit toujours pas où l'erreur s'est produite, mais simplement qu'elle s'est produite quelque part.)
Si vous ne pouvez pas décider de la manière dont votre code doit se comporter, vous pouvez toujours lancer la décision à l'appelant!
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; });
C’est essentiellement ce que fait votre collègue. sauf qu'il factorise le "gestionnaire d'erreur" dans une variable 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);
Déplacer des paramètres importants de paramètres de fonction explicites vers un état global est presque toujours une mauvaise idée. Je ne le recommande pas. (Le fait qu'il ne s'agisse pas d' un état global dans votre cas, mais simplement d' un État membre au niveau de l' instance atténue légèrement le problème, mais pas beaucoup.)
De plus, votre collègue limite inutilement le nombre de comportements possibles de gestion des erreurs. Plutôt que de permettre une erreur de traitement des erreurs, il en a décidé deux:
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);
C’est probablement le «mauvais côté» de l’une de ces stratégies possibles. Vous avez retiré toute la flexibilité de l'utilisateur final en le forçant à utiliser l'un de vos deux rappels de gestion des erreurs; et vous avez tous les problèmes d'état global partagé; et vous payez toujours pour cette branche conditionnelle partout.
Enfin, une solution commune en C ++ (ou n’importe quel langage avec compilation conditionnelle) consisterait à obliger l’utilisateur à prendre la décision pour l’ensemble du programme, globalement, au moment de la compilation, afin que le chemin de code non utilisé puisse être entièrement optimisé:
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 exemple de quelque chose qui fonctionne de cette façon est la assert
macro en C et C ++, qui conditionne son comportement sur la macro du préprocesseur NDEBUG
.