关于c#:Debug.Assert与特定的抛出异常

关于c#:Debug.Assert与特定的抛出异常

Debug.Assert vs. Specific Thrown Exceptions

我刚开始浏览John Robbins的"调试MS .Net 2.0应用程序",并且因为他对Debug.Assert(...)的传福音而感到困惑。

他指出,良好实现的Asserts会在某种程度上存储错误状态,例如:

1
Debug.Assert(i > 3,"i > 3","This means I got a bad parameter");

现在,就个人而言,我似乎很疯狂,他如此喜欢在没有真正明智的"商业逻辑"评论的情况下重申他的考试,也许"因为flobittyjam widgitification过程,我必须永远不会发生i <= 3"。

所以,我认为我认为Asserts是一种低级别的"让我保护我的假设"的东西......假设一个人认为这是一个只需要在调试中做的测试 - 即你保护自己不受同事的影响和未来的程序员,并希望他们实际测试的东西。

但是我没有得到的是,他继续说除了正常的错误处理之外你还应该使用断言;现在我设想的是这样的:

1
2
3
4
5
Debug.Assert(i > 3,"i must be greater than 3 because of the flibbity widgit status");
if (i <= 3)
{
    throw new ArgumentOutOfRangeException("i","i must be > 3 because... i=" + i.ToString());
}

我通过Debug.Assert重复错误条件测试获得了什么?如果我们谈论一个非常重要的计算的仅调试双重检查,我想我会得到它...

1
2
double interestAmount = loan.GetInterest();
Debug.Assert(debugInterestDoubleCheck(loan) == interestAmount,"Mismatch on interest calc");

...但是我没有得到参数测试,这肯定值得检查(在DEBUG和Release版本中)......或者不是。我错过了什么?


断言不适用于参数检查。应始终进行参数检查(并且精确地根据文档和/或规范中指定的前提条件),并根据需要抛出ArgumentOutOfRangeException

断言用于测试"不可能"的情况,即(在程序逻辑中)假设的事情是真的。断言是告诉你这些假设是否因任何原因被破坏。

希望这可以帮助!


断言与异常抛出有沟通方面。

假设我们有一个带有Name属性和ToString方法的User类。

如果ToString是这样实现的:

1
2
3
4
5
public string ToString()
{
     Debug.Assert(Name != null);
     return Name;
}

它表示Name应该永远不会为null,如果是,则User类中存在错误。

如果ToString是这样实现的:

1
2
3
4
5
6
7
8
9
public string ToString()
{
     if ( Name == null )
     {
          throw new InvalidOperationException("Name is null");
     }

     return Name;
}

它表示如果Name为null,调用者正在使用ToString,并且应该在调用之前检查它。

两者的实施

1
2
3
4
5
6
7
8
9
10
public string ToString()
{
     Debug.Assert(Name != null);
     if ( Name == null )
     {
          throw new InvalidOperationException("Name is null");
     }

     return Name;
}

如果Name为null,那么User类中会出现bug,但我们还是要处理它。 (用户在打电话之前不需要检查姓名。)我认为这是罗宾斯推荐的那种安全措施。


在提供关于测试问题的调试与断言的指导时,我已经考虑过这个漫长而艰难的过程。

您应该能够使用错误的输入,错误的状态,无效的操作顺序以及任何其他可能的错误条件来测试您的类,并且断言应该永远不会跳闸。每个断言都在检查一些事情应该始终为真,无论输入或计算是什么。

我已经达到了良好的经验法则:

  • 断言不能替代能够正确独立于配置运行的健壮代码。它们是互补的。

  • 在单元测试运行期间,即使在输入无效值或测试错误条件时,断言也不应跳闸。代码应该处理这些条件而不会发生断言。

  • 如果断言跳闸(在单元测试中或在测试期间),则会对该类进行窃听。

  • 对于所有其他错误 - 通常是环境(网络连接丢失)或误用(调用者传递空值) - 使用硬检查和异常会更好,更容易理解。如果发生异常,则调用者知道它可能是他们的错。如果发生断言,则调用者知道它可能是断言所在代码中的错误。

    关于重复:我同意。我不明白为什么你会用Debug.Assert和异常检查来复制验证。它不仅会给代码增加一些噪音,而且还会让人感到困惑,因为这是一种重复的形式。


    我使用显式检查,在私有方法的公共和受保护方法和断言上抛出异常。

    通常,显式检查会保护私有方法无论如何都看不到错误的值。所以,断言正在检查一个应该是不可能的条件。如果一个断言触发,它告诉我在该类的一个公共例程中包含的验证逻辑中存在缺陷。


    可以捕获并吞下异常,使错误对测试不可见。 Debug.Assert不会发生这种情况。

    没有人应该有一个能够捕获所有异常的捕获处理程序,但无论如何人们都会这样做,有时这是不可避免的。如果您的代码是从COM调用的,则互操作层会捕获所有异常并将其转换为COM错误代码,这意味着您将看不到未处理的异常。断言不会受此影响。

    此外,当异常未得到处理时,更好的做法是采取小型转储。 VB比C#更强大的一个领域是,当异常处于运行状态时,您可以使用异常过滤器来捕捉小型转储,并保持其余的异常处理不变。 Gregg Miskelly关于异常过滤器注入的博客文章提供了一种从c#执行此操作的有用方法。

    关于资产的另一个注意事项......他们在单元测试代码中的错误条件时表现不佳。有一个包装器来关闭单元测试的断言是值得的。


    IMO只是失去了开发时间。正确实施的例外可让您清楚了解发生的情况。我看到太多的应用程序显示模糊的"断言失败:我<10"错误。我认为断言是一种临时解决方案。在我看来,没有断言应该在程序的最终版本中。在我的实践中,我使用断言进行快速和脏检查。代码的最终版本应考虑错误的情况并相应地采取行动。如果发生了不好的事情,你有2个选择:处理它或离开它。如果传入了错误的参数,函数应抛出一个带有意义描述的异常。我认为验证逻辑重复没有任何意义。


    很好地使用Assert的示例:

    1
    2
    Debug.Assert(flibbles.count() < 1000000,"too many flibbles"); // indicate something is awry
    log.warning("flibble count reached" + flibbles.count()); // log in production as early warning

    我个人认为只有当你知道某些东西超出了理想的限制时才应该使用Assert,但是你可以确定它是合理安全的。在所有其他情况下(随意指出我没有想到的情况)使用异常来快速失败。

    对我来说,关键的权衡是你是想要关闭带有异常的实时/生产系统以避免损坏并使故障排除更容易,或者是否遇到了一个永远不允许在测试/调试版本中继续被忽视的情况但是被允许继续生产(当然记录警告)。

    比照http://c2.com/cgi/wiki?FailFast
    从java问题复制和修改:异常与断言


    这是2美分。

    我认为最好的方法是使用断言和异常。两个方法之间的主要区别,即imho,如果Assert语句可以从应用程序文本中轻松删除(定义,条件属性......),而抛出的Exception依赖于(tipically)一个更难删除的条件代码(具有预处理器条件的多段)。

    每个应用程序异常都应该正确处理,而断言只应在算法开发和测试期间得到满足。

    如果将空对象引用作为例程参数传递,并且使用此值,则会得到空指针异常。确实:你为什么要写一个断言?在这种情况下浪费时间。
    但是在课程例程中使用的私人班级成员呢?当在某处设置这些值时,如果设置了空值,最好使用断言进行检查。这只是因为当你使用该成员时,你得到一个空指针异常,但你不知道该值是如何设置的。这导致重新启动程序,在所有入口点使用时设置私有成员。

    异常更有用,但它们可能(imho)非常重,无法管理,并且有可能使用太多异常。并且它们需要额外的检查,可能不希望优化代码。
    我个人只在代码需要深度捕获控件(调用堆栈中的catch语句非常低)或者代码中没有硬编码函数参数时才使用异常。


    推荐阅读