65.9K
CodeProject 正在变化。 阅读更多。
Home

异常

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.22/5 (6投票s)

2009年1月28日

CPOL

7分钟阅读

viewsIcon

32242

如何从用户/系统管理员的角度处理异常

引言

这是我第一次向万维网投稿,请大家多多包涵。

在过去的几年里,实际的编程不再像以前那样具有挑战性了。我从 VB6 转向 C#.NET 给了我一些新的动力(我仍在学习 .NET Framework),但我的重心更多地转向了技术设计和指导其他开发人员。

在过去的几个月里,我一直在通过聊天和电子邮件文档来指导印度的几位开发人员。相信我,这让这项工作重新充满了挑战。

正如我之前提到的,这涉及到相当多的文档。那么为什么不把这些文档放到 Code Project 上,看看其他人对此有什么看法呢?

当我审查源代码时,我注意到的第一件事就是异常的处理方式。如果你考虑到目标受众(用户和/或系统管理员),这部分可以进行一些改进。现在让我们看看世界其他地方是否同意我的看法。

注意:英语是我的第二语言,对于任何用词/短语错误,请见谅。

异常

异常会中断我的应用程序中的“正常路径”(http://en.wikipedia.org/wiki/Happy_path)。对我来说,只有三种异常

  1. 技术异常
  2. 业务异常
  3. 实际错误

要区分前两种,有一个非常简单的规则;如果完全相同的调用(具有相同的参数和在相同的上下文中)可以在另一个时间点成功,那么它就是技术异常,否则就是业务异常。

注意:我们所讨论的时间范围(对于同步过程)通常是几毫秒。如果服务器非常繁忙,那么客户端将得到一个(技术)超时异常。几毫秒后,服务器可能再次有时间处理此类请求。

我们区分这两种情况的原因是

  • 由系统管理员确定问题的原因。
    如果是技术异常,那么他应该解决它。
    如果是业务异常,用户应该解决它。
  • 客户端是否应该重试?
    特别是对于异步过程,这非常有用。

你注意到我没有责怪开发人员吗?我会谈到这一点,别担心。

业务异常

通常,这是由触发(有效?)业务规则引起的。例如;将用户信息存储到数据库中可能需要名字。如果我们没有收到这样的名字,我们就会引发一个业务异常。

如果第二次进行完全相同的调用,我们将引发完全相同的异常。

技术异常

通常,这与硬件故障或无法访问的外部服务有关。例如;将用户信息存储到数据库中需要数据库。如果数据库离线,我们就会引发一个异常。如果第二次进行完全相同的调用,并且数据库重新在线,那么这个调用就会成功。

逻辑异常

逻辑异常是一种非常特殊的异常。它表现为技术异常(我们没有抛出它),但重新发送相同的调用将始终导致相同的异常。因此,从本质上讲,它是一个(未处理的)业务异常。例如

  • 无效数据转换,例如将 string 转换为整数
  • 外部接口的更改/外部组件版本不正确
  • 计算结果无效,例如“除以零”
  • 等等。

通常这是一个错误,在最广义的词义上,应该由开发人员修复(你不会认为我会忘记责怪开发人员吧?)。

示例

例如,一个(C#)应用程序,它获取单个用户的详细信息;

public string GetDetailsOfUserAsXml(string userIdentification)
{
  //Get the data
  Guid UserId = new Guid(userIdentification);
  DataSet.userDataTable UserDataSet;
  UserDataSet = GetDetailsOfUser(UserId); // Call to data access layer

  //Convert the data
  string DataToReturn;
  string Username = UserDataSet.Rows[0]["name"].ToString();
  string UserAge = UserDataSet.Rows[0]["age"].ToString();
  //Dirty solution to build XML but please ignore for this example :-)
  DataToReturn = "<data><name>" + Username + "</name>" +
                 "<age>" + UserAge + "</age></data>";
  return DataToReturn;
}

如果用户输入一个有效且存在的 GUID,那么该方法将遵循正常路径并返回一个响应,例如

<data><name>me</name><age>36</age></data>

一个典型的初级开发人员会说“好的,一切正常。完成。”

错误输入

不,我们还没有完成。让我们考虑一下用户没有输入 GUID 而是输入了名字“me”的情况。

你将得到一个丑陋的逻辑异常:“GUID 应该包含 32 位数字……”

这个异常是意料之中的,但它不能帮助用户解决问题。所以我们更新应用程序

public string GetDetailsOfUserAsXml(string userIdentification)
{
//Get the data
Guid UserId;
try
{
  UserId = new Guid(userIdentification);
}
catch (Exception)
{
  throw new Exception("Could not convert the received 
      userIdentification (" + userIdentification + ") to a guid");
}
…

现在产生的错误是:无法将收到的 userIdentification (me)转换为 GUID。

此服务的使用者现在知道我们期望的是一个 GUID 作为 userIdentification ,而不是单词“me”。

请注意,在这种情况下,用户是另一个开发人员,一个普通用户不知道 GUID 这个词的含义,更不用说将 GUID 作为参数输入了。

意外结果(1)

我们还没有完成。如果用户输入了一个数据库中不存在的标识符会发生什么?

又是一个丑陋的逻辑异常:“位置 0 没有行”。

这个特定的异常是不必要的,它是一个有效的 ID,但根本没有数据显示。让我们修复这个“错误”;

…
  //Convert the data
  string DataToReturn;
  string Username = string.Empty;
  string UserAge = string.Empty;
  if (UserDataSet.Rows.Count != 0)
  {
    Username = UserDataSet.Rows[0]["name"].ToString();
    UserAge = UserDataSet.Rows[0]["age"].ToString();
  }
  //Dirty solution to build XML but please ignore for this example :-)
  DataToReturn = "<data><name>" + Username + "</name>" +
                 "<age>" + UserAge + "</age></data>";
  return DataToReturn;
}

结果现在是一个空的 XML

<data><name/><age/></data>

意外结果(2)

你可能会想,还有什么可能出错。考虑一下:用户输入了属于“某人”的 GUID 作为参数,但得到了响应

<data><name>me</name><age>36</age></data>

嗯?这不是预期的结果,我们得到的是“我”的数据而不是“某人”的数据。唯一的原因是 ID 在数据库中出现了两次。

注意:这不完全是一个真实世界的例子,但可能发生在数据库级别的上传之后。

在这种情况下,输入是有效的,我们得到了一个有效的响应。但是,与上一章相反,现在我们必须引发异常而不是阻止异常。让我们也修复这个错误

…
//if (UserDataSet.Rows.Count == 0) do nothing, just use the empty strings
if (UserDataSet.Rows.Count == 1)
{
  Username = UserDataSet.Rows[0]["name"].ToString();
  UserAge = UserDataSet.Rows[0]["age"].ToString();
}
if (UserDataSet.Rows.Count > 1)
{
  throw new Exception("Could not identify a unique user using
    userIdentification '" + userIdentification + "', please contact your
    system administrator");
}
//Dirty solution to build XML but please ignore for this example :-)
DataToReturn = "<data><name>" + Username + "</name>" +
               "<age>" + UserAge + "</age></data>";
return DataToReturn;
}

结果现在是一个很好的业务异常:“无法使用 userIdentification 识别唯一用户……”

顺便说一句,不要忘记解雇 DBA,因为他们犯了如此(非唯一 ID)严重的错误。

缺少什么?

好吧,谈论了很多异常,但我没有使用 try-catch。为什么?
仅仅因为我没有计划对异常做任何事情,那为什么还要捕获它呢?

我只在必须向异常添加信息或在最高级别捕获异常。在后一种情况下,我向用户显示一个友好的消息框,或者将异常放入事件日志和/或日志文件中。

另请注意,不应使用异常来控制应用程序的流程。另请参阅下一章。

不该做什么(1)

异常的成本很高,不应该用于编写正常的应用程序流程逻辑。而应该添加额外的检查。请看下面的例子

public string TestFileLoopExceptionBased()
{
string Filename = "C:\\Inetpub\\wwwroot\\ExpSample\\App_Data\\TestDoc.txt";
string Result = string.Empty;
long StartTime = DateTime.Now.Ticks;
for (int i = 0; i < 100; i++)
{
  try
  {
    Result = System.IO.File.ReadAllText(Filename);
  }
  catch (Exception)
  { 
    //Ignore, file did not exist
  }
}
Result = (DateTime.Now.Ticks - StartTime).ToString();
return Result;
}
public string TestFileLoop()
{
string Filename = "C:\\Inetpub\\wwwroot\\ExpSample\\App_Data\\TestDoc.txt";
string Result = string.Empty;
long StartTime = DateTime.Now.Ticks;
for (int i = 0; i < 100; i++)
{
  if (System.IO.File.Exists(Filename))
  {
    Result = System.IO.File.ReadAllText(Filename);
  } 
}
  
Result = (DateTime.Now.Ticks - StartTime).ToString();
return Result;
}

结果

  • TestFileLoop (有效文件名)耗时 156250 个时钟周期
  • TestFileLoopExceptionBased (有效文件名)也耗时 156250 个时钟周期
  • TestFileLoop (无效文件名)再次耗时 156250 个时钟周期
  • TestFileLoopExceptionBased (无效文件名)耗时 4531250 个时钟周期

正如你所看到的,基于异常的方法多花了一点时间,多花了 29 倍!

请注意,在实际情况中,你应该结合使用这两种方法。TestFileLoop() 方法不包含任何额外的检查或保护措施来防止其他异常,例如

  • 如果文件无法打开怎么办? 
  • 如果无法确定文件长度怎么办? 
  • 如果无法分配足够的内存怎么办? 
  • 如果读取失败怎么办? 
  • 如果文件无法关闭怎么办?

此示例的目的是展示异常相对于逻辑检查而言有多昂贵(在 CPU 周期方面)。

不该做什么(2)

考虑以下代码片段

public string GetDetailsOfUserAsXml(string userIdentification)
{
try
{
//A lot of coding goes here ...
//Result could be <data><name/><age/></data>;
}
catch (Exception Exp)
{
return Exp.Message;
}
}

我没有引发异常,而是将错误消息(作为有效响应!)返回给调用方。一个可以使用此 Web 服务的小程序

public void btnGetData_Click()
{
//Get the details
string UserDataAsXml;
try
{
  UserDataAsXml = WebService.GetDetailsOfUserAsXml(textbox1.Text);
  //Parse the results, extract the name and display it
  XmlDocument UserData = new XmlDocument();
  UserData.LoadXml(UserDataAsXml);
  string UserName;
  UserName = UserData.SelectSingleNode("data/name").InnerText;
  MessageBox.Show("Username is " + UserName);
}
catch (Exception Exp)
{
  MessageBox.Show("Unable to display name because " + Exp.Message);
}
}

开发人员期望异常,如果发生异常,则显示一条消息,告知用户发生了什么。

现在分析如果在 GetDetailsOfUserAsXml 中发生异常会发生什么。假设返回的异常是“无法使用 userIdentification 'x' 识别唯一用户,请联系您的系统管理员”;

客户端收到一个答案(不是异常!),并尝试将其解析为 XML。这将导致一个实际的异常

System.Xml.XmlException: Data at the root level is invalid. Line 1, position 1 

此时,没有人知道哪里出了问题!

结论

几点提示

  • 如果你要捕获异常
    • 将它们呈现给用户
    • 如果你想在重新抛出异常之前添加额外信息
  • 如果结果与当前方法的预期不同,则添加异常。GetSingleRecord() 绝不应该返回多条记录!
  • 尽可能防止异常。
  • 在最后一个可能点捕获异常。通常,这发生在用户启动的事件中。
  • 异常中的消息是任何应用程序中用户实际阅读指令的少数几个地方之一!因此,请确保您的异常能够告诉用户一些事情;出了什么问题以及如何预防/修复它。
  • 异常的成本很高,不要基于异常编写正常的应用程序流程。而应该添加额外的检查。

历史

  • 2009 年 1 月 28 日:初始发布 
© . All rights reserved.