为什么类型系统很重要
原文由 Matthias Endler 于 发布,订阅该博客
我大部分代码都是用 Python、PHP 这类动态类型语言编写的;但自从接触了 Rust 以来,我就迷上了静态类型系统。
它让我觉得非常自然,就像一种全新的表达方式。
类型是来帮你的
有了类型,你就能传达你的保证和预期——既是向机器,也是向其他开发者。类型表达的是意图。
作为程序员,你可能已经对类型有了一些直觉。
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'我们该如何解决这个问题?
我们可以有把握地认为,文件大小永远是一个数字。更精确地说,它必须是正的自然数。不存在负的文件大小,而我们最小的内存块是一个字节(除了那些最冷门的系统)。而且,由于我们面对的是一台离散的机器,我们知道它只能是计算机能够处理的文件大小。要是能把这些都精确地表达出来就好了……?
这时类型系统就该登场了。
在 Rust 中,你可以定义一个带有 size 字段的 File 类型。
struct File {
name: String,
size: usize,
}usize 保证它永远大到足以容纳任何指向内存的指针(在 64 位计算机上 usize = u64)。现在 size 的类型不再有任何歧义。你甚至无法创建一个非法的 file 对象:
// 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 容易得多。
注意:原生的 enum 类型在 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都必须是偏序的。
所有这些信息都能帮助编译器防止我们搬起石头砸自己的脚。而且我们无需查看别处就能理解函数的输入和输出。
总结
- 类型迫使开发者做好功课,认真思考代码的保证和局限。
- 不要把类型看作约束,而要把它看作一张安全网,它能保护你免受自身有缺陷的思维模型的影响。
- 始终选择最能精确表达你意图的类型。
- 如果标准库中没有完美的类型,就用更简单的类型自己创建一个。
遵循这些规则后,我发现自己神奇地被引向了对想法最优雅的表达。我的代码也变得更加地道了。
随机一篇博客
评论
登录后参与讨论