断言(软件开发)

维基百科,自由的百科全书
跳转到导航跳转到搜索
本文是关于计算机编程概念的。对于安全性断言标记语言(SAML)开放标准的上下文中的断言,请参阅安全性断言标记语言§断言。
在计算机编程,使用特别是当命令式编程范式,一个断言是谓词(一个布尔值函数在状态空间,通常表示为逻辑命题使用变量连接到在程序的点的程序的),即始终应该在代码执行中评估为true。断言可以帮助程序员读取代码,帮助编译器编译代码,或帮助程序检测自己的缺陷。

对于后者,一些程序通过在谓词运行时实际评估谓词来检查断言。然后,如果它实际上不是真的 - 断言失败 - ,程序认为自己被破坏并且通常故意崩溃或抛出断言失败异常。

##内容
1 详情
2 用法
2.1 合同设计中的断言
2.2 运行时检查的断言
2.3 开发周期中的断言
2.4 生产环境中的断言
2.5 静态断言
3 禁用断言
4 与错误处理的比较
5 历史
6 另见
7 参考文献
8 外部链接
详情
以下代码包含两个断言,x > 0并且x > 1在执行期间它们确实在指定的点处为真:

x = 1 ;
断言 x > 0 ;
x ++ ;
断言 x > 1 ;
程序员可以使用断言来帮助指定程序并推理程序的正确性。例如,放置在代码段开头的前提条件 - 断言 - 确定程序员期望代码执行的状态集。甲后置在-放置最终描述了在执行结束时的预期状态。例如:x > 0 { x++ } x > 1。

上面的示例使用符号来包含CAR Hoare在1969年的文章中使用的断言。[1]该符号不能用于现有的主流编程语言。但是,程序员可以使用其编程语言的注释功能包含未经检查的断言。例如,在C中:

x = 5 ;
x = x + 1 ;
// {x> 1}
注释中包含的大括号有助于将注释的使用与其他用途区分开来。

库也可以提供断言功能。例如,在C中使用glibc并支持C99:

#include <assert.h>

int f (void )
{
int x = 5 ;
x = x + 1 ;
断言(x > 1 );
}
一些现代编程语言包括已检查的断言 - 在运行时或有时静态检查的语句。如果断言在运行时评估为false,则会导致断言失败,这通常会导致执行中止。这引起了对检测到逻辑不一致的位置的关注,并且可能优先于否则将导致的行为。

断言的使用有助于程序员设计,开发和推理程序。

用法
在诸如Eiffel之类的语言中,断言构成了设计过程的一部分; 其他语言(如C和Java)仅用于检查运行时的假设。在这两种情况下,可以在运行时检查它们的有效性,但通常也可以抑制它们。

合同设计中的断言
断言可以作为一种形式的文档:它们可以描述代码在运行之前期望找到的状态(它的前置条件),以及代码在运行完成后期望产生的状态(后置条件); 他们还可以指定不变量 a的类。Eiffel将这些断言集成到语言中并自动提取它们以记录该类。这构成了合同设计方法的重要组成部分。

这种方法在没有明确支持它的语言中也很有用:在注释中使用断言语句而不是断言的优点是程序可以在每次运行时检查断言; 如果断言不再成立,则可以报告错误。这可以防止代码与断言失去同步。

运行时检查的断言
断言可以用于验证程序实现期间程序员在程序执行时保持有效的假设。例如,请考虑以下Java代码:

int total = countNumberOfUsers ();
if (total % 2 == 0 ) {
// total is even
} else {
// total is odd and non-negative
assert total % 2 == 1 ;
}
在Java中,%是余数运算符(modulo),在Java中,如果它的第一个操作数是负数,结果也可以是负数(与数学中使用的模数不同)。程序员假设这total是非负的,因此2的除法的余数将始终为0或1.断言使此假设显式:如果countNumberOfUsers确实返回负值,则程序可能有错误。

这种技术的一个主要优点是,当确实发生错误时,会立即直接检测到错误,而不是通过其常常模糊的副作用。由于断言失败通常会报告代码位置,因此通常可以精确定位错误而无需进一步调试。

断言有时也放在执行不应达到的点上。例如,断言可以放在语句的default子句中,switch例如C,C ++和Java。程序员故意不处理的任何情况都会引发错误,程序将中止而不是默默地继续处于错误状态。在D中,当switch语句不包含default子句时,会自动添加断言。

在Java中,自版本1.4以来,断言一直是语言的一部分。断言失败导致AssertionError程序在使用适当的标志运行时引发,如果没有该标志,则忽略断言语句。在C中,它们被assert.h定义为宏的标准头添加,该宏在发生故障时发出错误信号,通常终止程序。在C ++中,both 和headers都提供了宏。 assert (assertion) assert.hcassertassert

断言的危险在于它们可能通过更改内存数据或更改线程时序来引起副作用。应谨慎实施断言,以免对程序代码产生任何副作用。

语言中的断言构造允许在不使用第三方库的情况下轻松进行测试驱动开发(TDD)。

开发周期中的断言
在开发周期中,程序员通常会在启用断言的情况下运行程序。当发生断言失败时,程序员会立即收到问题通知。许多断言实现也将停止程序的执行:这很有用,因为如果程序在发生断言违规后继续运行,它可能会破坏其状态并使问题的原因更难以找到。使用断言失败提供的信息(例如失败的位置和堆栈跟踪,如果环境支持核心转储或者程序在调试器中运行,甚至是完整的程序状态),程序员通常可以解决问题。因此断言提供了一个非常强大的调试工具。

生产环境中的断言
当程序部署到生产中时,通常会关闭断言,以避免它们可能产生的任何开销或副作用。在某些情况下,部署的代码中完全没有断言,例如通过宏的C / C ++断言。在其他情况下,例如Java,断言存在于已部署的代码中,并且可以在字段中打开以进行调试。[2]

断言还可以用于向编译器承诺给定边缘条件实际上不可达,从而允许某些本来不可能的优化。在这种情况下,禁用断言实际上可能会降低性能。

静态断言
在编译时检查的断言称为静态断言。

静态断言在编译时模板元编程中特别有用,但如果(并且仅当)断言失败,也可以通过引入非法代码在像C这样的低级语言中使用。C11和C ++ 11直接支持静态断言static_assert。在早期的C版本中,可以实现静态断言,例如,如下所示:

#define SASSERT(pred)switch(0){case 0:case pred:;}

SASSERT ( BOOLEAN CONDITION );
如果(BOOLEAN CONDITION)部件的计算结果为false,则上述代码将无法编译,因为编译器不允许两个具有相同常量的大小写标签。布尔表达式必须是编译时常量值,例如,(sizeof(int)==4)它将是该上下文中的有效表达式。此构造在文件范围内不起作用(即不在函数内),因此必须将其包装在函数内。

在C中实现断言的另一种流行的[3]方法是:

static char const static_assertion [ (BOOLEAN CONDITION )
? 1 : - 1
] = { ‘!’ };
如果(BOOLEAN CONDITION)部件的计算结果为false,则上述代码将无法编译,因为数组可能没有负长度。如果事实上编译器允许负长度,则初始化字节(’!'部分)应该导致即使是这样过于宽松的编译器也会抱怨。布尔表达式必须是编译时常量值,例如,(sizeof(int)==4)它将是该上下文中的有效表达式。

这两种方法都需要一种构造唯一名称的方法。现代编译器支持__COUNTER__预处理器定义,通过为每个编译单元返回单调递增的数字,便于构造唯一名称。[4]

D通过使用提供静态断言static assert。[5]

禁用断言
大多数语言允许在全局启用或禁用断言,有时独立启用或禁用断言。断言通常在开发期间启用,在最终测试期间和向客户发布时禁用。不检查断言避免了评估断言的成本,同时(假设断言没有副作用)仍然在正常条件下产生相同的结果。在异常情况下,禁用断言检查可能意味着已中止的程序将继续运行。这有时是优选的。

某些语言(包括C和C ++)使用预处理器在编译时完全删除断言。Java需要将一个选项传递给运行时引擎才能启用断言。如果没有该选项,则会绕过断言,但它们始终保留在代码中,除非在运行时由JIT编译器优化或在编译时由if(false)条件排除,因此它们不需要具有运行时空间或Java中的时间成本。

程序员可以通过绕过或操纵语言的正常断言检查机制来构建对其代码始终有效的检查。

与错误处理的比较
断言与常规错误处理不同。断言记录逻辑上不可能的情况并发现编程错误:如果不可能发生,那么程序的根本就是明显错误。这与错误处理不同:大多数错误条件都是可能的,尽管在实践中可能不太可能发生某些错误条件。使用断言作为通用错误处理机制是不明智的:断言不允许从错误中恢复; 断言失败通常会突然停止程序的执行; 并且通常在生产代码中禁用断言。断言也不会显示用户友好的错误消息。

请考虑以下使用断言来处理错误的示例:

int * ptr = malloc (sizeof (int ) * 10 );
断言(ptr );
//使用ptr

在这里,程序员知道如果没有分配内存,malloc它将返回一个NULL指针。这是可能的:操作系统不保证每次调用malloc都会成功。如果发生内存不足错误,程序将立即中止。没有断言,程序将继续运行直到ptr 被取消引用,可能更长,具体取决于所使用的特定硬件。只要断言没有被禁用,就可以立即退出。但是如果需要优雅的失败,程序必须处理失败。例如,服务器可能具有多个客户端,或者可能包含不会干净地释放的资源,或者可能具有未提交的更改以写入数据存储。在这种情况下,单个交易失败比突然中止更好。

另一个错误是依赖于用作断言参数的表达式的副作用。应该始终牢记断言可能根本不会被执行,因为它们的唯一目的是验证一个应该总是为真的条件实际上是否成立。因此,如果程序被认为是无错误并且被释放,则可以禁用断言并且将不再对其进行评估。

考虑前一个示例的另一个版本:

int * ptr ;
//如果malloc()返回NULL,
//
下面的语句失败,但在使用-NDEBUG进行编译时根本没有执行!assert (ptr = malloc (sizeof (int ) * 10 ));
//使用ptr:ptr在使用-NDEBUG进行编译时未初始化!

这可能看起来像分配的返回值一个聪明的方式malloc来ptr检查它是否是NULL一步到位,但malloc呼叫和分配ptr是评价形成表达的副作用assert的条件。当NDEBUG参数传递给编译器时,就像程序被认为没有错误并被释放一样,assert()语句被删除,因此malloc()不被调用,呈现ptr未初始化。这可能会导致程序执行过程中的分段错误或类似的空指针错误,导致可能是零星的错误和/或很难追查。程序员有时会使用类似的VERIFY(X)定义来缓解这个问题。

现代编译器在遇到上述代码时可能会发出警告。[6]