mit6.031 2023spring 软件构造 笔记 Specification
为什么需要规范
当程序失败时,很难确定错误在哪里。 代码中的精确规范可以让您将责任分担给代码片段,而不是人。
有助于使模块更易于理解,就像黑匣子外部的标签一样。
拥有规范可以让你了解模块的作用,而无需阅读代码。
规范的结构 Specification structure
- 函数签名,给出名称、参数类型和返回类型
- require 子句,描述对参数的其他限制
- effects 子句,描述函数的返回值、异常和其他效果
TypeScript中的规范
一些语言(尤其是 Eiffel)将前置条件和后置条件作为语言的基本组成部分,作为运行时系统(甚至编译器)可以自动检查的表达式,以强制执行客户端和实现者之间的契约。
TypeScript 并没有走得这么远,但它的静态类型声明实际上是函数的前置条件和后置条件的一部分,这是由编译器自动检查和强制执行的部分。 契约的其余部分——我们不能写成类型的部分——必须在函数之前的注释中进行描述,并且通常依赖于人类来检查和保证它。
// 函数签名:明确规定了参数 a 和 b 必须是数字,返回值也必须是数字。
// TypeScript 编译器会确保:
// 1. 调用者传入的必须是两个 number 类型的数据。
// 2. 函数实现者必须返回一个 number 类型的数据。
// 如果违反这些,代码在编译阶段就会报错,无法正常运行。
function average(a: number, b: number): number {return (a + b) / 2;
}// 合法的调用(编译器通过)
let result: number = average(5, 10); // result 为 7.5// 非法的调用(编译器报错,无法运行)
// average("5", "10"); // 错误:类型“string”的参数不能赋给类型“number”的参数。
一个常见的约定,最初是为 Java 设计的,但现在也被 ts 和 js 使用,是在函数之前放置一个文档注释,其中:
- 参数由@param子句描述
- 结果由 @returns 子句描述
What a specification may talk about
函数的规范可以谈论函数的参数和返回值,但永远不应该谈论函数的局部变量或函数类的私有字段。 您应该考虑该实现对规范的读者不可见。
就客户端而言,它位于防火墙后面。
避免空值
空值既麻烦又不安全,以至于好的编程会试图避免它们。
当 ts 启用了严格的 null 检查时,静态类型检查器保证这一点:
let word: string = null; // compile error when strict null checking is on
let words: Array<string> = [ null ]; // compile error when strict null checking is on
谷歌在其核心Java库Guava中针对null值的使用进行了讨论,该项目说明:
- null是”万恶之源“
- “粗心使用
null
会导致各种惊人的bug”: 这是因为null
本质上是一个“空值”或“无值”的表示。如果你试图在一个期望有实际对象的地方使用null
(例如,调用null.toString()
),程序会立即抛出NullPointerException
(NPE),导致崩溃。 - “95%的集合本不应包含任何null值”: 这是一个非常重要的观察。在绝大多数业务逻辑中,我们往集合(如
List
,Map
,Set
)里放的都是有意义的实际对象。允许null
被放入集合,相当于在数据结构中埋下了一颗“地雷”,未来任何遍历或使用这个集合的代码都可能意外踩到它而崩溃。 - “快速失败(Fail Fast*)比静默接受更有帮助”:
- “静默接受”: 指代码不做检查,允许
null
被存入集合或传递给方法。这很危险,因为错误不会在源头被发现,而是会传播到后续代码中,在某个不可预知的地方爆发,使得调试变得极其困难。 - “快速失败”: 指立即报错。如果一个方法或集合明确规定不允许
null
,那么一旦传入null
,它就应该立刻抛出异常(如NullPointerException
或IllegalArgumentException
)。 - 为什么“快速失败”更好? 因为它让错误在发生的地点就立刻暴露出来,而不是隐藏起来直到后续某个不确定的时刻。这极大地简化了调试过程,开发者能马上定位到是哪个调用者传入了非法的
null
值。
- “静默接受”: 指代码不做检查,允许
-
“
null
令人不快的歧义性”: 这是null
的另一个致命缺点。当你看到一个方法返回null
时,你无法确切知道这个null
到底想表达什么语义。经典例子:
Map.get(key)
:- 这个调用返回
null
可能有两种完全不同的含义:- 键(key)不存在于映射(Map)中。 (这是一种“失败”或“未找到”的信号)
- 键(key)存在,但它对应的值(value)本身就是
null
。 (这是一种“成功找到,但找到的值是null
”的信号)
- 作为API的调用者,你根本无法区分这两种情况!你必须额外调用
map.containsKey(key)
来确认,这很麻烦,而且容易忘记。
“Null可以意味着失败、成功,几乎任何事”: 在不同的API设计里,
null
被用来代表各种意思:“未初始化”、“错误”、“空结果”、“未找到”等等。没有统一的规范,导致代码可读性急剧下降。“使用
null
以外的其他东西可以使你的意图清晰”: 这是Guava(以及现代Java、其他语言)推荐的解决方案。使用明确、无歧义的表达方式来替代null
。- Guava/Java 8+ 的解决方案:
Optional<T>
- 如果一个方法可能没有返回值,它应该声明为返回
Optional<String>
,而不是String
。 Optional.of("Hello")
表示肯定有一个值(“Hello”)。Optional.absent()
(Guava) 或Optional.empty()
(Java 8) 表示肯定没有值。- 这样,调用者看到返回类型就知道可能需要处理空值的情况,并且
null
不再被用作“无值”的信号,歧义性就消失了。
- 如果一个方法可能没有返回值,它应该声明为返回
- 这个调用返回