Why Type Systems Matter

Matthias Endler

类型系统为何重要

我大部分代码都是用 Python 或 PHP 这类动态类型语言(dynamically typed languages)编写的;但自从接触了 Rust 之后,我便爱上了静态类型系统(static type systems)。
它开始让我觉得非常自然;仿佛找到了一种全新的表达方式。

类型是来帮忙的

借助类型,你可以向机器和其他开发者传达你的保证与预期。类型表达了意图。

作为程序员,你可能已经对类型形成了一些直觉

sentence = "hello world"

你可能会猜测,sentence 是一个字符串。毕竟它被引号包围着。但如果类型是从其他地方推断出来的,事情就会变得棘手一些。

sentence = x

sentence 还是字符串吗?呃……我们不知道。这取决于 x 的类型。也许 x 是一个数字,所以 sentence 也是一个数字?也许 x 原本是字符串,但在重构过程中变成了字节数组?大家都玩得很开心。🎉

那这个呢?

filesize = "5000" # Size in bytes

这里,我们把文件大小表示成了字符串。

虽然这样或许能运行,但这个想法令人不安。
即使是简单的计算,也可能导致意料之外的结果:

file1 = "5000"
file2 = "3000"
total = file1 + file2
print(total) # prints '50003000'

我们该如何解决?

我们完全可以假定文件大小始终是一个数字。更准确地说,它必须是一个正的自然数。文件大小不可能为负数,而我们的最小内存块是一个字节(除了最< a href="https://en.wikipedia.org/wiki/4-bit">晦涩的系统之外,所有系统都是如此)。既然我们面对的是一台离散的机器,就知道文件大小只能是计算机能够处理的数值。要是我们能用一种精确的方式表达这一切就好了……

这就是类型系统登场的地方。
在 Rust 中,你可以定义一个 File 类型,并为它添加一个名为 size 的字段。

struct File {
  name: String,
  size: usize,
}

usize 保证始终足够大,能够容纳指向内存的任意指针(在 64 位计算机上,usize = u64)。现在,size 的类型不再有任何歧义。你甚至无法创建一个无效的文件对象:

// Error: `size` can't be a string.
let weird_file = File { name: 123, size: "hello" };

类型系统会阻止无效状态。它根本不允许你违反自己制定的规则。它会让你为自己的设计选择负责。我敢说:它会成为你大脑的延伸。过一段时间后,你会开始依赖类型检查器。“能编译,就能运行”是一句很有力量的箴言。

类型提高可读性并提供上下文

看看下面这段 Python 代码:

def filter_files(files):
  matches = []
  for file in files:
    if file.status == 0:
      matches.append(file)
  return matches

0 代表什么?我们无法判断。因为缺少上下文!

一旦我们定义出这样的 枚举(enum) 类型,事情就清楚多了:

from enum import Enum

class FileStatus(Enum):
  OPEN = 0
  CLOSED = 1

上面的例子就变成了:

def filter_files(files):
  matches = []
  for file in files:
    if file.status == FileStatus.OPEN:
      matches.append(file)
  return matches

在较大的代码库中,搜索 FileStatus.OPEN 要比搜索 0 容易得多。

注意:原生枚举类型在 Python 的发展历史中很晚才被引入。它很好地说明了,增强类型系统能够帮助提高可读性。

当你组合不同类型时,奇迹就会发生。

当你明智地选择类型时,所有片段会突然各就各位。编译器会凭空开始检查你的设计决策,以及所有类型是否能够良好地协同工作。它会指出你的思维模型中的缺陷。这会让你在重构时充满信心。

比如,让我们想想排序。想到排序时,我首先想到的是一个数字列表:

sorted([1,5,4,3,2]) # [1,2,3,4,5]

这是顺利的情况。那这个呢?

sorted(1)

糟糕。这行不通,因为 1 是一个单独的数字,而不是集合!如果我们在把它传给 sorted 之前忘记检查类型,就会在程序运行时得到一个错误。

sorted([1, "fish"])

在 Python 2 中,这会得到 [1, 'fish']因为字符串会按长度进行比较)。

编辑:Reddit 用户 jcdyer3 指出,原因是比较不可比较的类型时,它们会按类型排序,因此所有整数都会排在所有字符串之前。这是一个 CPython 实现细节)。

根据 Python 2,1 < fish
根据 Python 2,1 < fish
来源:插图由 Freepik 提供

从 Python 3 开始,这会抛出异常。

TypeError: '<' not supported between instances of 'str' and 'int'

好多了!少了一个错误来源。不过问题在于,这件事发生在运行时。这是因为 Python 使用动态类型。要是使用静态类型语言,我们本可以避免这种情况。

fn sorted<T>(collection: &mut [T]) where T: PartialOrd {
  // TODO: Sort the collection here.
}

看起来很吓人,但其实并不是。

我们定义了一个名为 sorted 的函数,它接受一个名为 collection 的输入参数。

collection 的类型由四个部分组成:

  1. & 表示我们“借用”这个集合,而不是拥有它。函数返回后,它仍然存在,不会被清理。
  2. mut 表示这个集合是可变的。我们可以修改它。
  3. [T] 表示我们期望输入一个列表/切片/向量。其他任何输入都会在编译时(甚至在程序运行之前)被拒绝。
  4. PartialOrd 是其中的神奇之处。它是一种 trait(特征),有点像接口。它表示集合中的所有元素 T 都必须是偏序的(partially ordered)

所有这些信息都有助于编译器防止我们搬起石头砸自己的脚。而且,无需查看其他地方,我们就能理解这个函数的输入和输出。

要点

  • 类型迫使开发者做好功课,思考代码的保证与局限。
  • 不要把类型看成约束,要把它们看成安全网,保护你免受自己有缺陷的思维模型影响。
  • 始终选择最精确表达你意图的类型。
  • 如果标准库中没有完美的类型,就用更简单的类型创建自己的类型。

遵循这些规则后,我发现自己仿佛被神奇地引导着,用最优雅的方式表达自己的想法。我的代码也变得更加符合惯用风格。

原文由 Matthias Endler 发布

本文章由 openai/gpt-5.6-luna 进行翻译