当我应该使用"调试"。Assert()?
-
02-07-2019 - |
题
我是一个专业软件工程师大约一年,现在,具有专业CS的程度。我知道断言为时在C++和C,但是没想到他们存在在C#。净在所有,直到最近。
我们的生产码包含任何声称任何和我的问题是这样的...
我应该开始使用称,在我们生产的代码?如果是这样,当被其使用最合适?会让更多的感觉做
Debug.Assert(val != null);
或
if ( val == null )
throw new exception();
解决方案
在调试Microsoft .NET 2.0应用程序中,John Robbins有一个很大的部分断言。他的要点是:
- 自由地断言。你永远不会有太多的断言。
- 断言不会替换异常。例外涵盖了您的代码所要求的内容;断言涵盖了它所假设的事物。
- 一个写得很好的断言不仅可以告诉你发生了什么以及在哪里(例如异常),而是为什么。
- 异常消息通常很神秘,要求您通过代码向后工作以重新创建导致错误的上下文。断言可以在错误发生时保留程序的状态。
- 断言加倍作为文档,告诉其他开发人员你的代码所依赖的隐含假设。
- 断言失败时出现的对话框允许您将调试器附加到进程,因此您可以在堆栈周围查看,就像在那里放置断点一样。 醇>
PS:如果您喜欢Code Complete,我建议您按照本书进行操作。我买它是为了学习使用WinDBG和转储文件,但是上半部分包含了一些提示,以帮助避免错误。
其他提示
在代码中的任何地方放置 Debug.Assert()
以进行完整性检查以确保不变量。当您编译Release版本(即,没有 DEBUG
编译器常量)时,对 Debug.Assert()
的调用将被删除,因此他们赢了不会影响表现。
在调用 Debug.Assert()
之前,你仍然应该抛出异常。断言只是确保在你还在开发的时候,一切都如预期的那样。
从代码完成
8防御性编程
8.2断言
断言是在开发期间使用的代码—通常是例程 或宏—允许程序在运行时自行检查。当一个 断言是真的,这意味着一切都按预期运作。 如果它是假的,那意味着它已经检测到了意外的错误 码。例如,如果系统假定客户信息 该程序可能永远不会有超过50,000条记录 包含一个断言记录数量少于或等于的断言 到50,000。只要记录数小于或等于 50,000,断言将是沉默。如果遇到的话多于 然而,它会大声地记录50,000条记录,并且会断言“#8220;有一个 程序中的错误。
断言在大而复杂的程序中特别有用 在高可靠性计划中。它们使程序员能够更快地完成任务 清除不匹配的接口假设,错误在何时蔓延 代码被修改,等等。
断言通常需要两个参数:布尔表达式 描述了假设“应该是真的”和“消息” 如果不是,则显示。
(…)
通常,您不希望用户看到断言消息 生产代码;断言主要用于开发期间 和维护。断言通常编译到代码中 开发时间并编译出生产代码。中 发展,断言消除了矛盾的假设, 意外情况,传递给例程的错误值,等等。 在生产过程中,它们是从代码中编译出来的 断言不会降低系统性能。
FWIW ...我发现我的公共方法倾向于使用 if(){throw; }
模式以确保正确调用该方法。我的私有方法倾向于使用 Debug.Assert()
。
我的想法是,使用我的私有方法,我是受控制的,所以如果我开始使用不正确的参数调用我自己的私有方法之一,那么我已经在某个地方打破了我自己的假设 - 我应该从来没有进入那个州。在生产中,这些私有断言理想上应该是不必要的工作,因为我应该保持我的内部状态有效和一致。与公共方法的参数对比,任何人都可以在运行时调用这些参数:我仍然需要通过抛出异常来强制执行参数约束。
此外,如果某些内容在运行时不起作用(网络错误,数据访问错误,从第三方服务检索到的错误数据等),我的私有方法仍然可以抛出异常。我的断言只是为了确保我没有打破我自己关于对象状态的内部假设。
使用断言检查开发人员的假设和例外,以检查环境假设。
如果我是你,我会这样做:
Debug.Assert(val != null);
if ( val == null )
throw new exception();
或者避免重复检查条件
if ( val == null )
{
Debug.Assert(false,"breakpoint if val== null");
throw new exception();
}
如果你想说在你的生产码(即发布版本),可以使用的痕迹。断言,而不是调试。断言。
这当然会增加开销你的生产可执行的。
此外,如果应用程序运行在用户接口模式,断言对话将显示默认情况下,这可能是一个有点令人不安的用户使用。
你可以替代这种行为除去DefaultTraceListener:看看文件跟踪。听众中的MSDN。
在摘要,
使用"调试"。断言自由地,以帮助捕捉虫子在调试版本。
如果您使用的痕迹。主张在用户接口模式,你可能想要删除的DefaultTraceListener为了避免令人不安的用户。
如果病情你在测试是什么你的应用程序不能处理,你最好扔一个例外,以确保执行不会继续下去。要知道,一个用户可以选择忽略一个断言。
断言用于捕获程序员(您的)错误,而不是用户错误。只有在用户无法触发断言时才应使用它们。例如,如果您正在编写API,则不应使用断言来检查API用户可以调用的任何方法中的参数是否为空。但是它可以用在私有方法中,而不是作为API的一部分公开,以断言你的代码在它不应该传递时永远不会传递null参数。
当我不确定时,我通常偏爱断言而不是断言。
我的书中大部分都没有。 在绝大多数情况下,如果你想检查一切是否理智,那么如果不是,就扔掉。
我不喜欢的是它使调试版本在功能上与发布版本不同。如果调试断言失败但功能在发布中有效,那么这有什么意义呢?当断言器长期离开公司并且没有人知道代码的那部分时,它会更好。然后,您必须花费一些时间来探索问题,看看它是否真的是一个问题。如果这是一个问题那么为什么不是第一个投掷的人呢?
对我来说,这表明使用Debug.Asserts你将问题推迟给别人,自己处理问题。如果事情应该是这样的话,那就不会抛出。
我想有可能是性能关键的情况,你想要优化你的断言,它们在那里很有用,但是我还没有遇到这种情况。
在短短的
Asserts
用于警卫和检查设计合同的约束,即:
Asserts
应为调试和非生产基础只。声称是通常被忽略的编译器中释放生成。Asserts
可以检查对于错误/意外的条件,这是在控制你的系统Asserts
是不是一种机制,用于第一线的验证用户输入或业务规则Asserts
应该 不 可用于检测到意想不到的环境条件(这是控制之外的代码)如内存,网络故障、数据库的失败,等等。虽然罕见,这些条件是可以预计(和应用的代码不能解决问题,如硬件故障或资源枯竭).通常的例外情形,将被抛-你的应用程序可以那么无论采取纠正行动(例如再试一数据库或网络操作,试图以释放缓存存储器),或中止优雅,如果例外不能进行处理。- 一个失败的断言应该是致命的系统-即不像一个例外,不要试图捕捉或处理失败
Asserts
-你的代码是操作在意想不到的领土。堆的痕迹和崩溃转储可用于确定哪里出了问题。
断言有巨大的好处:
- 协助寻找失踪的验证用户的投入,或上的错误较高一级的代码。
- 声称在代码基明确传达的假设作出的代码阅读器
- 主张将检查在运行中
Debug
基础之上。 - 一旦代码已被彻底的测试,重建的代码作为释放,将删除所表现的开销验证的假设(但与的利益,在后进行的调试建立将始终恢复检查,如果需要的话)。
...更详细
Debug.Assert
表示的一个条件而已,假定有关国家通过的其余部分代码的方框内部控制程序。这可能包括的国家提供的参数,国家成员一类的实例,或者返回的方法的呼吁是在其合同/设计的范围。通常,声称应崩溃的线/处理/程序的所有必要的信息(堆跟踪,崩溃的倾倒等),因为它们表明存在一个错误,或者未被考虑的条件,没有被设计(即不要尝试和捕捉或处理失败的断言),一个可能的例外的,当一个断言本身可能造成更大的破坏于错误(例如空中交通管制不会想要一个YSOD时,一架飞机去潜水艇,虽然它没有实际意义是否是"调试"建立应部署到生产...)
当你应该使用 Asserts?
-在任何时候在一系统,或者图书馆API或服务投入的一个函数或州一级的都是假设的有效的(例如当验证已经完成对用户输入的介绍层系统的商业和数据层的课程通常假定空检查,范围内检查,长串检查等在输入已经完成的).-共同的 Assert
检查包括其无效假设将导致一个空对象引用,零除数、数或日期算术溢出,一般的乐队/设计不行为(例如如果一个32位int是用于模型的一个人的年龄,这将是审慎的做法 Assert
年龄实际上是介于0和125或等值的-100和10^10没有设计)。
.净码的合同
中。净栈, 代码合同 可以用 此外,或作为替代 使用 Debug.Assert
.代码合同可进一步正式确定状态检查,并且可以协助检测违反行为的假设在-汇编时(或其后不久,如果作为背景检查在IDE)。
设计合同(DBC)检查可包括:
Contract.Requires
-合同的先决条件Contract.Ensures
-收缩后置条件Invariant
-表达了一种假设有关国家的对象在所有各点,在其使用寿命。Contract.Assumes
-安抚的静态检查时,呼吁非合同的装饰方法。
根据 IDesign标准,您应该
断言每一个假设。平均而言,每五行就是一个断言。
using System.Diagnostics;
object GetObject()
{...}
object someObject = GetObject();
Debug.Assert(someObject != null);
作为免责声明,我应该提一下,我认为实施这个IRL并不实用。但这是他们的标准。
仅在您希望为发布版本删除检查的情况下使用断言。请记住,如果不在调试模式下编译,则不会触发断言。
给出check-for-null示例,如果这是在仅内部API中,我可能会使用断言。如果它在公共API中,我肯定会使用显式检查并抛出。
所有断言都应该是可以优化的代码:
Debug.Assert(true);
因为它正在检查你已经假设的东西是真的。 E.g:
public static void ConsumeEnumeration<T>(this IEnumerable<T> source)
{
if(source != null)
using(var en = source.GetEnumerator())
RunThroughEnumerator(en);
}
public static T GetFirstAndConsume<T>(this IEnumerable<T> source)
{
if(source == null)
throw new ArgumentNullException("source");
using(var en = source.GetEnumerator())
{
if(!en.MoveNext())
throw new InvalidOperationException("Empty sequence");
T ret = en.Current;
RunThroughEnumerator(en);
return ret;
}
}
private static void RunThroughEnumerator<T>(IEnumerator<T> en)
{
Debug.Assert(en != null);
while(en.MoveNext());
}
在上文中,null参数有三种不同的方法。第一个接受它是允许的(它什么都不做)。第二个抛出调用代码处理的异常(或不抛出,导致错误消息)。第三种假设它不可能发生,并声称它是如此。
在第一种情况下,没有问题。
在第二种情况下,调用代码存在问题 - 它不应该使用null调用 GetFirstAndConsume
,因此它会返回异常。
在第三种情况下,这段代码存在问题,因为在调用它之前应该已经检查过 en!= null
,所以它不是真的是一个bug 。或者换句话说,它应该是理论上可以优化 Debug.Assert(true)
的代码,sicne en!= null
应始终 true 代码>!
我想我会再增加四个案例,其中Debug.Assert可能是正确的选择。
1)我在这里没有提到的是自动化测试期间Asserts可以提供的额外概念性覆盖。举个简单的例子:
如果某个更高级别的调用者被认为已扩展代码范围以处理其他方案的作者修改,理想情况下(!)他们将编写单元测试来覆盖这个新条件。这可能是完全集成的代码似乎工作正常。
然而,实际上已经引入了一个微妙的缺陷,但在测试结果中没有检测到。在这种情况下,被调用者变得不确定,只有发生才能提供预期结果。或许它已经产生了一个未被注意到的舍入错误。或导致错误在其他地方平均抵消。或者不仅授予所请求的访问权限,还授予不应授予的其他权限。等
此时,被调用者中包含的Debug.Assert()语句与单元测试驱动的新案例(或边缘案例)一起可以在测试期间提供宝贵的通知,原始作者的假设已经失效,并且如果没有额外的审核,则不应发布代码。单元测试的断言是完美的合作伙伴。
2)此外,某些测试编写起来很简单,但考虑到最初的假设,则成本高且不必要。例如:
如果只能从某个安全入口点访问对象,是否应该从每个对象方法对网络权限数据库进行额外查询以确保调用者具有权限?当然不是。也许理想的解决方案包括缓存或其他一些功能扩展,但设计并不需要它。当对象被附加到不安全的入口点时,Debug.Assert()将立即显示。
3)接下来,在某些情况下,产品在发布模式下部署时,可能无法对其全部或部分操作进行有用的诊断交互。例如:
假设它是嵌入式实时设备。抛出异常并在遇到格式错误的数据包时重新启动会适得其反。相反,该设备可以受益于尽力而为的操作,甚至可以在其输出中产生噪声。它也可能没有人机界面,记录设备,甚至在发布模式下部署时甚至可以被人物物理访问,并且通过评估相同的输出最好地提供对错误的认识。在这种情况下,自由断言和彻底的预释放测试比例外更有价值。
4)最后,某些测试是不必要的,因为被调用者被认为非常可靠。在大多数情况下,代码越多,可重复使用的代码越多,就越努力使其可靠。因此,来自调用者的意外参数的异常是常见的,但是来自被调用者的意外结果是Assert。例如:
如果核心 String.Find
操作声明它将在找不到搜索条件时返回 -1
,您可能能够安全地执行一个操作而不是三。但是,如果它实际返回 -2
,您可能没有合理的行动方案。将单纯的计算替换为单独测试 -1
值的计算是没有用的,并且在大多数发布环境中不合理地使用测试来破坏代码,以确保核心库按预期运行。在这种情况下,断言是理想的。
保持断言
关于断言存在一个常见的误解,由...颁布 编写编译器和语言环境的人。它去了 像这样的东西:
断言会给代码增加一些开销。因为他们检查的东西 这应该永远不会发生,它们只会被一个错误触发 码。代码经过测试和发货后,就不再存在了 需要,应该关闭,以使代码运行得更快。 断言是一个调试工具。
这里有两个明显错误的假设。首先,他们认为 测试找到所有的错误。实际上,对于任何复杂的程序你 甚至不太可能测试排列的微小百分比 你的代码将被接通(参见无情测试)。
其次,乐观主义者忘记了你的程序运行在 危险的世界。在测试过程中,老鼠可能不会啃咬 通信电缆,有人玩游戏不会耗尽内存,而且 日志文件不会填满硬盘。这些事情可能发生在 您的程序在生产环境中运行。你的第一行 防御正在检查任何可能的错误,而你的第二个正在使用 断言试图发现你错过的那些。
在将程序投放到生产环境时关闭断言 就像你曾经做过的那样,在没有网的情况下穿过高线 在实践中。有巨大的价值,但很难获得生命 险。
即使您确实遇到了性能问题,也请仅关闭这些问题 真正打击你的断言。
你应该总是使用第二种方法(抛出异常)。
此外,如果你正在进行生产(并且有一个发布版本),最好抛出一个异常(让应用程序在最坏的情况下崩溃),而不是处理无效值,并可能破坏客户的数据(可能要花费数千美元。)
您应该使用Debug.Assert来测试程序中的逻辑错误。编译器只能告诉您语法错误。因此,您应该定义使用Assert语句来测试逻辑错误。就像测试销售汽车的程序一样,只有蓝色的宝马应该获得15%的折扣。编译器可以告诉你关于你的程序在执行此操作时是否在逻辑上是正确的,但是断言语句可以。
我已经在这里阅读了答案,我认为我应该添加一个重要的区别。使用断言的方式有两种截然不同的方式。一个是作为临时开发人员的快捷方式,“这不应该真的发生,如果它确实让我知道所以我可以决定做什么”,有点像条件断点,对于你的程序能够继续的情况。另一种方法是在代码中对有效程序状态进行假设。
在第一种情况下,断言甚至不需要在最终代码中。您应该在开发期间使用 Debug.Assert
,如果/不再需要,可以删除它们。如果您想要离开它们或者您忘记删除它们没有问题,因为它们不会对Release版本产生任何影响。
但在第二种情况下,断言是代码的一部分。他们断言,你的假设是正确的,并且还要记录它们。在这种情况下,你真的想把它们留在代码中。如果程序处于无效状态,则不应允许其继续。如果您无法承受性能损失,则不会使用C#。一方面,如果调试器发生,它可能是有用的。另一方面,您不希望堆栈跟踪弹出您的用户,也许更重要的是您不希望他们忽略它。此外,如果它在服务中,它将永远被忽略。因此,在生产中,正确的行为是抛出异常,并使用程序的正常异常处理,这可能会向用户显示一条好消息并记录详细信息。
Trace.Assert
有完美的方法来实现这一目标。它不会在生产中删除,可以使用app.config配置不同的侦听器。
因此,对于开发,默认处理程序很好,对于生产,您可以创建一个简单的TraceListener,如下所示抛出异常并在生产配置文件中激活它。
using System.Diagnostics;
public class ExceptionTraceListener : DefaultTraceListener
{
[DebuggerStepThrough]
public override void Fail(string message, string detailMessage)
{
throw new AssertException(message);
}
}
public class AssertException : Exception
{
public AssertException(string message) : base(message) { }
}
在生产配置文件中:
<system.diagnostics>
<trace>
<listeners>
<remove name="Default"/>
<add name="ExceptionListener" type="Namespace.ExceptionTraceListener,AssemblyName"/>
</listeners>
</trace>
</system.diagnostics>
我不知道它在C#和.NET中是怎么回事,但是在C中,assert()只有在使用-DDEBUG编译时才能工作 - 如果没有编译,enduser将永远不会看到assert()。它仅适用于开发人员。我经常使用它,有时更容易跟踪错误。
我不会在生产代码中使用它们。抛出异常,捕获并记录。
在asp.net中也需要小心,因为断言可以显示在控制台上并冻结请求。