try/catch 与 if/then/else - 具体案例

try/catch vs. if/then/else - Specific Case

在每个人都拥护代码正确性之前,我意识到通常正确的做事方式是不使用 try/catch 来控制流程。但是,这有点极端,我想了解其他人在这种情况下会怎么做。

这是示例代码,不是实际代码,但它应该能说明问题。如果有人想让我为这里的类型定义一个字典或一些模拟 类,请告诉我,我会的。

public bool IsSomethingTrue1(string aString)
{
    if (aDictionary.ContainsKey(aString)) //If the key exists..
    {
        var aStringField = aDictionary[aString].Field; //Get the field from the value.
        if (aStringField != null) //If the field isn't null..
        {
            return aStringField.SubField != "someValue"; //Return whether a subfield isn't equal to a specific value.
        }
    }
    return false; //If the key isn't found or the field is null, return false.
}

public bool IsSomethingTrue2(string aString)
{
    try
    {
        return aDictionary[aString].Field.SubField != "someValue"; //Return whether the subfield isn't equal to a specific value.
    }
    catch (KeyNotFoundException) //The key wasn't in the dictionary..
    {
        return false;
    }
    catch (NullReferenceException) //The field (or the dictionary, if it is dynamically assigned rather than hardcoded) was null..
    {
        return false;
    }
}

因此,在上面的示例中,第一个方法检查字典是否包含键,如果包含,则检查字段是否为 null,如果子值 returns true不等于特定值,否则 return 为假。这避免了 try/catch,但每次访问代码时,它都会对字典执行检查以查看密钥是否存在(假设没有底层 caching/etc。-如果有,请告诉我有任何问题),然后检查字段是否为空,然后是我们关心的实际检查。

在第二种方法中,我们使用try/catch并避免了多层控制逻辑。我们立即去 "straight to the point" 并尝试访问有问题的值,如果它以两种已知方式中的任何一种失败,它将 return false。如果代码执行成功,可能returntrue或false(同上)

这两种方法都能完成工作,但我的理解是,如果在大多数情况下没有出错,第一种方法平均会更慢。第一种方法每次都必须检查所有内容,而第二种方法只需要处理出现问题的情况。将有一个额外的 space 用于堆栈上的异常,以及一些用于跳转到不同捕获 blocks/etc 的指令。这应该不会影响正常情况下的性能,除此之外没有任何错误一个堆栈变量,但是,如果我正确理解我在这里阅读的内容: Do try/catch blocks hurt performance when exceptions are not thrown?

当然,对于这个确切的例子,差异可以忽略不计 - 然而,想象一个复杂的场景,在一个复杂的 if/then/else 树中进行大量检查,而 try/catch 具有一个列表故障条件。我意识到是的,这种代码总是可以分解成更小的部分或以其他方式重构,但为了论证,我们假设除了控制流之外不能改变它(在某些情况下,改变实际逻辑需要重新验证科学算法,例如,costly/slow 需要最小化)。

教科书 我意识到答案是使用第一种方法,但在某些情况下,第二种方法可能 显着 更快。

同样,我知道这有点迂腐,但我真的很想确保在重要的地方编写尽可能高效的代码,同时保持所有内容的可读性和可维护性(以及良好的注释)。在此先感谢您为我提供有关此事的意见!

Exceptions 应该用在 异常 情况下(例如,当出现问题时:找不到文件,连接断开等)。它们非常(堆栈跟踪消耗时间)。在你的实现中,密钥的缺失是一个相当routin的情况:

    public bool IsSomethingTrue2(string aString) {
      if (null == aString)
        return false; 

      MyType value;

      if (!aDictionary.TryGetValue(aString, out value))
        return false;

      return (null == value) 
        ? false 
        : null == value.Field 
           ? false
           : value.Field.SubField == "someValue"; 
    }

另一个问题是 KeyNotFoundExceptionNullReferenceException 不应该被抛出,除非例程有 bug。调试似乎是一个艰难的过程,请不要把它变成噩梦。

您似乎同意不使用 boneheaded exceptions 来控制程序流是正确的事情

尽管如此,您仍然更喜欢 try-catch 解决方案,因为它是一种简洁且相对清晰的方法,并且由于缺少 null/presence 检查以及异常抛出和处理,它可能会更快可能没那么糟糕 performance-wise.

这是一个很好的推理,但有几个时刻我们应该更加小心:

性能。

对于与性能相关的任何事情,您只能(相对)确定一件事 - 基准测试。 测量一切

我们不知道 dictionary 中数据的确切模式。如果在 属性 钻取过程中没有任何东西可以抛出异常,那么使用异常可能会更好。

但是与异常抛出相比,实际空值比较/TryGetValue 方法的成本微不足道。

再次 - 根据示例数据对其进行衡量,考虑此代码的执行频率,然后才对 acceptable/unacceptable 性能做出任何决定。

可读性

没有人会争辩说这段代码比有很多 if 的代码短。而且更难出错。

但是所有这些可读性背后​​隐藏着一个谬误 - 这样的 try-catch 代码并不完全等同于原始代码。

Correctness/equivalence.

为什么不完全等价

  1. 因为使用的字典可能不是Dictionary<String, T>,而是一些可能有缺陷的自定义IDictionary,导致在内部计算期间NullReferenceException
  2. 或者存储的 T 对象可能是具有内部对象的代理对象,在某些情况下开始初始化为 null,导致 NullReferenceException in T.SomeProp
  3. 或者 T 可以是另一个 Dictionary 中某个对象的代理,它实际上不存在,因此 KeyNotFoundException.

在所有这些情况下,我们都会忽略潜在的缺陷。

这难道不是 movie plot threat 在您的特定情况下不会发生,您可能会说?也许,也许不是。您可能会决定接受这种相当小的风险,但这种情况并非不可能,因为系统会更改,而此代码不会。


总而言之,我宁愿坚持第一个 noexcept 解决方案。它有点长,但它完全按照它说的去做,无论如何不会有明显更差的性能,并且不会无缘无故地抛出异常。