【问题标题】:try/catch vs. if/then/else - Specific Casetry/catch vs. if/then/else - 具体情况
【发布时间】:2017-06-08 09:50:15
【问题描述】:

在每个人都支持代码正确性之前,我意识到通常正确的做事方式是不使用 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;
    }
}

因此,在上面的示例中,第一个方法检查字典是否包含键,如果包含,则检查字段是否为空,如果子值不等于特定值,则返回 true值,否则返回 false。这避免了尝试/捕获,但是每次访问代码时,它都会检查字典以查看密钥是否存在(假设没有引擎盖下的缓存/等 - 如果有请告诉我) ,然后检查一个字段是否为空,然后是我们关心的实际检查。

在第二种方法中,我们使用try/catch,避免了多层控制逻辑。我们立即“开门见山”并尝试访问有问题的值,如果它以两种已知方式中的任何一种失败,它将返回 false。如果代码执行成功,可能返回true或false(同上)。

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

当然,对于这个确切的例子,差异可以忽略不计 - 但是,想象一个复杂的场景,在一个复杂的 if/then/else 树中进行大量检查,与带有失败条件列表的 try/catch 相比。我意识到是的,这种代码总是可以分解成更小的位或以其他方式重构,但为了论证,我们说它除了控制流之外不能更改(在某些情况下需要更改实际逻辑重新验证科学算法,例如,成本高昂/速度慢,需要尽量减少)。

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

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

【问题讨论】:

  • 我不明白你为什么要这样做“return aDictionary[aString].Field.SubField != "someValue";"而不是 "return aStringField.SubField != "someValue"";这样你就不会检查字典两次
  • 例外适用于“例外”情况。在我看来,字典中不存在的键并不例外。第一个例子更容易理解。
  • 这有点太宽泛了,有点基于意见。 IBTL,但除此之外,还有更多的理由避免使用异常进行流控制,而不仅仅是性能。如果字典应该存在但为空,会发生什么?您唯一的线索是您返回“false”,这很可能来自尝试实际通过您的条件,也可能来自您隐藏的合法异常。
  • @Koenyn - 这是一个错误,感谢您发现它。
  • 也许你会对Dictionary.TryGetValue 和 C# 6 中新的 null 条件运算符感兴趣,它可以让你做 return aDictionary[aString]?.Field?.SubField ?? string.Empty != "someValue"

标签: c# if-statement exception-handling try-catch control-structure


【解决方案1】:

您似乎同意不使用boneheaded exceptions 来控制程序流是一种正确的做法

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

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

性能。

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

我们不知道dictionary 中数据的确切模式。如果在属性钻取期间没有任何东西可以引发异常,那么您可能会更好地处理异常。

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

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

可读性

没有人会争辩说这段代码比有很多ifs 的代码短。而且要犯任何错误要困难得多。

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

正确性/等价性。

为什么不完全等效

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

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

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


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

【讨论】:

  • 这正是我希望得到的答案——关于该主题的清晰、简洁的事实和观点,对任何一种观点都没有太大的偏见。谢谢!我会更喜欢 if/then/else 除非特定情况的性能成本足够显着以保证在处理这些情况时偏离规范。在这个特定的例子中,那些将是“电影情节威胁”,但这是一个例子,现实世界很可能有一些这样的潜在失败,我同意。再次感谢您。
【解决方案2】:

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

    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 不应该被抛出,除非例程有一个错误。调试似乎是一个艰难的过程,请不要把它变成噩梦。

【讨论】:

  • 是在抛出异常时构建堆栈跟踪,还是在代码执行时构建和维护堆栈跟踪,即使没有出现任何问题?如果它在代码执行时保持不变,那么我完全同意。同样,我同意您的解决方案,但就像 cmets 中的另一个解决方案一样,我认为这通过解决这个 exact 场景来回避这个思考练习的重点,而这个问题意味着稍微更笼统。不过,也许这让它太宽泛了。
  • @Yushatak:每当抛出异常时,它都会进行堆栈跟踪。当没有任何问题(没有抛出异常)时,不会执行堆栈跟踪;所以试试{...} finally {...} 很快; try {} catch {...} 也很快,只要不抛出异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-11-15
  • 1970-01-01
  • 2019-04-06
  • 2017-04-07
  • 1970-01-01
  • 1970-01-01
  • 2012-03-09
相关资源
最近更新 更多