编程手搓工艺技能
六年编程老鸟:用“类型”把Bug扼杀在编译阶段
#编程语言平台比较
#符号推理与形式逻辑
#类型
#RUST教程
2026-08-02
5K
banq
写代码六年,被代数数据类型救过三百次,你还在靠注释给自己挖坑?
代数数据类型听起来像大学里那种教授讲完你就想退课的概念,但我要告诉你,这东西比你周末打游戏时的装备搭配还简单。学习它之后,我写代码的方式被彻底掀翻了,就像以前在泥巴路上开车,现在直接上了高速公路,还是带自动驾驶的那种。
这篇文章就是要把这套东西用你能听懂的方式讲透,让你以后看到代码里的错误提示不再是害怕,而是兴奋。
别被这破名字唬住
代数数据类型其实就是两种基本结构的组合:
- 一种是积类型,你可以把它理解成一个包含多个东西的包裹,比如一个用户有ID和角色,这两个东西必须同时存在才算完整。
- 另一种是和类型,它的核心逻辑是选择,一个值要么是这种情况,要么是那种情况,永远不可能同时是两者。
这两种类型加起来,就构成了代数数据类型这个听起来高深莫测的术语。
积类型在主流编程语言里很常见,几乎每个你用过的对象或者结构体都是它的亲戚。比如你定义一个用户,里面必须有名字、邮箱、年龄,这些字段全部都要有,缺一个就报错,这就是积类型在背后默默干活。和类型就更有趣了,它允许你在几个选项里只选一个,比如一个会话状态,要么是匿名访客,要么是已经登录的认证用户,没有第三种可能。
这两种类型之所以叫代数,是因为它们真的可以像数学里的加法和乘法一样运算。积类型相当于乘法,两个集合的组合数量是它们大小的乘积。和类型相当于加法,两个集合的合并数量是它们大小的和。这些数学背景你不用太纠结,只要记住积类型代表同时拥有,和类型代表二选一就够了。
积类型就像你的早餐套餐
点过早餐套餐吧,一个套餐里必须有主食、饮料和小吃,三样东西一个都不能少。积类型就是这个逻辑,一个完整的值必须包含所有规定的部分,少一块就不是那个东西了。在代码里定义一个用户结构体时,ID和角色必须同时存在,不存在只有ID没有角色或者只有角色没有ID的情况。
这种设计的好处是强制完整,你永远不需要担心某个字段突然消失了。当你看到某个变量是用户类型时,你就知道它身上肯定挂着ID和角色,不可能缺胳膊少腿。这种确定性让代码的推理变得极其简单,你不用到处加判断来检查字段是否存在,因为类型系统已经替你打包票了。
不过积类型也有局限,它只能表示必须同时拥有的关系。如果你的场景里有些字段是互斥的,比如根据用户类型需要不同的信息组合,积类型就有点吃力了。这时候你就得靠后面要说的和类型来救场,让不同类型的数据可以各自为政。
和类型才是真正的王牌
和类型是一个值的多种可能形态,每个形态都可以携带自己的专属数据。比如一个操作结果要么成功返回数据,要么失败返回错误原因,两个分支可以带着完全不同的信息结构。
在Rust语言里,这种类型叫枚举,在别的语言里可能叫联合或变体,但核心思想都一样:从多个候选中精确选中一个。
这种设计把选择关系直接写进了类型里,看类型的人一眼就知道这个值有几种可能。比起用一堆布尔标记和可空字段来模拟选择,和类型要清晰一万倍。因为你不用猜哪些组合是合法的,类型本身已经把非法组合挡在门外了。
更厉害的是,和类型可以嵌套使用。你可以让一个枚举的某个变体携带另一个枚举,这样就能表达非常复杂的业务规则,而所有规则都被压缩在类型定义里。编译器会帮你检查所有分支是否被处理,漏掉任何一个都不让你编译通过。
类型系统替你当保安
写代码最怕的是什么,是那些藏在角落里的非法状态。比如一个网站的主题设置,如果它可以切换亮色和暗色模式,那它必须要有默认模式、亮色方案和暗色方案。如果它是固定的,那就只需要一个模式和一个方案。用普通对象加上一堆可空字段来写,分分钟出现亮色方案为空但暗色方案有值这种脑残组合。
这种组合在类型层面是合法的,但业务上完全不成立。为了挡住这些非法组合,你只能在代码里加验证逻辑、写单元测试、靠团队内部口口相传哪些组合是可以用的。这种防御方式漏洞百出,任何一个环节出问题都会导致程序炸掉。
用和类型重新建模之后,情况就完全变了。你定义一个枚举,两个变体分别对应可切换和固定两种场景,每个变体只携带自己需要的字段。这样编译器天生就禁止了混乱组合,因为只有两个合法形态,别的根本构造不出来。你把类型定义往那一放,连文档都不用写了。
错误也要写进合同里
函数签名不光是告诉调用者成功时返回什么,还应该明确失败时会发生什么。在那些靠异常处理错误的语言里,函数签名通常只写了成功返回的类型,至于会不会抛异常,你得去看函数内部实现才知道。这就好比签合同时只写了对方给你的好处,但没写违约责任,万一对方跑路你完全没辙。
一个更好的做法是用结果类型把成功和失败都列在签名里。成功时返回数据,失败时返回一个具体的错误枚举,每种错误还能带上自己的上下文信息。比如解析端口号时,可能失败是因为输入不是数字,也可能是因为数字超出了范围,这两种错误需要的信息不一样,用枚举就可以分别处理。
调用者拿到这个结果类型之后,必须自己决定怎么处理两种可能。他要么处理错误,要么把错误继续往上抛,但绝对没有办法忽略错误拿到成功值。这种强制处理机制让你在写代码的时候就把所有异常路径走了一遍,而不是等到上线后才发现某个异常没人管。
没有值也是一种值
空值问题已经坑了程序员几十年,发明空引用的那位大佬后来自己都承认这是个价值十亿美金的错误。在普通语言里,一个变量可能指向一个对象,也可能指向空,但你从类型上看不出来它到底是哪种情况。于是代码里到处是空值检查,防不胜防。
用可选类型来显式表示值缺失之后,情况就清晰了。一个函数如果返回可选类型,那调用者一看就知道这个函数可能找不到东西,必须处理找不到的情况才能拿到里面的值。编译器强制你处理两种分支,你没法假装空值不存在。
更重要的是,可选类型和结果类型可以组合使用。比如查找用户这个操作,可能找不到用户,也可能查找过程本身出错。用嵌套类型可以表示三种结果:找到用户、没找到用户、查找出错。三种情况泾渭分明,不会混为一谈。
模式匹配逼你负责到底
有了精准的类型定义还不够,你还要确保每个分支都被正确对待。模式匹配就是一个强制处理所有可能分支的工具,编译器会检查你是否覆盖了所有变体,漏掉任何一个都不让你编译通过。这种检查让重构变得极其安全,因为你新增一个变体之后,编译器会给你列出所有需要修改的地方。
以前修改一个枚举,你得手动搜索代码里所有用到这个枚举的地方,然后一个个改。改漏了编译也不会报错,只是运行时可能走错分支导致逻辑错误。有了穷尽匹配之后,这些担忧全都没了,编译器变成了你的自动排查机器。
这种强制检查还有一个隐藏的好处,它逼你在写代码的时候就把所有情况都想清楚。你没办法偷懒跳过一个分支,所以你在编译阶段就把所有场景走了一遍。这比依赖测试覆盖率来发现遗漏要可靠得多,因为编译器是刚性的,它不会漏报。
改代码再也不用猜了
软件开发最频繁的操作就是改需求,今天加个字段,明天加个状态,改完代码能不能跑全看运气。用普通数据建模的时候,加一个新状态通常意味着要手动找出所有相关的判断逻辑,一个个改过来。这个过程极度依赖记忆力,改漏一个就会导致逻辑错误。
用和类型加穷尽匹配之后,加新状态的流程就变了。你先在枚举里新增一个变体,然后编译器报错说哪些匹配没覆盖新变体。你顺着编译器的错误列表一个一个改过去,改完所有错误代码就完全适配新状态了。整个过程不需要动脑子去回忆哪些地方用了这个类型,编译器全替你记住了。
这种编译器引导的修改方式把重构从脑力劳动变成了机械操作。真正难的是设计层面的决策,一旦把业务规则用类型表达清楚了,后续的修改维护就变得极其自动化。你把注意力集中在决策本身,而不是盯着代码到处找哪里需要改。
学完这套东西后我飘了
以前写代码,我总觉得类型系统就是个帮忙检查语法错误的工具,不认为它能对业务逻辑有多大贡献。但学会用和类型建模之后,我发现大部分常见的编程错误都能被编译器提前拦住。空指针、非法状态、未处理异常这些坑,在我切换编程范式之后几乎再也没有遇到过。
当然这套东西也有学习曲线,刚开始接触的时候你会觉得一个简单的逻辑为什么要写那么多匹配分支。但习惯了之后,你会发现自己写的代码几乎没有运行时错误了,因为所有边界情况在编译阶段就被处理完了。这种安全感是异常体系和空值检查给不了的。
现在我对编程语言的最低要求就是必须支持和类型和模式匹配。如果一门语言连这两个基础能力都没有,我压根不会考虑用它写正经项目。因为这意味着我得手动模拟类型安全,而这已经在几十年前被证明是一条通往痛苦的路。工具帮不了你的时候,就只能靠加倍努力来填坑,但努力填坑的结果往往比不填还糟。
说白了就是让类型替你打工
整个这套理念的核心就一句话:把业务规则直接编码到类型里,让编译器替你执行这些规则。你不用在代码里写注释说明哪些组合合法,也不用写验证函数来检查数据是否完整,因为你定义的类型本身已经把边界画死了。
模式匹配和穷尽检查是这个流程的配套工具,它们确保你处理了所有分支。类型定义了你有哪些可能性,匹配检查确保你针对每种可能性都写了处理逻辑。两者结合在一起,就形成了一个闭环的防御系统,从定义到使用全程被编译器监控。
这套做法的最大收获是改变了我的思维方式。以前遇到复杂业务逻辑,我会直接开始写代码,一边写一边思考边界情况。现在的习惯是先停下来,用类型把领域模型定义清楚,然后再动键盘。前面花的时间变长了,但后面调试和修 Bug 的时间几乎归零了。
回头看这些年写过的代码,最稳固、改动最轻松的模块全部是用这种思路构建的。而那些用松散数据类型堆砌出来的模块,改一次就塌一次,每次加新功能都要战战兢兢测试半天。两种体验的区别大到让我觉得以前的日子简直是裸奔。
现在你让我回到没有和类型的语言去写代码,我肯定是不干的。那种缺乏安全感的编程体验,就像骑自行车不戴头盔,一时半会儿可能没事,但真摔一次就够你受的。类型帮你分担了大脑的负担,让你可以把宝贵的注意力集中到真正需要创造力的地方去。
原文期刊:Draftist / 发表日期:2026-08-01 / 原文标题:The Bedrock of Software Design / 作者单位背景:Alex Fedoseev
写代码六年,现在我看着那些还在靠注释和文档防Bug的人,就像看着一个明明有雨伞却非要冒雨狂奔的傻子。