目录
介绍
没有“一个真正的来源”
它为什么如此重要?
文章的形式
1.吃异常
2.不正确的日志记录
3.重新抛出异常和方法上下文日志记录
4.控制程序流程的异常
全局异常处理
总结
结论
介绍大家好,我想向您介绍我对软件开发人员工作中最重要部分之一的观点——异常处理和日志记录。
虽然我在不同公司工作多年,做过很多不同类型的应用程序,但我多次改变对此文主题的看法。但在那次经历之后,我对异常处理和日志记录应该如何运作有了最终的观点。我想与大家分享我如何看待它——当然使用不好的例子——与本系列之前的三篇文章相同!
你可以在这里找到我之前的三篇文章:
C#坏习惯:通过不好的例子学习如何制作好的代码——第1部分C#坏习惯:通过不好的例子学习如何制作好的代码——第2部分C#坏习惯:通过不好的例子学习如何制作好的代码——第3部分
要理解这篇文章,你不必阅读我之前的任何文章,但我鼓励这样做:)
没有“一个真正的来源”我们应该如何处理异常总是取决于情况。它是什么样的应用程序?有什么要求?它是什么样的异常?
但是,我认为在每个正确编写的企业应用程序中都应遵循一些通用规则。
当应用程序在生产环境中工作时,从维护角度来看,具有良好的异常处理非常重要,甚至更重要的——记录这些异常。糟糕的编写代码可以引入例如“沉默的杀手”,这可能会花费公司很多钱。
让我们想象一下,有人试图在网上商店购买一些产品。对于特定数据,该过程的一个步骤失败。未记录错误。客户在此在线商店退出购物并且不报告问题。他很生气,不再依赖我们的服务了。他转投竞争对手。对于特定情况,这种情况发生了一年。你能想象公司将损失多少钱?
这只是许多情况中的一个例子,如果您在实现异常处理和日志记录时没有遵循良好实践,则可能会发生这种情况:)
文章的形式在本文中,我将介绍4,这是我在担任软件开发人员时遇到的最常见的错误。我将展示一个C#代码的真实世界示例,它会产生麻烦,然后介绍我认为这些代码应该如何正确编写。我已经为每个示例添加了业务上下文,以表明这种情况会影响我们雇主的业务。
因此我总是强调,这些代码片段只是示例,而不是完全工作的应用程序,我只是用它们来展示一种方法。我还假设,有时您将不得不使用一些遗留组件,这些组件不是由您自己编写的,并且可能包含“不理想的代码”,您无法完全依赖它们。
我不会写“你应该永远遵循这一点,你永远不应该这样做”,而是试图说明这些错误在实践中会给我们带来什么后果。
要理解本文,您只需要具备C#语言的基本知识。
1.吃异常这是我最喜欢的一个。
想象一下,在线商店,客户必须注册我们的系统然后,他们才可以购买产品。每个人都可以想象注册过程有多烦人但无论如何......
我们有一个UserService处理客户注册流程的类。它在数据库中创建用户,生成激活链接,将其存储在数据库中,然后向我们的队列系统发送消息。另一个组件——电子邮件发件人——订阅此队列。它从中获取消息并发送带有激活链接的电子邮件给我们的客户。一个常见的功能。
现在,在UserService类中实现RegisterUser方法的开发人员得到一个需求,即如果注册过程的所有步骤都将在没有异常的情况下执行,但在向队列发送消息时会出现错误,则不应通知用户该错误,因为这是一个内部问题。用户不应该对某些网络或配置问题感到困扰,因为所有重要数据都存储在数据库中,并且其中存在用户定义。
所以我们的开发人员实现了下面的类
public class UserService
{
private readonly IMessagingService _messagingService;
public UserService(IMessagingService messagingService)
{
_messagingService = messagingService;
}
public bool RegisterUser(string login, string emailAddress, string password)
{
// insert user to the database
// generate an activation code to send via email
// save activation code to the database
// create an email message object
try
{
_messagingService.SendRegistrationEmailMessage(message);
}
catch (Exception)
{
// boom!
}
return true;
}
}
代码很简单,我仅仅为这个例子添加了最重要的部分。我们正在使用哪个队列系统并不重要,因为在示例中,您只能看到写入队列的实际实现的包装器,由接口隐藏。
由于我们的开发人员是一个有趣的人,他在一个catch块写了一个有趣的注释:)
是的,我在一个严肃的商业应用程序中看到了这个注释:)
正如我们实现的全局异常处理(将在全局错误处理段落中解释)——有一个代码处理所有未处理的异常,记录它们并将错误页面返回给最终用户——所有异常,除了那些异常在try块,会得到妥善处理和用户会看到一个错误页面。
如果消息传递服务类(IMessagingService接口的实现)中发生异常,用户仍将获得一切正常的信息。
看起来我们的开发人员满足了这些要求。
但...
他很饿,他吃掉了异常而不是记录它。
会有什么后果?
想象一下现在有两种情况:
A)在其中一次部署之后
- 部署工程师弄乱了配置文件,他在队列的地址中输入了一个拼写错误
- 队列被移动到另一个服务器,有人忘了我们的应用程序正在写入它并且没有更改web.config文件中的地址
B)我们的Web应用程序和安装了队列的外部服务器之间经常存在网络问题,或者服务器不时停机几个小时,出于某些原因
所以...
用户正在尝试注册,系统说一切都很顺利。但他们从未获得过激活电子邮件,因此他们无法登录系统。他们试图再次注册,但系统说具有此登录/电子邮件地址的用户已经存在于系统中。他们试图再次使用新凭证注册,但结果是一样的。您认为他们会联系支持者还是会选择竞争对手?自己回答这个问题。
我没有必要补充说我们的日志完全是空的:)
现在,在选项A)的情况下,可能会在一段时间后发现问题,因为所有者将看到新用户的数量没有增加并且将开始挖掘。
但是在选项B)的情况下,我们面临着一个我称之为“沉默的杀手”的事情。想象一下,这种情况多年来一直在发生——公司将失去多少客户?如果我们的开发人员预测到这种情况,他将按如下方式实现代码:
public class UserService
{
private readonly IMessagingService _messagingService;
private readonly ILogger _logger;
public UserService(IMessagingService messagingService, ILogger logger)
{
_messagingService = messagingService;
_logger = logger;
}
public bool RegisterUser(string login, string emailAddress, string password)
{
// insert user to the database
// generate an activation code to send via email
// save activation code to the database
// create an email message object
try
{
_messagingService.SendRegistrationEmailMessage(message);
}
catch (Exception ex)
{
_logger.Error(string.Format("Exception occurred while sending an activation emailto a user with login: {0}, email: {1}", login, emailAddress), ex.ToString());
}
return true;
}
}
您可以注意到我们引入了一个记录器类,它现在记录了一个详细的异常。
我们将Error自定义消息和原始异常调用堆栈传递给该方法,并将两者写入日志文件。
它有什么变化?
现在,当我们查看日志文件时,我们会注意到我们的注册过程中存在严重问题,因为自上次部署以来,不会发送激活电子邮件。当我们在数据库中收到电子邮件时,公司甚至可以通过手动联系具体人员来尝试解决这种情况。也可以有一些“重新发送激活电子邮件给用户”的过程实现,但这是一个细节。
2.不正确的日志记录我们现在改变上下文。想象一下,我们公司为其客户提供了一个复杂的网站,用ASP.NET MVC编写。报告错误的唯一方法是通过联系表单。让我们来看看这个功能(向应用程序支持发送消息)是如何实现的......
MVC控制器的动作实现:
[HttpPost]
public ActionResult SendMessage(MessageModel messageModel)
{
try
{
var emailAddress = _contactControllerHelper.GetEmailReceiverAddress(messageModel.Category); // line: 37
// code responsible for sending an e-mail
}
catch(Exception ex)
{
_logger.Error(ex.Message);
return Json(false);
}
return Json(true);
}
我们可以注意到,该操作采用带有消息作为参数的模型,使用从视图发送的ajax调用。然后,根据发送的类别(从联系表单(UI)的下拉列表中选择),_contactControllerHelper获取特定部门的电子邮件地址。联系人类别定义存储在C#枚举中:
public enum ContactCategory
{
NewCustomersSupport = 1,
ApplicationSupport = 2,
ExistingCustomersSupport = 3
}
而_contactControllerHelper的实现如下:
public class ContactControllerHelper : IContactControllerHelper
{
private readonly Dictionary _emailAddressesForContactCategories;
public ContactControllerHelper(Dictionary emailAddressesForContactCategories)
{
_emailAddressesForContactCategories = emailAddressesForContactCategories;
}
public string GetEmailReceiverAddress(ContactCategory contactCategory)
{
return _emailAddressesForContactCategories[contactCategory]; // line: 22
}
}
它通过构造函数将电子邮件地址的分配字典保存并传递给类别,并使用GetEmailReceiverAddress方法返回部门分配类别的电子邮件地址。如果字典不包含传递给GetEmailReceiverAddress方法类别的定义,它将快速失败,并且MVC控制器的操作中的try-catch块将捕获异常,记录它并向视图返回失败,这将呈现自定义错误页面给最终用户。我们可以考虑哪些将会更好:
- 操作方法中的try-catch
- 全局错误处理
- 以面向方面的方式实现日志记录
- 等等
我想在这里展示的问题与异常处理方法无关。
现在想象一下,将字典传递给ContactControllerHelper实例的代码有一个bug。由于某种原因,它不包含ApplicationSupport类别的分配定义,它看起来像这样:
var emailAddressesForContactCategories = new Dictionary() {
{ ContactCategory.NewCustomersSupport, ConfigurationManager.AppSettings["NewCustomersSupportEmailAddress"] },
{ ContactCategory.ExistingCustomersSupport, ConfigurationManager.AppSettings["ExistingCustomersSupportEmailAddress"] }
};
因此,当最终用户尝试报告错误(向应用程序支持发送消息)时,系统将向他显示错误页面,我们将在日志文件中看到以下条目:
“ 给定的键没有出现在字典中。 ”
由于我们的应用程序非常庞大,从上面的信息来看,我们相当于什么也没说。
我们甚至不能说出我们系统的哪个部分有问题。
总而言之,我们发现我们的应用程序存在问题但我们无法对其进行本地化。我们的最终用户没有报告任何错误,因为他们基本上无法报告:)
我认为我们的用户不会对我们提供给他们的产品感到满意。我会开始寻找竞争对手......
那么为什么我们的日志文件中有关于异常的信息很少呢?
让我们看看我们的action方法中的catch块:
catch(Exception ex)
{
_logger.Error(ex.Message);
return Json(false);
}
这是我们真正的问题:
_logger.Error(ex.Message);
我们的开发人员犯了一个错误。他没有记录异常的完整堆栈,而只使用ex.Message属性记录了一般消息。我们该如何解决?非常简单地。让我们用新的替换我们的旧的catch块:
catch(Exception ex)
{
_logger.Error(ex.ToString());
return Json(false);
}
现在检查我们在日志文件中看到的内容:
“System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
at ExceptionTest.Helpers.ContactControllerHelper.GetEmailReceiverAddress(ContactCategory contactCategory)
in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\Helpers\\ContactControllerHelper.cs:line 22
at ExceptionTest.Controllers.ContactController.SendMessage(MessageModel messageModel)
in c:\\[PATH_TO_PROJECT]\\ExceptionTes\\Controllers\\ContactController.cs:line 37“
你现在可以看到,我们有一个存储在我们的日志文件中的异常的完整调用堆栈。
我们现在很容易说我们在从联系表单发送消息时遇到问题,并且_emailAddressesForContactCategories字典中不存在其中一个联系人类别的键,因为在日志文件中报告:ContactControllerHelper.cs:第22行是:
return _emailAddressesForContactCategories[contactCategory];
一行代码造成了很大的变化。显然,最好的解决方案是:
public string GetEmailReceiverAddress(ContactCategory contactCategory)
{
string emailAddress;
if (!_emailAddressesForContactCategories.TryGetValue(contactCategory, out emailAddress))
{
throw new ArgumentOutOfRangeException(string.Format("There is no e-mail address definition for the given category id: ({0})", contactCategory));
}
return emailAddress;
}
而不是:
public string GetEmailReceiverAddress(ContactCategory contactCategory)
{
return _emailAddressesForContactCategories[contactCategory];
}
哪个会告诉我们我们需要的东西,但经常(特别是在包含大量遗留代码的大型项目中),我们必须处理“不完美的代码”,就像使用旧的共享类一样,并且此类已使用在许多模块中,我们不希望改变它的行为(以避免引入错误)。这就是为什么在任何情况下,我们都需要正确实现异常处理和日志记录的原因。
3.重新抛出异常和方法上下文日志记录
另一个例子。
让我们假设我们有一个系统(ASP.NET MVC网站),在UI(管理面板)上,我们可以通过用户名搜索用户。要求是,如果搜索到的用户不存在或不活动,我们的最终用户将看到屏幕:
- “用户不存在”文本——作为表单标题,而不是用户名
- “---”——作为名、姓和组名
如果找到用户,系统将从数据库中显示正确的值。
如果发生异常,我们希望我们的异常冒泡,因为我们有适用于应用程序的全局错误处理(将在全局错误处理段落中解释),因此异常将被捕获、记录和以友好的消息形式呈现给用户(例如,自定义错误页面)。
为了实现这一点,我们的开发人员选择了Null Object Design Pattern 来避免处理null和if-else逻辑。
该NullUserViewModel类的样子:
public class NullUserViewModel : IUserViewModel
{
public NullUserViewModel()
{
UserName = "User doesn't exist";
FirstName = "---";
Surname = "---";
GroupName = "---";
}
public string UserName { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public string GroupName { get; set; }
}
如果发生异常,我们的开发人员会想到将一些更具描述性的信息记录到日志文件中。他希望那些根本无法访问源代码或在查看日志时无法访问源代码的人能够大致了解哪些功能不能正常工作。
很公平,在我看来,每一个提高系统问题可见性的信息都是一个优势。
我们来看看他是如何实现它的:
public class UserService
{
private readonly ILogger _logger;
private readonly IRepository _repository;
public UserService(ILogger logger, IRepository repository)
{
_logger = logger;
_repository = repository;
}
public IUserViewModel GetUserInfo(string userName)
{
try
{
var user = _repository.GetUser(userName);
if (user != null && user.IsActive)
{
return new UserViewModel() // line: 27
{
FirstName = user.FirstName,
Surname = user.Surname,
GroupName = user.Group.Name
};
}
return new NullUserViewModel();
}
catch (Exception ex)
{
_logger.Error("An error occurred while trying to get user data from the database.");
throw ex; //line: 39
}
}
}
您可以注意到,当找不到活动用户时,该方法GetUserInfo仅返回NullUserViewModel类。catch块中的代码将详细信息写入日志文件并重新抛出异常。
开发人员的想法是,当某人打开日志文件时,从业务角度来看他将能够说明系统的哪个部分不起作用。深入研究代码需要更多时间,需要开发人员知识和源代码。因此,例如,如果注册或日志记录模块不起作用,我们会发出警报,我们需要立即修复它,但如果系统没有返回FAQ部分,那么它就不那么重要了。
所以现在,让我们把自己置于维护此代码的开发人员的脚下。
想象一下,我们的GetUserInfo方法的try块中发生异常。它是:NullReferenceException。
用户看到一条漂亮的错误消息,我们的应用程序记录如下:
“An error occurred while trying to get user data from the database.System.NullReferenceException: Object reference not set to an instance of an object.at ExceptionTest.UserService.GetUserInfo(String userName) in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\UserService.cs:line 39at ExceptionTest.Controllers.AdminController.GetUser(string userName)in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\Controllers\\AdminController.cs:line 40“
您可以注意到,我们没有看到原始异常发生在哪一行。那是因为重新抛出异常被错误地实现了:
throw ex;
上面的行删除了原始异常的调用堆栈。你可以注意到堆栈的底部显示的是UserService.cs:第39行,它是:
throw ex;
因此,即使我们将在全局错误处理程序(使用ex.ToString())中正确记录错误,我们也不会看到原始调用堆栈。
另一件事是我们的附加信息对我们没有任何帮助,因为在我们检查了报告的行后,我们已经知道了。
我们现在可以说什么?我们能说出究竟发生了什么吗?
因此,可能出现的情况很少:
- 执行时发生异常:user.Group.NameUser类的Group属性未填充且为null。
- 执行下面的代码时发生异常:_repository.GetUser(userName);null作为存储库对象传递给我们的UserService构造函数,并且在尝试访问此服务时发生异常。如果在某些特殊情况下偶尔只发生异常,则这种情况不太可能发生。但也许有另一个被放在另一个很少使用的模块中的UserService类的使用者——那么这个选项是可能的。
- 异常发生在一个方法管道中的某个地方,这些方法从以下开始:_repository.GetUser(userName);可能是X方法在管道中以及可能发生的很多地方。假设其中一个位于数据访问层级别。
此外,即使我们可以将生产数据迁移到开发数据库,我们也无法在开发环境中重现确切的案例,因为我们不知道userName发生异常时请求的内容。
太多未知数,太多的操作要做。请记住,这是一个非常简单的例子。想象一下,如果我们在try块中使用更多组件以及可能发生潜在异常的更多位置,那将是多么困难。
可能长时间的调查和试错法最终会给你答案,但我们不想把我们的应用程序当作黑盒子来对待
让我们修改一下我们的catch块:
catch (Exception ex)
{
_logger.Error(string.Format("An error occurred while trying to get user data from the database. UserName = {0}",userName));
throw; // line: 39
}
我们改变:
throw ex;
为
throw;
现在我们遵循众所周知的规则“始终使用throw;在重新抛出异常时使用以保留原始异常调用堆栈”。
我们还丰富了我们的额外日志信息,以添加方法调用的上下文:
string.Format("An error occurred while trying to get user data. UserName = {0}", userName)
记录输入参数(userName)将允许我们在必要时重现相同的情况。
现在检查我们在日志文件中看到了什么:
“An error occurred while trying to get user data from the database. UserName = Test.System.NullReferenceException: Object reference not set to an instance of an object.
at ExceptionTest.UserService.GetUserInfo(String userName)in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\UserService.cs:line 39at ExceptionTest.Controllers.AdminController.GetUser(string userName)in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\Controllers\\AdminController.cs:line 40“
惊讶吗?
我们现在看到更多信息:
“UserName = Test”,如果我们有原始数据,它允许我们重现问题,但异常调用堆栈仍然与更改前相同:
throw ex;
至
throw;
为什么?我们遇到了特殊情况。
IMPORTANT:如果我们重新抛出在捕获 此异常的方法中发生的异常,则抛出的异常将不会保留原始调用堆栈(即使我们正在使用throw;)。
如果方法A会捕获在方法B中发生的异常(从方法A调用),并且我们使用以下方法重新抛出它:
throw;
然后,抛出异常将包含原始异常调用堆栈。
那么如果在同一个方法中发生异常,我们如何保留原始异常调用堆栈呢?
我们可以使用下面的代码保留原始调用堆栈:
MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
BindingFlags.Instance | BindingFlags.NonPublic);
preserveStackTrace.Invoke(exception, null);
我在这里找到了这个技巧:
https://weblogs.asp.net/fmarguerie/rethrowing-exceptions-and-preserving-the-full-call-stack-trace
上面的代码可以实现为 异常类型的扩展方法:
public static class ExceptionExtensions
{
public static void PreserveStackTrace(this Exception exception)
{
MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
BindingFlags.Instance | BindingFlags.NonPublic);
preserveStackTrace.Invoke(exception, null);
}
}
}
然后我们新的catch块将如下所示:
catch (Exception ex)
{
ex.PreserveStackTrace();
_logger.Error(string.Format("An error occurred while trying to get user data from the database. UserName = {0}", userName));
throw; // line: 40
}
我们现在检查一下我们在日志文件中会看到什么:
“ An error occurred while trying to get user data from the database. UserName = Test.System.NullReferenceException: Object reference not set to an instance of an object.
at ExceptionTest.UserService.GetUserInfo(String userName)in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\UserService.cs:line 27at ExceptionTest.UserService.GetUserInfo(String userName)in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\UserService.cs:line 40at ExceptionTest.Controllers.AdminController.GetUser(string userName)in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\Controllers\\AdminController.cs:line 40“
最后!
我们得到了一个信息,其中发生了真正的异常,它是第27行,它是:
return new UserViewModel()
{
FirstName = user.FirstName,
Surname = user.Surname,
GroupName = user.Group.Name
};
我个人可以看到上述解决方案的一些缺点,我们基于反射,而不是.Net Framework api 给出的,它是某种黑客攻击。
理论上,在框架的下一个版本中,可能会进行一些内部更改,并且此解决方案可能会中断。
那么我们如何才能更好地使用我们的代码来获取日志文件中的原始异常,同时使用Microsoft api给出的代码呢?
让我们将catch块更改为:
catch(Exception ex)
{
throw new Exception(string.Format("An error occurred while trying to get user data from the database. UserName = {0}", userName), ex); // line: 38
}
我们基本上可以丰富一个异常,无论如何都将通过我们的全局错误处理记录(它将在全局错误处理部分中进行解释)。新的抛出异常将在InnerException字段中包含原始异常堆栈跟踪。
感谢您的帮助,我们将能够从UserService类中删除记录器:
public class UserService
{
private readonly IRepository _repository;
public UserService(IRepository repository)
{
_repository = repository;
}
public IUserViewModel GetUserInfo(string userName)
{
try
{
var user = _repository.GetUser(userName);
if (user != null && user.IsActive)
{
return new UserViewModel() // line: 28
{
FirstName = user.FirstName,
Surname = user.Surname,
GroupName = user.Group.Name
};
}
return new NullUserViewModel();
}
catch (Exception ex)
{
throw new Exception(string.Format("An error occurred while trying to get user data from the database. UserName = {0}", userName), ex); // line: 38
}
}
}
让我们现在检查一下日志文件中的内容:
“ System.Exception: An error occurred while trying to get user data from the database. UserName = Test. --->
System.NullReferenceException: Object reference not set to an instance of an object.
at ExceptionTest.UserService.GetUserInfo(String userName)
in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\UserService.cs:line 28
--- End of inner exception stack trace ---
at ExceptionTest.UserService.GetUserInfo(String userName)in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\UserService.cs:line 38
at ExceptionTest.Controllers.AdminController.GetUser(string userName)
in c:\\[PATH_TO_PROJECT]\\ExceptionTest\\Controllers\\AdminController.cs:line 40“
我们的全局异常处理程序正确记录异常,使用:
ex.ToString()
这样,将记录异常堆栈跟踪(新异常)和内部异常堆栈跟踪(原始异常)。
而且我们仍然得到原始异常发生的那一行(第28行)。
这是我推荐的方法或这类问题。
现在,我们知道:
要么该组未从数据库中取出,要么某些C#代码未将有关该组的信息(从数据库返回)分配给用户对象。
我们现在可以检查这个特定用户是否在生产数据库中分配了一个组(或者让负责数据库的人员检查它)。如果不是,我们发现了问题,否则我们必须检查C#中的组分配代码。
如果我们无法找到任何内容,我们可以将数据迁移到开发数据库并重现确切的情况,因为我们在日志文件中有关于搜索userName的信息。
4.控制程序流程的异常最后一个示例将是数据访问层中非常简单的方法。想象一下,我们正在使用Entity Framework从数据库中获取用户数据。如果找不到用户,则方法应该只返回null。
我们可怜的开发者实现了以下代码:
public class DAL
{
// the rest of the code
public User GetActiveUser(string userName)
{
try
{
return _dbContext.Users.First(x => x.UserName == userName && x.IsActive);
}
catch(Exception)
{
return null;
}
}
}
我们这里有什么问题?
显然这是一个非常危险的代码。如果发生任何异常,我们的方法将返回null。因此,例如,如果数据库服务器已关闭,存在网络连接问题或连接字符串中存在错误,则我们的方法将返回null,与“未找到用户”的情况相同。此代码隐藏了潜在的严重问题。
显然,我们可以将代码更改为仅缓存具体的异常类型,其余的将会冒泡,这将是一个更好的解决方案。问题会很快被发现。
代码如下所示:
public class DAL
{
// the rest of the code
public User GetActiveUser(string userName)
{
try
{
return _dbContext.Users.First(x => x.UserName == userName && x.IsActive);
}
catch(InvalidOperationException)
{
return null;
}
}
}
但代码仍然不理想。
为什么?
- 我们会注意到性能下降很少,因为不必要地抛出一个异常将会使程序慢一点
- 有很多不必要的代码——与改进版本相比
- 可读性不是那么好
- 调试时经验不佳——这让我很疯狂——在调试代码时,每当有用户请求不存在或不活动时,调试器就会停止。想象一下,我们的例子中有很多类似代码的地方。你开始寻找那个对你这样做的人:)
显然你可以在Visual Studio中设置:“当异常发生时不要中断”,但是你不会注意到你感兴趣的真正异常。
那么如何解决呢?
下面的代码使用IQueryable集合FirstOrDefault()上的方法,如果存在则返回值。如果没有,它将返回null而不抛出异常。代码更简单,更易读。这就是我们想要的:
public class DAL
{
// the rest of the code
public User GetActiveUser(string userName)
{
return _dbContext.Users.FirstOrDefault(x => x.UserName == userName && x.IsActive);
}
}
所以总结一下这段话,如果可以避免发生异常,那就去做吧。
我可以理解,该FirstOrDefault()方法是在.Net Framework 3.5版本中引入的,但是嘿,从那时起已经过了9年;)
同样的情况是Parse()和TryParse()方法等。
全局异常处理你们有谁在每种方法中都看到了带有try-catch的应用程序?当有一行真正的方法正在执行,而不是try-catch块的X行、异常日志记录和返回特定响应时,它看起来可读吗?
手动在所有方法中重复相同的代码。如果有人忘记用其中一种方法编写,则异常将无法处理。这是最好的方法吗?我会说不。
我更喜欢实现全局异常处理。
应该怎么样?
我们的应用程序中没有使用try-catch块,除了一些特殊情况,例如:
“要求是,如果在发送电子邮件时发生错误,应用程序应该只记录此异常而不是冒泡”。
所有未处理的异常将由一段代码捕获,该代码将记录它并向最终用户提供一般消息:
- 错误页面——在Web应用程序的情况下
- 一个消息框——在Windows窗体应用程序的情况下
- 对web请求的响应——对于web api
- 等等
我不会将它的任何实现呈现,因为:
- 取决于应用程序类型(桌面应用程序,Web应用程序, Windows服务,控制台应用程序),您将不得不使用不同的机制
- 没有“一种方法”可以做到这一点
- 你可以在这个主题上写X个单独的文章
- 这不是本文的主要内容
总之,我的文章解释说:
- 为什么你不应该吃异常
- 区别:Exception.Message和Exception.ToString()
- 区别:throw ex; throw; 和throw new Exception(“Method call context”, ex);
- 为什么使用异常来控制程序流程并不是最好的主意
在文章中,我向您介绍了常见异常处理和日志记录错误可能带来的严重后果。有时,一行代码会产生很大的不同并导致许多麻烦。
我能给你的建议是:
“在实现异常处理和日志记录的同时,让自己成为开发人员——谁将维护这些代码——并认为当异常发生时对他有多大帮助”。
下一篇:C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分
原文地址:https://www.codeproject.com/Articles/1173928/Csharp-BAD-PRACTICES-Learn-how-to-make-a-good-co