错误处理没有例外

时间:2009-08-13 14:32:59

标签: c# .net winforms exception-handling error-handling

在搜索SO以获取与业务规则验证相关的错误处理方法时,我遇到的只是结构化异常处理的示例。

MSDN和许多其他声誉良好的开发资源非常清楚,例外不能用于处理例行错误案例。它们仅用于特殊情况和可能由于不正确而发生的意外错误由程序员(但不是用户)使用。在许多情况下,用户错误(例如留空的字段)很常见,而我们的程序应该期望的事情,因此不是例外而不是候选人使用例外。

QUOTE:

  

请记住使用该术语   编程中的异常必须要做   随着思想的异常   应该代表一个例外   条件。特殊情况,由   他们的本性,通常不会   发生;所以你的代码不应该抛出   例外是其日常生活的一部分   操作

     

不要向信号抛出异常   常见的事件。考虑一下   使用备用方法进行通信   调用者的发生   事件和保留例外   什么东西真的出来了   普通的事情发生了。

例如,正确使用:

private void DoSomething(string requiredParameter)
{
if (requiredParameter == null) throw new ArgumentExpcetion("requiredParameter cannot be null");
// Remainder of method body...
}

使用不当:

// Renames item to a name supplied by the user.  Name must begin with an "F".
public void RenameItem(string newName)
{
   // Items must have names that begin with "F"
   if (!newName.StartsWith("F")) throw new RenameException("New name must begin with /"F/"");
   // Remainder of method body...
}

在上述情况下,根据最佳实践,最好将错误传递给UI而不涉及/要求.NET的异常处理机制。

使用上面的相同示例,假设有人需要对项目强制执行一组命名规则。什么方法最好?

  1. 让方法返回a 枚举结果? RenameResult.Success, RenameResult.TooShort, RenameResult.TooLong, RenameResult.InvalidCharacters等。

  2. 在控制器类中使用事件 向UI类报告?用户界面调用 控制器的RenameItem方法,然后处理一个 控制器引发的AfterRename事件 作为事件args的一部分重命名状态?

  3. 控制类直接引用 并从UI类调用一个方法 处理错误,例如ReportError(字符串文本)。

  4. 别的什么......?

  5. 基本上,我想知道如何在可能不是Form类本身的类中执行复杂验证,并将错误传递回Form类进行显示 - 但我不想让异常处理涉及它应该在哪里不被使用(即使它看起来容易得多!)


    根据对问题的回答,我觉得我必须以更具体的方式陈述问题:

    UI =用户界面,BLL =业务逻辑层(在这种情况下,只是一个不同的类)

    1. 用户在UI中输入值。
    2. UI向BLL报告值。
    3. BLL执行值的常规验证。
    4. BLL发现规则违规。
    5. BLL将规则违规返回给UI。
    6. UI接收BLL的回复并向用户报告错误。
    7. 由于用户输入无效值是常规的,因此不应使用例外。没有例外的正确方法是什么?

7 个答案:

答案 0 :(得分:4)

我认为您对预期信息的印象错误。这是我昨天从current edition of Visual Studio magazine(第19卷,第8期)开始的一个很好的引用。

  

要么会员履行合同,要么退出。期。没有中间立场。没有返回代码,有时没有返回代码,有时却没有。

应该谨慎使用异常,因为创建和抛出它们很昂贵 - 但是,它们是.NET框架通知客户端(我的意思是任何调用组件)错误的方式。

答案 1 :(得分:4)

我假设您正在创建自己的业务规则验证引擎,因为您没有提到您正在使用的那个。

我会使用异常,但我不会抛弃它们。您显然需要在某处累积评估状态 - 记录特定规则失败的事实,我将存储描述失败的Exception实例。这是因为:

  1. 例外是可序列化的
  2. 异常始终具有人类可读的Message属性,并且可以具有其他属性,以机器可读的形式记录异常的详细信息。
  3. 事实上,某些业务规则失败可能已被异常信号发出 - 例如FormatException。您可以捕获该异常并将其添加到列表中。
  4. 事实上,本月的MSDN杂志有一篇文章提到了.NET 4.0中新的AggregateException类,它是一组在特定上下文中发生的异常。


    由于您使用的是Windows窗体,因此应使用内置机制进行验证:Validating事件和ErrorProvider组件。

答案 2 :(得分:3)

您提供的示例是UI验证输入。

因此,一种好的方法是将验证与操作分开。 WinForms有一个内置的验证系统,但原则上它的工作原理如下:

ValidationResult v = ValidateName(string newName);
if (v == ValidationResult.NameOk)
    SetName(newName);
else
    ReportErrorAndAskUserToRetry(...);

此外,您可以在SetName方法中应用验证,以确保已检查有效性:

public void SetName(string newName)
{
    if (ValidateName(newName) != ValidationResult.NameOk)
        throw new InvalidOperationException("name has not been correctly validated");

    name = newName;
}

(请注意,这可能不是性能的最佳方法,但是在对UI输入应用简单验证检查的情况下,验证两次具有任何意义的可能性不大。或者,上述检查可能是完全作为一个仅调试的断言检查,以捕获程序员在没有首先验证输入的情况下调用该方法的任何尝试。一旦你知道所有调用者都遵守他们的契约,通常根本不需要发布运行时检查)

引用另一个答案:

Either a member fulfills its contract or it throws an exception. Period.

这件事遗漏的是:合同是什么?在“契约”中声明方法返回状态值是完全合理的。例如File.Exists()返回状态代码,而不是异常,因为这是它的合同。

但是,你的例子不同。在其中,您实际上执行两个单独的操作:验证和存储。如果SetName可以返回状态代码设置名称,它会尝试在一个中执行两个任务,这意味着调用者永远不知道它将展示哪种行为,并且必须具有特殊的案例处理对于那些情况。但是,如果将SetName拆分为单独的验证和存储步骤,则StoreName的合同可以是您传入有效输入(由ValidateName传递),如果不符合此合同,则会抛出异常。因为每个方法只做一件事而且只做一件事,合同非常明确,而且应该抛出异常是显而易见的。

答案 3 :(得分:2)

我同意Henk的建议。

传统上,“通过/失败”操作是作为具有整数或bool返回类型的函数实现的,该类型将指定调用的结果。但是,有些人反对这一点,指出“一个函数或方法应该执行一个动作或返回一个值,但不能同时返回两个值。”换句话说,返回值的类memeber也不应该是更改对象状态的类memeber。

我找到了在生成广告的类中添加 .HasErrors / .IsValid .Errors 属性的最佳解决方案错误。前两个属性允许客户端类测试是否存在错误,如果需要,还可以读取.Errors属性并报告包含的一个或所有错误。然后,每个方法都必须了解这些属性并适当地管理错误状态。然后,可以将这些属性转换为各种业务规则层外观类可以合并的IErrorReporting接口。

答案 4 :(得分:1)

在我看来,如果有疑问,请在业务规则验证中抛出异常。我知道这有点违反直觉,我可能会因此而受到抨击,但我坚持认为,因为什么是例行公事,什么不是取决于具体情况,是商业决策,而不是编程决策。

例如,如果您有WPF应用程序和WCF服务使用的业务域类,则在WPF应用程序中无效的字段输入可能是常规的,但是当在域中使用域对象时,这将是灾难性的。您正在处理来自其他应用程序的服务请求的WCF情况。

我想了很久很努力,想出了这个解决方案。对域类执行以下操作:

  • 添加属性:ThrowsOnBusinessRule。抛出异常时,默认值应为true。如果你不想扔它,请将其设置为false。
  • 添加私有字典集合以存储具有密钥作为具有业务规则违规的域属性的异常。 (当然,如果需要,可以公开公开)
  • 添加方法:ThrowsBusinessRule(string propertyName,Exception e)来处理上面的逻辑
  • 如果需要,可以实现IDataErrorInfo并使用Dictionary集合。鉴于上面的设置,IDataErrorInfo的实现是微不足道的。

答案 5 :(得分:0)

例外只是:一种处理特殊情况的方法,这种情况通常不会在您的应用程序中发生。提供的两个示例都是如何正确使用异常的合理示例。在这两个实例中,他们都确定已经调用了一个不允许发生的操作,并且对于应用程序的正常流程来说是例外。

误解是第二个错误,重命名方法,是唯一机制来检测重命名错误。永远不应将异常用作将消息传递到用户界面的机制。在这种情况下,您将有一些逻辑检查为重命名指定的名称在UI验证中的某处有效。此验证将使异常永远不会成为正常流程的一部分。

可以阻止“坏事”的发生,例如,它们可以作为API调用的最后防线,以确保错误停止并且只能发生合法行为。它们是程序员级别的错误,应该只表示发生以下情况之一:

  • 发生了灾难性和系统性的事情,例如内存不足。
  • 一个程序员已经编写了错误的程序,无论是对方法还是非法SQL的错误调用。

它们不应该是防止用户错误的唯一防线。用户需要的反馈和关注远远超过异常提供的范围,并且例行 尝试做一些超出预期应用程序流程的事情。

答案 6 :(得分:0)

我认为我几乎已经确信,抛出异常实际上是验证类型操作(尤其是聚合类型操作)的最佳做法。

以一个示例为例,更新数据库中的一条记录。这是许多可能个别失败的操作的总和。

例如。

  1. 检查数据库中是否有要更新的记录。 -记录可能不存在
  2. 验证要更新的字段。 -新值可能无效
  3. 更新数据库记录。 -数据库可能会引发其他错误

如果我们想避免出现异常,那么我们可能想使用可以保存成功或错误状态的对象:

public class Result<T> {
  public T Value { get; }
  public string Error { get; }

  public Result(T value) => Value = value;
  public Result(string error) => Error = error;

  public bool HasError() => Error != null;
  public bool Ok() => !HasError();
}

现在,我们可以在一些辅助方法中使用此对象:

public Result<Record> FindRecord(int id) {
  var record = Records.Find(id);
  if (record == null) return new Result<Record>("Record not found");
  return new Result<Record>(record);
}

public Results<bool> RecordIsValid(Record record) {
  var validator = new Validator<Record>(record);
  if (validator.IsValid()) return new Result<bool>(true);
  return new Result<bool>(validator.ErrorMessages);
}

public Result<bool> UpdateRecord(Record record) {
  try {
    Records.Update(record);
    Records.Save();
    return new Result<bool>(true);
  }
  catch (DbUpdateException e) {
    new Result<bool>(e.Message);
  }
}

现在将我们的聚合方法结合在一起:

public Result<bool> UpdateRecord(int id, Record record) {
  if (id != record.ID) return new Result<bool>("ID of record cannot be modified");

  var dbRecordResults = FindRecord(id);
  if (dbRecordResults.HasError())
    return new Result<bool>(dbRecordResults.Error);

  var validationResults = RecordIsValid(record);
  if (validationResults.HasError())
    return validationResults;

  var updateResult = UpdateRecord(record);
  return updateResult;
}

哇!真是一团糟!

我们可以更进一步。我们可以创建Result<T>的子类来指示特定的错误类型:

public class ValidationError : Result<bool> {
  public ValidationError(string validationError) : base(validationError) {}
}

public class RecordNotFound: Result<Record> {
  public RecordNotFound(int id) : base($"Record not found: ID = {id}") {}
}

public class DbUpdateError : Result<bool> {
  public DbUpdateError(DbUpdateException e) : base(e.Message) {}
}

然后我们可以测试特定的错误情况:

var result = UpdateRecord(id, record);
if (result is RecordNotFound) return NotFound();
if (result is ValidationError) return UnprocessableEntity(result.Error);
if (result.HasError()) return UnprocessableEntity(result.Error);
return Ok(result.Value);

但是,在上面的示例中,result is RecordNotFound总是返回false,因为它是Result<Record>,而UpdateRecord(id, record)返回Result<bool>

一些积极的方面: *它基本上可以工作 *避免异常 *当事情失败时它会返回好消息 * Result<T>类可以根据需要复杂。例如,在验证错误消息的情况下,也许它可以处理一系列错误消息。 * Result<T>的子类可用于指示常见错误

底片: *有些转换问题中T可能有所不同。例如。 Result<T>Result<Record> *这些方法现在正在执行多种操作,错误处理以及它们应该执行的操作 *非常冗长 *诸如UpdateRecord(int, Record)之类的聚合方法现在需要让他们自己关注调用的方法的结果。

现在使用例外...

public class ValidationException : Exception {
  public ValidationException(string message) : base(message) {}
}

public class RecordNotFoundException : Exception  {
  public RecordNotFoundException (int id) : base($"Record not found: ID = {id}") {}
}

public class IdMisMatchException : Exception {
  public IdMisMatchException(string message) : base(message) {}
}

public Record FindRecord(int id) {
  var record = Records.Find(id);
  if (record == null) throw new RecordNotFoundException("Record not found");
  return record;
}

public bool RecordIsValid(Record record) {
  var validator = new Validator<Record>(record);
  if (!validator.IsValid()) throw new ValidationException(validator.ErrorMessages)
  return true;
}

public bool UpdateRecord(Record record) {
  Records.Update(record);
  Records.Save();
  return true;
}

public bool UpdateRecord(int id, Record record) {
  if (id != record.ID) throw new IdMisMatchException("ID of record cannot be modified");

  FindRecord(id);
  RecordIsValid(record);
  UpdateRecord(record);
  return true;
}

然后在控制器操作中:

try {
  UpdateRecord(id, record)
  return Ok(record);
}
catch (RecordNotFoundException) { return NotFound(); }
// ...

该代码更加简单... 每个方法要么起作用,要么引发特定的异常子类... 没有检查方法是否成功的检查。 没有类型转换带有异常结果... 您可以轻松添加一个应用程序范围的异常处理程序,该处理程序根据异常类型返回正确的响应和状态代码... 在控制流中使用异常有很多优点……

我不确定负面因素是什么...人们说他们像GOTOs ...不是很确定为什么这很糟糕...他们还说性能不好...但是那又是什么呢?与进行的数据库调用相比,这有何不同?我不确定这些负面因素是否真的是正当理由。