Skip to content

Lec 1 静态检查

本节主要讲两个主题:

  • 静态类型
  • 好的软件的三大性质

引入示例: 冰雹序列

我们将以冰雹序列作为贯穿示例,其定义如下:从数字 n 开始,

  • 若 n 为偶数,则序列下一项为 n/2;
  • 若为奇数,则下一项为 3n+1。当序列达到 1 时终止。

示例如下:

  • 2, 1
  • 3, 10, 5, 16, 8, 4, 2, 1
  • 4, 2, 1
  • 2n, 2n-1, … , 4, 2, 1
  • 5, 16, 8, 4, 2, 1
  • 7, 22, 11, 34, 17, 52, 26, 13, 40, …?(此序列何时终止?)

由于奇数运算规则,序列可能在最终降至 1 之前反复起伏。数学界猜想所有冰雹终将坠落——即对于任意起始值 n,序列最终都会收敛到 1——但这仍是未解之谜。其名称源自冰雹在云层中上下反弹,直至重量足够才坠落地面的自然现象。

以下是用代码实现从n出发,打印冰雹序列的代码。我们用python和TypeScript对比

python
# python实现
n = 3
while n != 1:
  print(n)
  if n % 2 == 0:
    n = n / 2
  else:
    n = 3 * n + 1
print(n)
typescript
// TypeScript or JavaSript,两个语言都可以正常工作
let n = 3;
while(n !== 1) {
  console.log(n);
  if (n % 2 === 0){
    n = n / 2;
  } else {
    n = 3 * n + 1;
  }
}
console.log(n);

以下语法上遇到注意的点:

  • JS在条件语句ifwhile需要括号,而Py不需要
  • JS结尾需要用分号。但这是可选的,因为每行JS都会被添加上分号。但是最好保持加分号的良好习惯
  • 比较运算符的陷阱。JavaScript 中 ==!= 会执行自动类型转换(例如 0 == "" 返回 true),这常导致意外结果。因此专业开发者只使用严格比较符 ===!==(如 0 === "" 返回 false)。从 Python 转来的学习者需特别注意这一差异。

类型

本课程实际使用的是 TypeScript——它在 JavaScript 基础上扩展了类型声明能力。例如可明确变量 n 为数值类型:

typescript
let n: number = 3;

Type 本质是值集合及其对应 operation 的组合。TypeScript 内置基础类型包括:

  • number: 整数与浮点数
  • boolean: 逻辑值 true/false
  • string: 字符序列

操作(函数)的语法表现形式多样。以下是在 Python 或 TypeScript 中表示操作的几种不同语法:

  • 作为运算符。如 a + b 对应函数 + : number × number → number
  • 作为函数。如 Math.sin(theta) 对应 sin: number → number(此处 Math 是包含函数的命名空间)
  • 作为对象的方法。如 str1.concat(str2) 对应 concat: string × string → string
  • 作为对象的属性。如 str.length 对应 length: string → number(注意无括号)

对比:TypeScript 的 str.length 与 Python 的 len(str) 功能相同但语法不同。

操作重载(operation overload)指同一操作符/函数名支持不同参数类型。例如 + 在 TypeScript 中:

  • 数值运算:5 + 3 → 8
  • 字符串拼接:"5" + "3" → "53" 重载不仅限于运算符,方法和函数也可重载(TypeScript 等语言支持)

静态类型

TypeScript 作为静态类型语言,能在编译阶段确定变量类型并推导表达式类型。例如当 ab 声明为 number 时,a+b 自动推导为 number。VS Code 等工具可在编码时实时显示类型错误。

相较之下,JavaScript 和 Python 属于动态类型语言,类型检查延迟到运行时执行。

静态类型是静态检查的核心手段,旨在编译期捕获错误。本课程多数工程实践都致力于消除代码缺陷,而静态检查正是第一道防线。它能预防大量因类型误用导致的错误——例如尝试执行 "5" * "6" 这类字符串乘法时,静态类型系统会在编码阶段立即报错,而非等到运行时才暴露问题。

ts
// TypeScript to compile
function hello(name: string): string {
  return 'Hi, ' + name;
}
let greeting: string = hello('type');
js
// JavaScript generated
function hello(name) {
  return "Hi, " + name;
}
let greeting = hello('types');

Pytho能够允许支持静态类型,比如

python
def hello(name: str) -> str:
  return 'Hi, ' + name

通过 Mypy/Pyright 等工具可进行静态类型检查。这种设计反映了软件工程的共识:静态类型对大型系统的构建和维护至关重要。

渐进式类型(Gradual Typing)允许代码中部分模块使用静态类型,其余保持动态类型,为原型快速迭代到系统稳定维护提供了平滑过渡路径。

TypeScript的数字类型陷阱

TypeScript 的 number 类型存在与数学实数不符的特殊行为:

整数精度限制

  • Number.MAX_SAFE_INTEGER(大约是 253 或 1036)是最大可精确表示的整数
  • Number.MIN_SAFE_INTEGER(大约是 -253 或 -1036)是最小可精确表示的整数

特殊值

  • Number.NaN(代表“Not a Number”)
  • Number.POSITIVE_INFINITY(显示为 "Infinity")
  • Number.NEGATIVE_INFINITY(显示为 "-Infinity")

溢出和下溢

TypeScript 也无法表示过大(远离零)或过小(接近零)的数字:

  • Number.MAX_VALUE(大约是 10308)是可以安全表示的最大数字
  • Number.MIN_VALUE(大约是 10-324)是可以安全表示的最小正数

注意:访问这些常量需通过 Number 类,但声明类型时应始终使用小写 number

数组

我们将冰雹序列存储在序列数据结构中,实现如下

ts
let array: Array<number> = [];
let n: number = 3;
while(n !== 1) {
  array.push(n)
  if (n % 2 === 0) {
    n = n / 2;
  } else {
    n = 3 * n + 1;
  }
}
array.push(n);

函数

我们将代码封装为可复用的函数:

ts
/**
 * 计算冰雹序列
 * @param n  序列起始数字(要求 n > 0)
 * @returns 以n开头、1结尾的冰雹序列数组
 */
function hailstoneSequence(n: number): Array<number> {
    let array: Array<number> = [];
    while (n !== 1) {
        array.push(n);
        n = (n % 2 === 0) ? n / 2 : 3 * n + 1;
    }
    array.push(n);
    return array;
}

对于函数规范,我们将在后续课程深入探讨规范编写规范,但你现在就需要开始阅读并应用它们。

可变值 vs 重赋值

优秀的程序员会限制可变性,因为意外变更易引发错误。本课程将不可变性作为核心设计原则,主要体现在两个维度:

  • 值的可变性。
    • 不可变类型:创建后值不可修改。
      • 例如TS/Py的string
    • 可变类型:允许内容修改
      • 例如TS的Array和Python的list 支持增删改操作
  • 引用的可重赋值性(Reassignment)
    • TypeScript 通过 const 声明不可重新赋值的常量
    • Python默认所有变量都可重新赋值

尽可能多地使用 const 是一个好习惯。就像变量的类型声明一样,这些声明是重要的文档,对代码的阅读者有用,并且由编译器进行静态检查。

好的编程习惯

记录代码假设的重要性

显式声明即文档

  • 类型注解(如 n: number)记录了对变量的假设,TypeScript 会在编译时验证该假设
  • 常量声明const)表明变量不可重新赋值,编译器会静态检查这一约束
  • 手工文档(如 n > 0)补充了类型系统无法自动检查的假设

为什么必须记录假设?因为编程充满隐性约定,若不显式记录:

  • 开发者自己会遗忘
  • 后续维护者只能靠猜测

编程的双重目标

  1. 与计算机对话
    • 通过编译器验证:语法正确性 → 类型正确性 → 运行时逻辑正确性
  2. 与人类对话
    • 确保代码可理解性,便于未来维护(包括你自己六个月后阅读时)

黑客式编程 vs 工程化开发

黑客行为(不可取)

  • ❌ 编写大量代码后一次性测试
  • ❌ 假设所有细节都记在脑子里(包括使用者)
  • ❌ 乐观认为"要么没bug,要么很容易修复"

工程实践(推荐)

  • 渐进开发:写少量代码立即测试(后续课程将介绍测试驱动开发)
  • 假设文档化:明确记录代码依赖的前提条件
  • 防御性编程:通过静态检查等机制防范错误(包括防范自己犯蠢)

为啥选用TypeScript

安全性是首要原因。TypeScript 具备静态检查能力(主要是类型检查,也包括其他静态验证,例如确保代码返回了声明要返回的值)。本课程聚焦软件工程,而"减少错误"正是软件工程的核心原则

普适性是另一考量。TypeScript 可编译为纯 JavaScript,后者在科研、教育和工业界应用广泛。JavaScript 不仅能在最初主流的浏览器中运行,如今还支持服务器端甚至 Windows/Mac/Linux 桌面应用开发。

相较 Java,TypeScript 类型系统更丰富,编写程序时所需模板代码更少,更是开发现代用户界面和网页应用的上佳之选。事实上,TypeScript 的类型系统非常先进,开发者甚至能用它编写出意想不到的程序(其中一些堪称精妙)。

优秀的程序员必须掌握多门语言。编程语言如同工具,需因场景制宜。

得益于 JavaScript 的普及,其生态拥有大量实用库和免费开发工具(如 VS Code 等 IDE、编辑器、编译器、测试框架、性能分析器、代码覆盖率工具和风格检查器)。JavaScript 与 Python 在生态丰富度上不相伯仲,而 TypeScript 能充分利用 JavaScript 的库资源。

当然,选择 JavaScript 也有遗憾。其语言规模庞大,历经数十年发展积累了过多特性;继承了 C/C++ 等旧式语言的包袱——例如从 C 沿袭而来的 switch 语句就是个过时且不安全的语法结构;JavaScript 早期设计也存在缺陷,比如使 0 == "" 返回 true 的类型转换规则,或是 [] + [] 莫名生成空字符串的行为,动态检查机制也弱于其他语言。TypeScript 修复了部分问题,但并未完全解决。如今 JavaScript 许多特性因使用风险已被开发者弃用。

总结

我们今天介绍的核心概念是静态检查。以下是这一概念与课程目标之间的关系:

  • 避免出BUG(Safe from bugs):静态检查通过在运行前捕捉类型错误和其他 bug,有助于提高程序的安全性。
  • 易于理解(Easy to understand):因为类型在代码中是显式声明的,静态检查有助于提升代码的可理解性。
  • 便于修改(Ready for change):当你修改代码时,静态检查可以指出需要一同更改的其他部分,从而使代码更易于维护和演进。