ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

模式与模式匹配:现代语言如何用数据形状替代if-else

模式与模式匹配:现代语言如何用数据形状替代if-else 教材里那一章往往被放在书的后半部分甚至有人直接跳过——“模式与模式匹配”听起来像是给编译器作者准备的内容。但这两年你会发现C# 9 的 switch 表达式、Java 21 的 switch 模式匹配、Python 3.10 的 match-case全都在往这个方向走。它已经不是某个函数式语言的小众玩具而是现代编程语言的一项基础能力直接影响你怎么拆解逻辑、怎么写分支、怎么处理“不同类型做不同事情”这件事。这篇文章就以“第19章 模式与模式匹配”为纲把这一章真正在讲什么、主流语言里的语法长什么样、日常项目和刷题场景里怎么落地以及它和设计模式之间那笔容易算错的账全部摊开讲一遍。适合正在学语言基础但想往上走一步的同学也适合写了好几年 if-else 想换换手感的开发者。1. 这一章到底在讲什么模式匹配的本质1.1 从一段老式代码看痛点先看一段很常见的代码按形状类型计算面积。public double CalcArea(Shape s) { if (s null) return 0; if (s is Circle c) { return Math.PI * c.Radius * c.Radius; } else if (s is Rectangle r) { return r.Width * r.Height; } else if (s is Triangle t) { return 0.5 * t.Base * t.Height; } else { throw new NotSupportedException($无法处理类型: {s.GetType().Name}); } }这段代码有问题吗功能上没问题。可读性也还行。但把它放大到一个真实系统里你就会闻到不舒服的味道每个处理逻辑都是“先判类型再强转再取属性”。分支一多整个方法就变成一大串阶梯式代码后面的人加一个新的 Shape 子类进来得先找到这段逻辑再小心翼翼插进某个 else if 中间稍不注意就漏掉一个分支或者把分支顺序搞错。更麻烦的是这种代码在表达的核心语义——不同类型映射到不同行为——被淹没在语法噪音里了。读这段代码你得花几秒才能反应过来“哦它就是在做类型分发”。模式匹配要解决的正是这个问题。1.2 模式匹配的本质按形状取数据模式匹配这个词字面上看是“拿一个模式去匹配数据”但更准确的理解应该是直接按数据的结构形状来提取和处理信息。你不需要先判断“它是不是 Circle”再去拿它的 Radius。你只需要写出“如果是 Circle把它当作一个带 Radius 的东西”语法层面直接完成类型判断、转换、属性提取这三件事。举个生活化的例子快递分拣。传统 if-else 的方式是——先看包裹外标签如果写着“朝阳区”就放进 A 筐然后打开包裹看看里面是不是书是书就登记书名。模式匹配的方式是——一个包裹送过来直接就按它的“形状”分拣到对应位置中间不需要额外的拆开-确认-重新打包过程。这里的“模式”不是算法里的“设计模式”更不是硬件里的“GPIO 模式”“虚拟机模式”而是语言层面的一个概念对数据结构形状的一种描述。你在代码里写一个模式运行时拿去和数据比对匹配上了就取得相应的数据。C# 的 is 表达式、switch 表达式Java 的 instanceof 模式Python 的 match-caseRust 的 match都是在做这件事。教材里把这个主题单独列为一章本质原因是它代表了一种思维转变从“命令式地一步步判断”转向“声明式地描述结构”。你告诉编译器“我想要哪种形状的数据”编译器负责帮你把判断、转换、取值这些繁琐细节全部处理掉。这也是为什么近几年的主流语言都不约而同地在加强这块能力。1.3 各主流语言的支持现状先看一张总表搞清楚这门技术在不同语言生态里的位置。语言关键版本基本形态主要适用场景C#C# 9 起系统化支持C# 11 加入列表模式增强 is、switch 表达式、属性模式、位置模式类型分发、集合匹配、判空、业务规则分支JavaJava 16 增强 instanceofJava 21 正式支持 switch 模式instanceof 模式、switch 模式、记录解构类型分层处理、消除强转PythonPython 3.10match-case 语句结构模式匹配字典、元组、类实例的结构化匹配Rust语言原生支持match 表达式必选穷尽处理枚举类型处理、状态机TypeScript配合可辨识联合类型discriminated union通过类型收窄实现类似效果API 返回类型处理这张表里有一个明显趋势无论静态类型还是动态类型语言都在用模式匹配去替代旧的“类型判断强转”套路说明这不是某家厂商拍脑袋的决定而是编程语言发展到现阶段的一个共同解法。2. 核心语法拆解以 C# 为主Java/Python 对照2.1 C# 模式匹配的几类基础模式C# 的模式匹配是现在主流语言里最完整的一套尤其 C# 11 之后几乎所有你想象得到的模式类别都有了。逐个过一遍。常量模式if (status is 200) { Console.WriteLine(成功); }常量模式就是把字面量、枚举值、const 常量拿来比较。它看起来和写status 200没什么区别但在 switch 表达式里能力会被放大可以和其他模式任意组合。类型模式与声明模式if (value is int i) { Console.WriteLine($整数: {i}); }这是最常用到的一类。用is int i表示“如果 value 是 int就把它当作 i 使用”。老写法是value is int加一次强转(int)value现在一步到位。Java 16 的instanceof也支持了这种写法if (obj instanceof Integer i) { System.out.println(整数: i); }再往下一层就是属性模式。这招在 C# 里特别实用if (rect is Rectangle { Width: 100, Height: 50 }) { Console.WriteLine(这是一个大号矩形); }Rectangle { Width: 100 }的含义是匹配 Rectangle 类型同时它的 Width 属性要大于 100。类型判断、属性提取、范围判断一次完成而且一眼就能看出业务意图。放在过去这就是一长串嵌套 if。位置模式位置模式配合元组或对象的 Deconstruct 方法使用var point (3, 4); if (point is (0, 0)) { Console.WriteLine(原点); } if (point is ( 0, 0)) { Console.WriteLine(第一象限); }(0, 0)这个模式直接匹配元组的两个元素。配合关系模式 0你还能顺便对每个位置上的值做条件判断。这个特性让很多坐标、经纬度、颜色值之类的成组数据变得非常好处理。关系模式string Level(int score) score switch { 90 优秀, 80 良好, 60 及格, 60 不及格 };注意这里的 90、 60不是比较表达式而是模式。C# 支持、、、四种关系模式它们可以自由组合。这种表达方式把“范围分段”描述得非常干净。逻辑模式// 是否在有效区间内 if (x is 0 and 100) { } // 是否非法 if (code is 400 or 404 or 500) { } // 是否非空 if (name is not null) { }and、or、not三个关键字把模式任意组合。尤其是is not null写起来比! null更像是在表达“非空”这个意图读代码的时候可以省掉用脑转换。var 模式if (student is var s s.Age 18) { Console.WriteLine($已成年{s.Name}); }var 模式不关心类型只做“声明一个变量绑定到被匹配对象”这件事。它主要配合或者属性模式起到一个临时变量的作用。弃元模式switch (value) { case int _: Console.WriteLine(是整数); break; case string _: Console.WriteLine(是字符串); break; }用_忽略匹配到的值本身只关心类型。在函数式语言里这叫通配符在 C# 里叫作弃元discard。列表模式C# 11int[] numbers { 1, 2, 3, 4 }; if (numbers is [1, 2, ..]) { Console.WriteLine(以 1,2 开头); } if (numbers is [.., 4]) { Console.WriteLine(以 4 结尾); }..表示“中间任意个元素”。这个模式特别适合解析协议、判断文件头、检查数组边界后面实操部分我会用判断无 BOM 文本编码来演示这是列表模式最典型的落地场景。2.2 求值顺序与守卫条件模式匹配的求值顺序非常重要。C# 的 switch 表达式按照从上往下的顺序依次尝试每个分支第一个匹配成功的生效。这带来两个实际影响。第一更具体的模式要放在更前面。比如有object和string两个模式string必须放前面否则第一个object把什么都接住了后面的分支永远执行不到。编译器对部分情况会给出警告但不会算错误。第二when 守卫条件可以在模式的基础上再叠加逻辑string GetTip(int temperature) temperature switch { 0 多穿点, 10 when IsRainy() 带伞有点冷, 10 有点凉, _ 正常 };when相当于给同一个模式继续套条件。执行顺序依然是自上而下所以数据一旦命中某个模式且守卫条件为真就不会再往下走。另外一个容易被忽略的能力是穷尽性检查。在 C# 的 switch 表达式中如果编译器判断你的分支没有覆盖所有可能的情况并且你没有写_兜底分支它会报“并非所有代码路径都返回值”之类的错误。这其实是个隐形的护身符——它逼着你在写的时候就把所有情况想清楚而不是等运行时报错。这也是 switch 表达式比传统 if-else 链安全的一个核心原因。2.3 Java 和 Python 的写法规整换个生态看看。Java 21 正式支持了 switch 模式匹配。前面提到的形状计算用 Java 写是这样public double calcArea(Shape s) { return switch (s) { case Circle c - Math.PI * c.radius() * c.radius(); case Rectangle r - r.width() * r.height(); case null - 0; default - throw new IllegalStateException(Unexpected: s); }; }case Circle c -的箭头意味着不需要 break也没办法穿透。null直接作为一个模式分支出现这是以前 Java 里写起来特别别扭的地方。整体上 Java 的实现吸收了很多 C# 和 Kotlin 的设计用起来非常接近。Python 的 match-case 风格差异大一点。Python 本身是动态类型所以这里的模式匹配更侧重结构匹配而不是类型分发def parse_command(cmd): match cmd: case {action: move, x: x, y: y}: return f移动到 ({x}, {y}) case [attack, target]: return f攻击 {target} case str() as s: return f文本指令: {s} case _: return 无法识别注意case {action: move, x: x}匹配的是字典结构键存在且变量绑定到对应值。case [attack, target]匹配长度为 2 的列表。这个表达能力是传统 Python 代码里很难实现的以前你得先判断“是不是 dict”“有没有 action 键”“action 是不是 move”现在一步到位。三套语法放到一起看差别只在外观内核完全一致声明一个结构模式运行时去匹配数据匹配成功后把数据提取到变量里然后执行对应逻辑。3. 实操场景从刷题到项目的三个落地案例3.1 刷题里的“ACM 模式”到底是什么这个话题几乎每个刷题的人都遇到过。“ACM 模式”这个词在国内 OJ 和面试刷题语境里经常出现它跟模式匹配没有直接关系但确实很多人会疑惑这里的“模式”指什么。简单说刷题有两种代码组织方式。一种是核心代码模式最常见的 LeetCode 风格平台已经把输入读取、测试、参数组装做好了你只需要实现一个函数public class Solution { public int[] TwoSum(int[] nums, int target) { var map new Dictionaryint, int(); for (int i 0; i nums.Length; i) { if (map.TryGetValue(target - nums[i], out int j)) { return new[] { j, i }; } map[nums[i]] i; } return Array.Emptyint(); } }另一种是ACM 模式全称源自 ACM 程序设计竞赛的输入输出要求你需要自己处理全部标准输入自己解析数据自己输出结果。同样的题ACM 模式下你的 Main 方法长这样using System; class Program { static void Main() { // 第1行数组长度 n // 第2行n 个数组元素 // 第3行目标值 string[] parts Console.ReadLine().Split(); int n int.Parse(parts[0]); int[] nums new int[n]; string[] values Console.ReadLine().Split(); for (int i 0; i n; i) { nums[i] int.Parse(values[i]); } int target int.Parse(Console.ReadLine()); // 到这里你才拿到数据接下来才是核心逻辑 } }两者的核心差异在于核心代码模式把注意力集中在算法本身ACM 模式额外考察你对输入输出格式的处理能力。如果你在网上看到“acm模式”和“核心代码模式”的切换教程说的就是在两个平台的提交格式之间做转换。从这个角度理解“模式”它指的是“代码运行时的组织形式”跟第 19 章的“模式匹配”虽然共享同一个词但完全是两个维度。刷题时经常有人把两者混在一起面试被问到“什么是模式匹配”又讲到 ACM 模式就偏了。记住ACM 模式是 IO 格式的约定模式匹配是语法机制。3.2 用列表模式判断无 BOM 的文本文件编码做文件解析的同学一定遇到过编码检测的问题。UTF-8 带 BOM 时文件开头三个字节是EF BB BF程序一眼就能认出来。可一旦文件没带 BOM很多 Linux 环境下生成的文件都是无 BOM 的 UTF-8编码判断就麻烦了。C# 11 的列表模式配合 ReadOnlySpan可以用一种非常“声明式”的方式写编码探测逻辑static Encoding DetectEncoding(ReadOnlySpanbyte bytes) { var encoding bytes switch { [0xEF, 0xBB, 0xBF, ..] Encoding.UTF8, [0xFF, 0xFE, ..] Encoding.Unicode, // UTF-16 LE [0xFE, 0xFF, ..] Encoding.BigEndianUnicode, // UTF-16 BE [0x00, 0x00, 0xFE, 0xFF, ..] new UTF32Encoding(bigEndian: false, byteOrderMark: true), [0xFF, 0xFE, 0x00, 0x00, ..] new UTF32Encoding(bigEndian: true, byteOrderMark: true), _ Encoding.Default }; return encoding; }[0xEF, 0xBB, 0xBF, ..]这个模式读起来几乎是自然语言前三个字节是 EF BB BF后面任意。和传统写bytes.Length 3 bytes[0] 0xEF bytes[1] 0xBB bytes[2] 0xBF相比列表模式把判断逻辑压缩成对数据形状的直接描述既短又不容易写错。这里有一个实际工作中常见的坑很多人判断无 BOM 时直接返回 Encoding.Default在 Windows 上通常指 GBK但文件其实是 UTF-8 无 BOM。GBK 和 UTF-8 在 ASCII 区段完全一致一旦文件里有中文GBK 按两个字节解析 UTF-8 的三字节序列轻则乱码重则直接报解码异常。所以代码里_ Encoding.Default这一行要非常谨慎宁可多花成本做一次内容级启发式判断——比如统计合法 UTF-8 序列占比——也不要无脑回落到系统默认编码。列表模式这里最好的地方是你能把文件头字节序列当成一种“形状”来描述而不是写一长串下标访问和逻辑与。3.3 用模式匹配重构支付渠道分发后端同学对这段代码应该很有共鸣。一个支付方法进来一个渠道参数按渠道走不同的处理逻辑。用传统 C# 写法if (channel wechat) { return WechatPay(order); } else if (channel alipay) { return AlipayPay(order); } else if (channel unionpay) { return UnionpayPay(order); } else { throw new NotSupportedException(不支持的渠道); }改成 switch 表达式加常量模式return channel switch { wechat WechatPay(order), alipay AlipayPay(order), unionpay UnionpayPay(order), _ throw new NotSupportedException($不支持的渠道: {channel}) };这段代码的改进不只是短了一点。switch 表达式强制每个分支都以表达式结尾天然适合这种“一个入口映射一个出口”的分发逻辑。配合枚举类型还能进一步利用穷尽性检查return payChannel switch { PayChannel.Wechat WechatPay(order), PayChannel.Alipay AlipayPay(order), PayChannel.Unionpay UnionpayPay(order), _ throw new NotSupportedException($不支持的渠道: {payChannel}) };当PayChannel是枚举时编译器可以列出所有枚举值。以后有人新增了一个支付渠道但忘记更新这段代码编译器有机会给出警告而不是留到线上跑出一个 NotSupportedException。这在代码评审阶段就能提前暴露问题比测试期发现要便宜得多。实操下来我的看法是任何“一个输入按类型或枚举分发到不同行为”的地方都是模式匹配的第一批改造对象。从 20 行的 if-else 缩到 8 行不只是少敲几行键盘更重要的是把业务分支变成了一张肉眼可检查的表。4. 模式匹配与设计模式的关系4.1 容易混淆的两个“模式”在所有搜索热词里“设计模式”“java策略模式”“23种设计模式记忆口诀”占了很大的比重。很多人第一次看到“第19章 模式与模式匹配”时会以为这章在讲设计模式结果翻开书发现讲的是语法刚建立的概念又被打散了。把这两个概念彻底分清楚你后面学任何东西都不会绕。设计模式是软件工程层的方法论。它研究的是“在特定场景下如何组织类和对象之间的关系”比如策略模式告诉你“把算法封装成一组可互换的策略对象”工厂模式告诉你“把创建对象的过程收敛到一个工厂里”。它不依赖任何具体语法是纯设计层面的东西。模式匹配是语言语法层的机制。它研究的是“如何在代码里描述一个数据的形状并由语言运行时完成匹配和取值”。它不关心你系统的架构怎么搭只负责让你的分支逻辑写得更简洁、更安全。一句话总结设计模式是“架构怎么做”的经验模式匹配是“分支怎么写”的语法。一个是软件工程一个是程序设计语言。考试和面试问到“设计模式期末怎么复习”背的是 UML 和类关系问到“模式匹配”写的是 match 和 switch 表达式。4.2 策略模式为什么可以被模式匹配简化策略模式是最典型的“原本为了绕过语言局限”而产生的模式。它的核心目的把一系列可以互换的算法封装成对象运行时可切换。这在 Java 早期版本里是标准答案因为当时的语言没有函数类型没有 switch 表达式没有模式匹配你只能通过接口多态来实现“按类型选行为”。但如果你的场景只是“根据一个枚举或字符串分发到固定逻辑”策略模式的复杂度其实是超出需要的。看个支付场景的对比。策略模式实现public interface IPaymentStrategy { void Pay(Order order); } public class WechatPayStrategy : IPaymentStrategy { public void Pay(Order order) { /* 微信支付逻辑 */ } } public class AlipayPayStrategy : IPaymentStrategy { public void Pay(Order order) { /* 支付宝支付逻辑 */ } } // 使用时要先注册或维护一个具体实例 var strategy strategyFactory.Create(channel); strategy.Pay(order);模式匹配实现void Pay(PayChannel channel, Order order) { switch (channel) { case PayChannel.Wechat: // 微信支付逻辑 break; case PayChannel.Alipay: // 支付宝支付逻辑 break; default: throw new NotSupportedException(); } }如果只是支付逻辑分发策略模式引入了接口、实现类、工厂三层结构换来的是运行时算法可替换、每个策略类的代码相互隔离。但如果业务根本不需要运行时替换策略只是习惯性地套用设计模式那这些类就变成了不必要的间接层。模式匹配在这里是更轻量的选择。那什么时候该保留策略模式当你的策略有状态、可组装、运行时需要动态切换或者需要被单独测试和复用策略模式依然有价值。比如订单计价规则普通价、会员价、促销价、叠加优惠每种策略还依赖不同上下文这种情况模式和单独类能更好地隔离复杂度。我的判断标准是如果行为是静态固定的“类型到行为”映射优先用模式匹配如果行为是可插拔的、需要动态组合的算法族优先用策略模式。两者不是替代关系而是适用层级不同。4.3 模式匹配对代码设计的影响模式匹配普及以后一些经典设计模式的“必要性”下降了。最典型的是访问者模式Visitor。这个模式的初衷是在不修改原有类的前提下给一组类增加新操作。Java 老代码里经常用访问者模式来处理 AST、处理形状对象集。但它实现起来极其繁琐——每个类要加 accept 方法每个访问者要实现所有 visit 重载。在支持模式匹配和密封类sealed class的语言里同样的需求可以直接用模式匹配解决把类层级封死然后用一个 switch 表达式覆盖所有子类编译器帮你检查穷尽性。C#、Java 21 都支持 sealed 类型配合模式匹配访问者模式的复杂度完全可以直接砍掉。这对代码设计的影响是深远的。以前你需要用额外的对象、额外的间接层去模拟“按类型分派行为”这件事现在语言直接支持了。你在面对设计问题时会多一个选项这个问题是真的需要引入一个模式还是可以用语言特性本身解决很多教科书里的设计模式本质上都是在补足语言表达能力的缺口。语言进步了模式自然就不再是必选项。顺着这个思路往后走模式匹配还给了一种“代数数据类型”的思维。类型不是单纯的数据容器而是“几种可能结构的集合”。一个Shape可以是一个Circle或Rectangle或Triangle模式匹配让你处理这些可能结构时不必漏掉任何一个。这也是为什么很多开发者一旦习惯了模式匹配看旧的 if-else 类型分发代码就越看越别扭——因为语言已经给了你更强的表达工具再回去用弱工具属实没必要。5. 常见问题与排查技巧实录5.1 五个典型报错和排查方法实践的时候总会遇到一些报错和奇怪行为整理成一张速查表遇到问题直接对号入座。场景报错或异常表现常见原因解决方式C# switch 表达式CS8509并非所有代码路径都返回了值遗漏了某些类型的分支且没有_兜底补全分支或加_ throw new NotSupportedException()C# 属性模式编译错误“名称 X 不在上下文中”在属性模式里绑定的变量名作用域仅在对应分支内确认变量是在分支体内使用不是分支外Java 21 switch“Illegal fall-through”新版 switch 表达式不允许 case 穿透使用-箭头语法不要用旧的:加 breakPython 3.8/3.9SyntaxError: invalid syntax当前环境 Python 版本低于 3.10不支持 match-case升级解释器或改用 if-elif 兼容旧版本C# 列表模式模式顺序写反导致第一个分支永远匹配不上更具体的模式放在了更宽泛的模式之后把具体模式移到前面..更长的约束放在前面CS8509这个错误很值得单独说一句。新人在 Cod 里第一次写 switch 表达式经常被它卡住明明逻辑都对为什么编译器非说没有覆盖所有路径原因就是缺一个_兜底。传统 if-else 不检查你是不是把每个情况都想到了运行到了没覆盖的分支也只会静默滑过去或返回一个默认值。switch 表达式把这个检查提前到了编译期这其实是一个功能不是 bug。养成习惯写 switch 表达式时要么保证覆盖完整要么主动写_处理例外情况。5.2 实操中容易踩的坑列表模式里的..是 C# 11 的新朋友很多人第一次用会把它和数学里的..范围运算符搞混。在列表模式里[1, ..]表示“以 1 开头后面任意个元素”这里的..是模式的一部分不是范围运算符。两者语法长得像但语义完全不同写的时候注意上下文。关系模式排列也有讲究。C# 对score switch { 60 ..., _ ... }这类写法没有强制的顺序约束但编译器会给出“此模式已被前面的模式捕获将无法匹配”的警告。遇到这种警告别无视它通常意味着你的分支顺序有问题或者某个关系条件写宽了。我在代码评审里见过几次这种警告被当作噪音忽略后来跑出来的结果都是预期之外的。Python 的 match-case 有一个非常典型的误用很多人以为它只是更高级的 switch 语句往里写match x: case 1: case 2:这样的 C 风格代码。实际上 Python 的 match 做的是结构匹配不是简单的等值判断。最直观的写正确方法是搞清楚case [1, 2]是匹配一个列表且恰好两个元素为 1 和 2而不是匹配一个等于[1, 2]的元组——两者行为接近但一个针对列表一个可以是任意可迭代结构。如果你拿到一个 generator 或者 set结果会完全不一样。所以 Python 里用 match 前先确认你要匹配的数据到底是不是可解构的类型。5.3 新手上手最快的三条路径直接啃语法列表效率很低我建议按下面三条路径练每一条都是从已有代码出发做加法风险小、见效快。路径一从重构 if-else 链开始。把自己最近写过的一段类型分发逻辑找出来先翻译成 switch 表达式。如果你的代码库里恰好有一些if (x is A a) ... else if (x is B b) ...的结构直接改成 switch这会让你最直观地感受到模式匹配的收益。路径二刷题时主动用新写法。LeetCode 或 PTA 上的题目里很多都涉及枚举、树节点类型、链表节点的不同形状。用模式匹配去写这些分支相当于用大量小题目做专项训练。比如链表中判断“当前节点是否为尾部”就可以写case ListNode { next: null } 这比写node.next null更带感也更接近“数据形状”的思考方式。路径三找一个列表模式能派上用场的日常脚本。比如判断文件魔数、解析二进制头部、校验字节序。这类代码用列表模式重写后可读性提升极其明显一眼就能看出数据的结构约束。验证一下自己是不是真的理解了列表模式就看能不能把一段if (bytes[0] 0x89 bytes[1] 0x50 ...)改写成[0x89, 0x50, 0x4E, 0x47, ..]。这三条路径走完模式匹配基本就能形成肌肉记忆了。之后再看那些不支持模式匹配的老代码库你会明显感觉到一种“差一代”的别扭那时候说明你已经把这一章真正吃进去了。就我个人经验来说模式匹配最重要的价值不是少写几行代码而是它让你在设计分支逻辑之前先把所有可能的输入形状枚举一遍。每次写_ 兜底的时候其实都是在逼自己回答一个问题“这里真的存在无法预料的输入吗”——带着这个问题写代码你的分支质量会比以前高出一截。教材把这一章放在书末也许正是因为它不是入门必需而是你写了一阵子代码、开始琢磨“能不能再干净一点”时才真正读得懂的内容。
返回列表