Contestaré desde el punto de vista de C ++. Estoy bastante seguro de que todos los conceptos básicos son transferibles a C #.
Parece que su estilo preferido es "siempre lanzar excepciones":
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
Esto puede ser un problema para el código C ++ porque el manejo de excepciones es pesado : hace que el caso de falla se ejecute lentamente y hace que el caso de falla asigne memoria (que a veces ni siquiera está disponible), y generalmente hace que las cosas sean menos predecibles. El peso pesado de EH es una de las razones por las que escuchas a la gente decir cosas como "No uses excepciones para controlar el flujo".
Entonces, algunas bibliotecas (como <filesystem>
) usan lo que C ++ llama una "API dual", o lo que C # llama el Try-Parse
patrón (¡gracias Peter por el consejo!)
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
}
Puede ver el problema con las "API duales" de inmediato: mucha duplicación de código, sin orientación para los usuarios sobre qué API es la "correcta" para usar, y el usuario debe tomar una decisión difícil entre mensajes de error útiles ( CalculateArea
) y speed ( TryCalculateArea
) porque la versión más rápida toma nuestra útil "negative side lengths"
excepción y la aplana en un inútil false
: "algo salió mal, no me preguntes qué o dónde". (Algunas API duales utilizan un tipo de error más expresivo, como int errno
o C ++ 's std::error_code
, pero que todavía no le dice dónde se produjo el error - sólo que lo hizo aparecer en alguna parte.)
Si no puede decidir cómo debe comportarse su código, ¡siempre puede pasar la decisión a la persona que llama!
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; });
Esto es esencialmente lo que está haciendo su compañero de trabajo; excepto que está factorizando el "controlador de errores" en una variable global:
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);
Mover parámetros importantes de parámetros de funciones explícitas al estado global es casi siempre una mala idea. No lo recomiendo. (El hecho de que no sea un estado global en su caso, sino simplemente un estado miembro de toda la instancia mitiga un poco la maldad, pero no mucho).
Además, su compañero de trabajo está limitando innecesariamente el número de posibles comportamientos de manejo de errores. En lugar de permitir cualquier lambda de manejo de errores, decidió solo dos:
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);
Este es probablemente el "punto amargo" de cualquiera de estas posibles estrategias. Le ha quitado toda la flexibilidad al usuario final al obligarlo a usar una de sus dos devoluciones de llamada para el manejo de errores; y tienes todos los problemas del estado global compartido; y todavía estás pagando por esa rama condicional en todas partes.
Finalmente, una solución común en C ++ (o cualquier lenguaje con compilación condicional) sería forzar al usuario a tomar la decisión de todo su programa, globalmente, en tiempo de compilación, para que la ruta de código no tomada se pueda optimizar por completo:
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 ejemplo de algo que funciona de esta manera es la assert
macro en C y C ++, que condiciona su comportamiento en la macro del preprocesador NDEBUG
.