编写函数时,我的实现经常看起来像这样:
关键部分是伐木。每个失败的函数都应该在日志中添加简短描述。这样,在处理异常的级别,可以向用户显示详细的错误消息。
例如,考虑一个可以创建新用户帐户的应用程序,并且数据库连接存在问题。以下反向堆栈跟踪结果:
使用例外功能,我将按如下方式实现:
void CreateUser()
{
try {
OpenDatabaseConnection();
}
catch(std::exception& e) {
e.AddLog("Failed to create the new user");
throw;
}
//...
}
使用简单的返回值,我会写下以下内容:
bool CreateUser(Log& log)
{
if (!OpenDatabaseConnection(log))
{
log.Add("Failed to create the new user");
return false;
}
//...
return true;
}
我发现两种实现都同样好。因此,我没有看到使用异常的优势。我很清楚异常处理通常被认为是一个有用的功能,但我真的不明白为什么。很久以前,我广泛使用异常处理,但我没有看到它的大优势,所以现在我再也不用它了。因此我的问题是:
注意:我使用术语记录作为“收集对错误的解释,以便以后可以将其呈现给用户”。我宁愿不将该解释存储在日志消息的全局集合中(在内存中,在文件中或在数据库中),因为它直接描述了特定的异常。
更新:感谢您的回复。我知道只有当用户不需要有关错误的详细反馈时,例外才有用。 (如果我误解了这个,请纠正我。)
答案 0 :(得分:6)
您的策略似乎避免了异常的最有用方面,您可以抛出一个异常类,其中已经包含日志信息的文本 - 这是在异常抛出时不生成日志的文本捕获异常的时间。然后你不必抓住每个级别上升到堆栈,但只是在顶层。
因此只有一个try块和一个log.add - 一般来说代码少得多。 像这样的东西似乎删除了你的所有复制。
void OpenDatabaseConnection()
{
if (Error) throw MyException("Failed opening database");
}
void CreateUser()
{
try {
OpenDatabaseConnection();
//...... do everything here
}
catch(MyException& E) { //only one copy of this code
E.AddLog(E.getMessage());
throw;
}
}
答案 1 :(得分:2)
我认为在这里使用异常的一个重要案例是,您现在已经将日志记录作为方法签名的一部分。总的来说,我认为不应该这样,因为这是一个贯穿各领域的问题。例如,想象一下尝试用用户权限做类似的事情。你打算到处写这个吗?
bool CreateUser(Log& log, UserSecurityContext& u) {
if (!HasPermissionsFor(u, SecurityRoles::AddUser)) {
log.Add("Insufficient permissions");
return false;
}
//...
return true;
}
还有其他原因想要使用异常(请参阅Elemental's answer),但只要不使用语言功能会影响软件的设计,就值得考虑这是否是正确的方法它
答案 2 :(得分:2)
如果您总是希望在通话结束后立即处理您的异常情况,那么没有真正的优势。
当您想要处理调用链上几层的条件时,优势就出现了。要用你的成功标志做到这一点,你必须将标志冒出几层子程序调用。每一层都必须用知识来编写,它必须在代码的内部跟踪特殊标志。这只是PITA的主要推广。
例如,对于实时工作,我们通常围绕迭代循环构建应用程序。循环期间的任何错误通常只是中止循环的迭代(除了“致命”错误,这会中止整个应用程序)。处理此问题的最简单方法是从发生的任何地方抛出异常,并在应用程序最外层的自己的catch块中处理它们。
答案 3 :(得分:1)
异常处理从正常控制流中删除错误处理。这样,代码结构更加干净。异常处理也会自动展开堆栈。这样,您就不必在出错的方法中调用每个方法中包含错误处理代码。如果您需要其中一项功能,请使用例外情况。如果不这样做,请使用错误代码或任何其他方法,因为异常会产生成本(计算时间),即使它们没有被抛出。
您的评论的其他答案。想象一下代码,它会调用几个可能失败的函数。
procedure exceptionTest(...)
{
try
{
call1();
call2();
call3();
call4();
}
catch (...)
{
//errorhandling outside the normal control flow
}
}
无一例外:
procedure normalTest(...)
{
if (!call1())
{
//errorHandling1
}
else if (!call2())
{
//errorHandling2
}
else if ....
...
}
您可以很容易地看到,正常的控制流程因错误处理而中断。与此代码相比,使用异常的代码更易于阅读。
如果您需要在每个调用的方法中添加错误处理,则异常可能无法带来好处。但是如果你有嵌套调用,每个调用都可能产生错误,那么在顶层捕获异常可能更容易。我正是这个意思。在你的例子中并非如此,知道从异常中受益的地方仍然是好的。
答案 4 :(得分:1)
例外情况仅在极端情况下使用。执行异常太慢了。对于日志不是很大的错误,请尝试使用返回值。
示例:
int someMethod{
if(erorr_file_not_found){
logger.add("File not found");
return 1;
}
if(error_permission){
logger.add("You have not permissons to write this file");
return 2;
}
return 0;
}
在这种情况下,您可以打印错误并在更高级别处理此错误。
或(更复杂):
int someMethod{
int retval=0;
if(someshit){
retval=1;
goto _return;
}
//...
_return:
switch(retval){
case 1:logger.add("Situation 1");break;
case 2:logger.add("Situation 2");break;
//...
}
/*
close files, sockets, etc.
*/
return retval;
}
这种方式更难但最快。
答案 5 :(得分:0)
根据您的具体情况,您可以从异常的构造函数(可能是异步的)进行日志记录,这样您的代码就像:
void CreateUser()
{
OpenDatabaseConnection();
}
当然,您需要从OpenDatabaseConnection()抛出自定义异常。
当这个策略成功使用时,我参与了两个项目。
答案 6 :(得分:0)
我建议将错误处理与日志记录和用户交互分开。
每个方法都可以为自己写入日志文件。使用小型日志消息框架,方法可以输出调试,信息和错误消息。根据配置文件定义的应用程序运行的上下文,例如,实际上只写入严重错误消息。
特别是在网络应用程序中,连接失败总是会发生,并且不是例外。对不应发生或很少发生的意外错误使用异常。如果您需要,例如,在内部使用例外也是有意义的。堆栈展开功能:
void CreateUser() {
try {
CDatabaseConnection db = ConnectToDatabase();
InsertIntoDB(db, "INSERT INTO ... ");
SetPermission(...);
} catch(...) {}
}
如果InsertIntoDB因为再次丢失网络连接而抛出异常,则将销毁对象CDatabaseConnection,并且永远不会运行SetPermission。使用它可以带来更好的代码。
您要做的第三件事是在交互式应用程序中提供用户反馈。这是完全不同的事情。让您的内部方法返回可能的错误代码的枚举eerrorCONNECTIONLOST, eerrorUSERNAMEINVALID, etc
不要从核心方法返回错误字符串。用户界面层应该打扰要显示的字符串(可能将它们国际化)。在内部,错误代码将更有用。你可以,例如如果您的登录方法返回eerrorCONNECTIONLOST
,则重试五次。