类型系统为何重要
我大部分代码都是用 Python 或 PHP 这类动态类型语言(dynamically typed languages)编写的;但自从接触了 Rust 之后,我便爱上了静态类型系统(static type systems)。
它开始让我觉得非常自然;仿佛找到了一种全新的表达方式。
类型是来帮忙的
借助类型,你可以向机器和其他开发者传达你的保证与预期。类型表达了意图。
作为程序员,你可能已经对类型形成了一些直觉。
sentence = "hello world"你可能会猜测,sentence 是一个字符串。毕竟它被引号包围着。但如果类型是从其他地方推断出来的,事情就会变得棘手一些。
sentence = xsentence 还是字符串吗?呃……我们不知道。这取决于 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 matches0 代表什么?我们无法判断。因为缺少上下文!
一旦我们定义出这样的 枚举(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 实现细节)。
来源:插图由 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 的类型由四个部分组成:
&表示我们“借用”这个集合,而不是拥有它。函数返回后,它仍然存在,不会被清理。mut表示这个集合是可变的。我们可以修改它。[T]表示我们期望输入一个列表/切片/向量。其他任何输入都会在编译时(甚至在程序运行之前)被拒绝。PartialOrd是其中的神奇之处。它是一种 trait(特征),有点像接口。它表示集合中的所有元素T都必须是偏序的(partially ordered)。
所有这些信息都有助于编译器防止我们搬起石头砸自己的脚。而且,无需查看其他地方,我们就能理解这个函数的输入和输出。
要点
- 类型迫使开发者做好功课,思考代码的保证与局限。
- 不要把类型看成约束,要把它们看成安全网,保护你免受自己有缺陷的思维模型影响。
- 始终选择最精确表达你意图的类型。
- 如果标准库中没有完美的类型,就用更简单的类型创建自己的类型。
遵循这些规则后,我发现自己仿佛被神奇地引导着,用最优雅的方式表达自己的想法。我的代码也变得更加符合惯用风格。
随机一篇博客