我将从C ++的角度回答。我很确定所有核心概念都可以移植到C#中。
听起来您的首选样式是“总是抛出异常”:
int CalculateArea(int x, int y) {
if (x < 0 || y < 0) {
throw Exception("negative side lengths");
}
return x * y;
}
对于C ++代码而言,这可能是一个问题,因为异常处理非常繁重 –它使故障案例运行缓慢,并使故障案例分配内存(有时甚至不可用),并且通常使事情难以预测。EH的重量级是您听到人们说“不要将异常用于控制流”之类的原因之一。
因此,某些库(例如<filesystem>
)使用C ++调用的“双重API”或C#调用的Try-Parse
模式(感谢Peter的技巧!)
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
}
您可以立即看到“双重API”的问题:大量的代码重复,没有为用户提供使用哪种API是“正确” API的指南,用户必须在有用的错误消息(CalculateArea
)和错误消息之间做出艰难的选择。速度(TryCalculateArea
),因为更快的版本采用了我们有用的"negative side lengths"
异常并将其简化为无用的false
东西-“出了点问题,不要问我是什么或在哪里。” (一些双重API使用更具表现力的错误类型,例如int errno
或C ++的错误类型,std::error_code
但这仍然不能告诉您错误发生在哪里 -只是它确实发生在某个地方。)
如果您不能决定代码的行为方式,则可以随时将决定权交给调用者!
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; });
这实际上是您的同事正在做的事情;除了他将“错误处理程序”分解为全局变量之外:
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);
将重要参数从显式函数参数转移到全局状态几乎总是一个坏主意。我不推荐它。(在您的情况下,它不是全局状态,而仅仅是实例范围的成员状态就可以稍微减轻一点(但不是很多)。)
此外,您的同事不必要地限制了可能的错误处理行为的数量。他没有决定使用任何错误处理lambda,而是只决定了两个:
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 ++(或带条件编译的任何语言)的通用解决方案将是迫使用户在编译时全局地决定整个程序,以便可以完全优化未使用的代码路径:
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);
这种工作方式的一个示例是C和C ++中的assert
宏,该宏在预处理器宏上确定其行为NDEBUG
。