错误处理没有例外

Jam*_*mes 21 .net c# error-handling exception-handling winforms

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

MSDN和许多其他声誉良好的开发资源非常明确,不能使用异常来处理常规错误情况. 它们仅用于特殊情况和程序员(而不是用户)不当使用时可能发生的意外错误.在许多情况下,用户错误(例如空白的字段)很常见,我们程序应该这样做期待,因此不是例外,也不是使用例外的候选人.

引用:

请记住,在编程中使用术语例外与认为异常应该代表异常条件的想法有关.特殊情况,就其本质而言,通常不会发生; 因此,您的代码不应该将异常作为其日常操作的一部分.

不要抛出异常来表示常见事件.考虑使用备用方法向调用者传达这些事件的发生,并在真正与众不同的事件发生时抛出异常.

例如,正确使用:

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

使用不当:

// 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...
}
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,根据最佳实践,最好将错误传递给UI而不涉及/要求.NET的异常处理机制.

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

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

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

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

  4. 别的......?

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


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

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

  1. 用户在UI中输入值.
  2. UI向BLL报告值.
  3. BLL执行值的常规验证.
  4. BLL发现规则违规.
  5. BLL将规则违规返回给UI.
  6. UI接收来自BLL的返回并向用户报告错误.

由于用户输入无效值是常规的,因此不应使用异常.没有例外,这样做的正确方法是什么?

Joh*_*ers 5

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

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

  1. 异常是可序列化的
  2. 异常总是有一个Message人类可读的属性,并且可以有额外的属性来以机器可读的形式记录异常的详细信息。
  3. 一些业务规则失败实际上可能是由异常发出的信号——FormatException例如 a 。您可以捕获该异常并将其添加到列表中。

事实上,本月的 MSDN 杂志有一篇文章提到了AggregateException.NET 4.0中的新类,它是在特定上下文中发生的异常的集合。


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


Jas*_*ams 4

您给出的示例是 UI 验证输入。

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

ValidationResult v = ValidateName(string newName);
if (v == ValidationResult.NameOk)
    SetName(newName);
else
    ReportErrorAndAskUserToRetry(...);
Run Code Online (Sandbox Code Playgroud)

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

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

    name = newName;
}
Run Code Online (Sandbox Code Playgroud)

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

引用另一个答案:

成员要么履行其合同,要么抛出异常。时期。

这忽略了一个问题:合同是什么?在“契约”中声明方法返回状态值是完全合理的。例如,File.Exists() 返回一个状态代码,而不是异常,因为这是它的约定。

但是,您的示例有所不同。在其中,您实际上执行两个单独的操作:验证和存储。如果 SetName 可以返回状态代码设置名称,则它正在尝试将两项任务合二为一,这意味着调用者永远不知道它将表现出哪种行为,并且必须对这些情况进行特殊情况处理。但是,如果将 SetName 拆分为单独的 Validate 和 Store 步骤,则 StoreName 的约定可以是您传入有效输入(由 ValidateName 传递),并且如果不满足此约定,则会引发异常。因为每个方法只做一件事,而且只做一件事,所以契约非常清晰,并且何时应该抛出异常是显而易见的。