断言类型
本课程解释了为何需要使用断言语句,随后探讨了D语言中不同类型的断言语句。
assert 的必要性
在前一章中,我们学习了如何使用异常和作用域语句来保证程序的正确性。assert 是实现这一目标的另一个强大工具。assert 确保程序所依赖的某些假设是成立的。
有时很难判断应该抛出异常还是使用 assert。在下面的所有示例中,我将不加过多解释地使用 assert。我们将在本章稍后详细讨论两者的区别。
尽管并不总是显而易见,但程序中充满了各种假设。例如,以下函数是在假设两个年龄参数都大于等于零的前提下编写的:
1import std.stdio; 2 3double averageAge(double first, double second) { 4 return (first + second) / 2; 5} 6 7void main() { 8 auto result = averageAge(-1, 10); 9 10 writeln(result); 11}
虽然程序中出现负年龄值可能是不合理的,但该函数仍然会计算出一个平均值,这个值可能在程序中被无意使用,导致程序继续处理错误的数据。
再举一个例子,以下函数假设它始终只会以两个命令之一调用:“sing” 或 “dance”:
1import std.stdio; 2 3void applyCommand(string command) { 4 if (command == "sing") { 5 robotSing(); 6 7 } else { 8 robotDance(); 9 } 10} 11 12void robotSing() { 13 14 writeln("robot singing"); 15} 16 17void robotDance() { 18 19 writeln("robot dancing"); 20} 21 22 23void main() { 24 string command = "teach"; 25 26 applyCommand(command); 27}
由于这个假设,除了“sing”之外的任何命令都会触发 robotDance() 函数的调用,无论命令是否有效。
当这些假设仅存在于程序员的脑海中时,程序很可能最终运行出错。assert 语句可以检查这些假设,并在假设不成立时立即终止程序。
assert
assert 可以以两种方式使用:
1assert(逻辑表达式); 2assert(逻辑表达式, 消息);
逻辑表达式代表了对程序的一个假设。assert 会评估该表达式以验证该假设是否成立。如果逻辑表达式的值为真,则认为假设有效;否则,假设无效,将抛出 AssertError 异常。
顾名思义,此异常继承自 Error。正如我们在异常章节中所知,继承自 Error 的异常绝不能被捕获。程序必须立即终止,而不是在假设失效的情况下继续运行。
上述 averageAge() 函数中的两个隐含假设可以通过两个 assert 调用来明确表达,如下所示:
1import std.stdio; 2 3double averageAge(double first, double second) { 4 assert(first >= 0); 5 assert(second >= 0); 6 return (first + second) / 2; 7} 8 9void main() { 10 auto result = averageAge(-1, 10); 11 12 writeln(result); 13}
这些 assert 检查传达了“假设两个年龄都大于等于零”的含义。也可以理解为“只有当两个年龄都大于等于零时,该函数才能正确工作”。
每个 assert 都会检查其假设,若不成立则以 AssertError 终止程序:
1core.exception.AssertError@deneme(2): Assertion failure
错误消息中 @ 符号之后的部分指明了失败的 assert 检查所在的源文件和行号。根据上述输出,失败的 assert 位于文件 deneme.d 的第 2 行。
assert 的另一种语法允许在检查失败时打印自定义消息:
1import std.stdio; 2 3double averageAge(double first, double second) { 4 assert(first >= 0, "年龄不能为负数。"); 5 assert(second >= 0, "年龄不能为负数。"); 6 return (first + second) / 2; 7} 8 9void main() { 10 auto result = averageAge(-1, 10); 11 12 writeln(result); 13}
有时,人们认为程序永远不可能进入某个代码路径。在这种情况下,通常会使用字面量 false 作为逻辑表达式,使 assert 检查失败。例如,为了表明 applyCommand() 函数不应被除“sing”和“dance”以外的命令调用,并防止这种情况发生,可以在不可能的分支中插入 assert(false):
1import std.stdio; 2 3void applyCommand(string command) { 4 if (command == "sing") { 5 robotSing(); 6 7 } else if (command == "dance") { 8 robotDance(); 9 10 } else { 11 assert(false); 12 } 13} 14 15void robotSing() { 16 17 writeln("robot singing"); 18} 19 20void robotDance() { 21 22 writeln("robot dancing"); 23} 24 25 26void main() { 27 string command = "teach"; 28 29 applyCommand(command); 30}
该函数确保只对它已知的两个命令正常工作。
注意: 在这里,另一种选择是使用
final switch语句。
static assert
由于 assert 检查用于程序的正确执行,因此它们在程序实际运行时才生效。但也存在一些关于程序结构的检查,可以在编译时就完成。
static assert 是 assert 的对应形式,它在编译时执行。static assert 的优势在于,它甚至不允许编译那些原本会在运行时出错的程序。自然的要求是,逻辑表达式必须能够在编译时求值。
例如,假设菜单标题将显示在一个宽度有限的输出设备上,以下 static assert 确保标题永远不会超过该限制:
1import std.stdio; 2 3void main() { 4 enum dstring menuTitle = "Command Menu"; 5 6 static assert(menuTitle.length <= 16); 7}
注意,string 被定义为 enum,以便其长度可以在编译时计算。
假设程序员将标题改为更详细的描述:
1import std.stdio; 2 3void main() { 4 enum dstring menuTitle = "Directional Commands Menu"; 5 6 static assert(menuTitle.length <= 16); 7}
此时,static assert 检查将阻止程序编译:
1Error: static assert (25u <= 16u) is false
这会提醒程序员输出设备的限制。
当用于模板时,static assert 的作用更加显著。
在下一课中,我们将深入探讨 assert 的各种特性。