6

首先,我知道标准答案是永远不会将异常用于流量控制。虽然我完全同意这一点,但我一直在思考我有时会做的事情,我将用以下伪代码来描述:

try
    string keyboardInput = read()
    int number = int.parse(keyboardInput)
    //the conversion succeeds
    if(number >= 1000) 
        //That's not what I asked for. The message to display to the user
        //is already in the catch-block below.
        throw new NumberFormatException() //well, there IS something wrong with the number...
 catch(NumberFormatException ex)  //the user entered text
    print("Please enter a valid number below 1000.")

首先,以非常抽象的方式来举这个例子。这不一定要发生。情况很简单:

用户输入需要受到约束,并且可能以两种方式出错,一种是语言定义的抛出异常,另一种是检查。用户以相同的方式报告这两个错误,因为他们不需要知道导致它的技术差异。

我想了几种方法来解决它。首先,最好抛出一个定制的异常。然后我面临的问题是,如果我在本地捕获它,如何处理另一个异常?在 se 中,自定义异常将导致第二个 catch 块,其中消息也将被复制到其中。我的解决方案:

//number is wrong
throw new MyException()
catch(NumberFormatException ex) 
    throw new MyException()
catch(MyException ex) {
    print("Please enter...")

异常名称的含义就是这里的一切。这种自定义异常的应用程序被广泛接受,但基本上我没有做任何与第一种方式不同的事情:我被迫进入一个 catch-block,尽管抛出一个自定义异常而不是标准库异常。

将异常抛出到调用方法的相同方法(因此没有自定义异常的 catch 块)似乎更有意义。我的方法在技术上可能有两种方式出错,但本质上是一种方式:错误的用户输入。因此,人们会编写一个UserInputException并让方法抛出这个。新问题:如果这是应用程序的主要方法怎么办?

我目前并没有为实现这种行为的特定应用程序而苦苦挣扎,我的问题纯粹是理论上的和非语言特定的。

解决这个问题的最佳方法是什么?

4

6 回答 6

5

我会认为第一个异常是低级的,我会在调用点处理它(在这种情况下通过翻译)。我发现这导致代码更容易维护和稍后重构,因为您需要处理的异常类型更少。

try
  string keyboardInput = read()
  try
    int number = int.parse(keyboardInput)
  catch(NumberFormatException ex)
    throw MyException("Input value was not a number")

  //the conversion succeeds
  if(number >= 1000) 
    throw MyException("Input value was out of range")

catch(MyException ex)  //the user entered text
  print( ex.ToString() )
  print("Please enter a valid number below 1000.")
于 2012-05-15T02:37:29.907 回答
1

在此特定示例中,您不需要任何例外。

int number;
if (int.TryParse(keyboardInput, out number) && number < 1000) // success
else // error

但是,您描述的情况在商业软件中很常见,并且抛出异常以到达统一的处理程序是很常见的。

一种这样的模式是 XML 验证,然后是 XSLT。在某些系统中,无效 XML 是通过捕获验证异常来处理的。在这些系统中,重用 XSLT 中现有的异常处理是很自然的(它可以自然地检测到特定验证语言无法检测到的某些类型的数据错误):

<xsl:if test="@required = 'yes' and @prohibited = 'yes'>
    <xsl:message terminate='yes'>Error message</xsl:message>
</xsl:if>

重要的是要看到,如果这种情况极为罕见(预计仅在早期集成测试期间发生,并且随着其他模块中的缺陷得到修复而消失),则大多数关于不使用异常进行流控制的典型问题并不真正适用。

于 2012-05-09T09:58:58.263 回答
1

我的看法是这样的:

假设没有其他方法可以解析不会引发异常的 int ,那么您现在的代码是正确且优雅的。

唯一的问题是您的代码是否处于某种循环中,在这种情况下,您可能会担心抛出和捕获不必要的异常的开销。在这种情况下,您将不得不牺牲一些代码的美感,而只在必要时处理异常。

error=false;

try {
    string keyboardInput = read();
    int number = int.parse(keyboardInput);
    //the conversion succeeds
    if(number >= 1000) {
        //That's not what I asked for. The message to display to the user
        //is already in the catch-block below.
        error=true;
} catch(NumberFormatException ex) { //the user entered text
    error=true;
}

if (error)
    print("Please enter a valid number below 1000.");

您还可以考虑为什么要尝试将两个错误聚合为一个。相反,您可以告知用户他们犯了什么错误,这在某些情况下可能更有帮助:

try {
    string keyboardInput = read();
    int number = int.parse(keyboardInput);
    //the conversion succeeds
    if(number >= 1000) {
        //That's not what I asked for. The message to display to the user
        //is already in the catch-block below.
        print("Please enter a number below 1000.");

} catch(NumberFormatException ex) { //the user entered text
    print("Please enter a valid number.");
}
于 2012-05-09T10:00:05.557 回答
1

我认为您基本上有几种方法可以在考虑到代码重复最少的情况下进行处理:

  1. 使用布尔变量/存储异常:如果您正在执行的特定任务的一般逻辑中的任何地方出现错误,您会在第一个错误迹象时退出并在单独的错误处理分支中处理它。

    优点:只有一处处理错误;您可以使用任何您喜欢的自定义异常/错误条件。

    缺点:你试图实现的逻辑可能很难发现。

  2. 创建一个通用函数,您可以使用它来通知用户错误(预先计算/存储所有描述一般错误的信息,例如显示用户的消息),这样您就可以在出现错误时调用一个函数发生。

    优点:对于代码的读者来说,你的意图逻辑可能更清晰;您可以使用您喜欢的 anu 自定义异常/错误条件。

    缺点:错误必须在不同的地方处理(尽管使用预先计算/存储的值,没有太多的复制粘贴,但是通知用户部分很复杂)。

  3. 如果意图很明确,我认为从 try 块中显式抛出异常不是一个坏主意。如果您不想抛出系统提供的异常之一,您始终可以创建自己的从其中之一派生的异常,因此您只需要最少数量(最好是一个)的 catch 块。

    优点:只有一个地方可以处理错误情况——如果在 try-block 中基本上只有一种类型的异常抛出。

    缺点:如果抛出不止一种类型的异常,则需要嵌套的 try-catch 块(将异常传播到最外层的异常)或非常通用的(例如 Exception)catch 块,以避免重复错误报告。

于 2012-05-15T02:07:49.533 回答
1

如何通过编写几个接受输入并返回错误或不返回错误的验证器类来解决这个验证问题。至于您与异常的斗争:将该逻辑放入每个验证器中,并根据具体情况进行处理。

之后,您找出正确的验证器用于您的输入,收集它们的错误并处理它们。

这样做的好处是:

  1. 验证器只做一件事,验证单个案例
  2. 由验证功能决定如何处理错误。您是在第一次验证错误时中断还是将它们全部收集然后处理它们?
  3. 您可以编写代码,使主验证函数可以使用相同的代码验证不同类型的输入,只需使用您最喜欢的技术选择正确的验证器。

和缺点:

  1. 您最终将编写更多代码(但如果您使用的是 java,则应将其放入“好处”桶中)

这是一些示例伪代码:

validate(input):
   validators = Validator.for(input.type)
   errors = []
   for validator in validators:
       errors.push(validator.validate(input))

   if errors:
       throw PoopException          

和一些验证器:

MaxValidator extends IntValidator: 
    validate(input):
        errors = []
        errors.push(super.validate(input))
        if input > 1000:
            errors.push("bleee!!!! to big!")
        return errors

IntValidator:
     validate(input):
        try:
           int.parse(input)
        catch NumberFormatException:
           return ['not an int']
        return []

当然,您需要做一些技巧来使父验证器可能返回输入的有效版本,在这种情况下,字符串“123”转换为 int,以便最大验证器可以处理它,但这可以通过以下方式轻松完成使验证器有状态或其他一些魔法。

于 2012-05-15T02:20:35.543 回答
1

我在这里的任何地方都看不到这个答案,所以我将其作为另一种观点发布。

众所周知,如果您对规则足够了解,您实际上可以打破规则,因此如果您知道这是适合您的情况的最佳解决方案,则可以使用抛出异常进行流量控制。从我所见,它通常发生在一些愚蠢的框架中......

也就是说,在 Java 7(它给我们带来了强大的构造)之前,这是我避免代码重复的方法:multicatch

try {
    someOffendingMethod();
} catch (Exception e) {
    if (e instanceof NumberFormatException || e instanceof MyException) {
        System.out.println("Please enter a valid number.");
    }
}

这也是 C# 中的一种有效技术。

于 2012-05-21T19:22:48.657 回答