随着我们的应用程序的增长,我们希望采用可管理的策略来处理错误,以保持用户的体验一致,更重要的是,为我们提供解决和修复问题的方法。
异常处理的最佳实践
在.NET框架中表达错误条件的惯用方法是抛出异常。在C#中,我们可以使用try-catch-finally语句处理它们:
try
{
//codewhichcanthrowexceptions
}
catch
{
//codeexecutedonlyifexceptionwasthrown
}
finally
{
//codeexecutedwhetheranexceptionwasthrownornot
}
每当在try块内抛出异常时,执行在catch块中继续。在最后块执行后的试块已成功完成。它也在退出catch块时执行,无论是成功还是异常。
在catch块中,我们通常需要有关我们正在处理的异常的信息。要抓住它,我们使用以下语法:
catch(Exceptione)
{
//codecanaccessexceptiondetailsinvariablee
}
使用的类型(在我们的例子中是Exception),指定catch块将捕获哪些异常(在我们的例子中都是,因为Exception是所有异常的基本类型)。
任何不属于给定类型或其后代的异常都将失败。
我们甚至可以将多个catch块添加到单个try块中。在这种情况下,异常将被具有匹配异常类型的第一个catch块捕获:
catch(FileNotFoundExceptione)
{
//codewillonlyhandleFileNotFoundException
}
catch(Exceptione)
{
//codewillhandlealltheotherexceptions
}
这允许我们以不同的方式处理不同类型的异常。我们可以通过非常具体的方式从预期的异常中恢复,例如:
如果用户选择了不存在或无效的文件,我们可以允许他选择其他文件或取消操作。如果网络操作超时,我们可以重试或邀请用户检查其网络连接。对于剩余的意外异常,例如由代码中的错误引起的NullReferenceExceptions,我们可以向用户显示一般错误消息,为他提供报告错误的选项,或者在没有用户干预的情况下自动记录错误。
避免以无声方式吞咽异常也非常重要:
catch(Exceptione)
{}
这样做对用户和开发人员都是不利的。
用户可能会错误地认为某个操作成功,而实际上它无声地失败或未完成;而开发人员不会获得有关异常的任何信息,而不知道他可能需要修复应用程序中的某些内容。
静默隐藏错误仅适用于非常特定的场景,例如捕获尝试在处理程序中记录错误时抛出的异常,以防止意外异常。
即使我们尝试记录此新错误或重试记录原始错误,它仍然很可能仍会失败。
在这种情况下,悄悄流产很可能是较小的罪恶。
集中式异常处理
编写异常处理代码时,务必在正确的位置执行此操作。您可能会尽可能接近异常的起源,例如在每个单独函数的级别:
voidMyFunction()
{
try
{
//actualfunctionbody
}
catch
{
//exceptionhandlingcode
}
}
这通常适用于您可以以编程方式处理的异常,而无需任何用户交互。这是在您的应用程序可以从异常中完全恢复并仍然成功完成所请求的操作的情况下,因此用户根本不需要知道发生了异常。
但是,如果请求的操作因异常而失败,则用户需要了解它。
在桌面应用程序中,技术上可以从最初发生异常的同一方法显示错误对话框,但在最坏的情况下,这可能会导致一系列错误对话框,因为随后调用的函数也可能失败-例如,因为他们没有所需的数据:
voidUpdatePersonEntity(PersonModelmodel)
{
varperson=GetPersonEntity(model.Id);
ApplyChanges(person,model);
Save(person);
}
让我们假设在GetPersonEntity中抛出一个不可恢复的异常(例如,没有具有给定Id的PersonEntity):
GetPersonEntity将捕获异常并向用户显示错误对话框。由于先前的失败,ApplyChanges将无法使用null值更新PersonEntity,因为第一个函数返回了该值。根据处理发生异常的策略,它将向用户显示第二个错误对话框。同样,由于PersonEntity具有空值,Save也将失败,并将连续显示第三个错误对话框。为了避免这种情况,您应该只处理不可恢复的异常,并在用户操作直接调用的函数中显示错误对话框。
在桌面应用程序中,这些通常是事件处理程序:
privatevoidSaveButton_Click(objectsender,RoutedEventArgse)
{
try
{
UpdatePersonEntity(model);
}
catch
{
//showerrordialog
}
}
相反,UpdatePersonEntity和它调用的任何其他函数都不应该捕获它们无法正确处理的任何异常。然后异常将冒泡到事件处理程序,该处理程序将仅向用户显示一个错误对话框。
这在Web应用程序中也很有效。
例如,在MVC应用程序中,用户直接调用的唯一函数是控制器中的操作方法。为了响应未处理的异常,这些方法可以将用户重定向到错误页面而不是常规页面。
为了简化在入口函数中捕获未处理异常的过程,.NET框架提供了全局异常处理的方法。尽管详细信息取决于应用程序的类型(桌面,Web),但是在异常从最外层函数冒泡后作为最后一次机会,总是会调用全局异常处理程序,以防止应用程序崩溃。
在WPF中,可以将全局异常处理程序连接到应用程序的DispatcherUnhandledException事件:
publicpartialclassApp:Application
{
publicApp()
{
DispatcherUnhandledException+=App_DispatcherUnhandledException;
}
privatevoidApp_DispatcherUnhandledException(objectsender,
DispatcherUnhandledExceptionEventArgse)
{
varexception=e.Exception;//getexception
//...showtheerrortotheuser
e.Handled=true;//preventtheapplicationfromcrashing
Shutdown();//quittheapplicationinacontrolledway
}
}
在ASP.NET中,全局异常处理程序是基于约定的-global.asax中名为Application_Error的方法:
privatevoidApplication_Error(objectsender,EventArgse)
{
varexception=Server.GetLastError();//getexception
Response.Redirect(error);//showerrorpagetouser
}
全局异常处理程序到底应该做什么?
从用户角度来看,它应该显示友好的错误对话框或错误页面,其中包含有关如何继续操作的说明,例如重试操作,重新启动应用程序,联系支持等。对于开发人员来说,记录异常详细信息更为重要进一步分析:
File.AppendAllText(logPath,exception.ToString());
在异常上调用ToString()将返回其所有细节,包括文本格式的描述和调用堆栈。这是您要记录的最少信息量。
为了便于以后的分析,您可以包含其他信息,例如当前时间和任何其他有用信息。
使用异常日志记录库
虽然将错误写入日志文件可能看起来很简单,但要解决以下几个问题:
文件应该放在哪里?大多数应用程序没有管理权限,因此无法写入安装目录。如何组织日志文件?应用程序可以将所有内容记录到单个日志文件中,或者根据日期,错误来源或其他一些条件使用多个日志文件。日志文件有多大?您的应用程序日志文件不应占用太多磁盘空间。您应该根据年龄或总日志文件大小删除旧日志文件。应用程序是否会从多个线程写入错误?一次只能有一个线程访问文件。多个线程需要同步对文件的访问或使用单独的文件。还有一些替代日志文件可能更适合某些情况:
Windows事件日志旨在记录应用程序中的错误和其他信息。它解决了上述所有挑战,但需要专用工具来访问日志条目。如果您的应用程序已使用数据库,它还可以将错误日志写入数据库。如果错误是由无法访问的数据库引起的,则应用程序将无法将该错误记录到数据库中。您可能不希望提前决定上述选项,而是在安装过程中配置行为。
支持所有这些并不是一项微不足道的工作。幸运的是,这是许多应用程序的常见要求。几个专用库支持上述所有内容以及更多内容。
适用于.NET的流行错误记录库
对于.NET框架,最流行的日志库可能是log4net和NLog。
虽然,两者之间当然存在差异,但主要概念非常相似。
例如,在NLog中,您通常会在每个需要向日志写入任何内容的类中创建一个静态记录器实例:
privatestaticLoggerlogger=LogManager.GetLogger(LoggerName);
要写入日志,您只需调用该类的方法:
logger.Error(exception.ToString());
您可能已经注意到,您尚未指定要记录错误的位置。您可以将它放在NLog.config配置文件中,而不是在应用程序中对此信息进行硬编码:
?xmlversion=1.0encoding=utf-8?
nlogxmlns=