Rust 编译器在我这儿,是那种半夜发消息的同事:你刚写完最后一行代码,他消息来了——"这个边界情况,你考虑了吗?"
你回"差不多得了"。他不回。下一条消息直接变红灯。
这个梗我讲过不止一次,因为真的隔三差五就来一回。最近又被他堵了一次,堵得无话可说。
给 Vec<f64> 调 sort(),编译直接不过。f64 没实现 Ord——NaN 不满足全序这个道理谁都懂,但编译器不允许你"装着不知道"。
【灯光渐暗,你想起其他语言里那些"看起来排过序"的列表】
Python 里含 NaN 的列表调 sorted() 是能正常返回的,正常到你会怀疑是不是需求方又改过文档。实际上排没排,看 NaN 站在哪。Node 里最常见的 (x, y) => x - y 比较器,NaN 就停在原地看着别人搬家,动都不带动。
C 挺有原则的:比较器不构成全序?未定义。
Rust 的标准答案是 sort_by 配 total_cmp,负 NaN、正 NaN、负零、正零,一个都跑不掉,全给你排出一个确定顺序。最气人的不是它对,是它连"先放着不改"这个选项都不给你。你见过哪门语言把全序当作编译器基础设施的?反正我没见过——甚至没想过"排序"这件事能这么严格。
字符串索引也一样。
Rust 不允许用整数直接访问字符串:UTF-8 的"第 0 个元素"可能是字节、码点、字素簇三个物种,你得先说清楚你要哪一个。"café"长 5 个字节、4 个字符,就这点破事,编译器也要跟你掰扯。
Python 按码点索引,背了额外内存代价;JS 按 UTF-16 码元索引,代价是 "🦀"[0] 打出来半边字符,一个断开的螃蟹,截图发给别人都解释不清。C 直接给你原始字节,读出什么看缘分。
Rust 给你 as_bytes()[0] 还是 chars().nth(0),随便选。真越界了?panic。
panic 确实不体面,但说真的,我宁可程序当场死给我看,也不愿意它带着半拉字符爬到生产环境丢人。这个取向我站 Rust。
还有一个我这几天才悟到的:迭代器是惰性的。
map 出来不消费,闭包根本不会执行。编译器给你警告:this map produces no value。翻译成项目管理黑话就是:你布置了任务,没人验收,等于没干——任务自己也知道自己没干。
别的语言?你写个 map,不消费就不消费,静默得像没发生过。你以为它执行了,它没有;你在生产环境等它输出,它在内存里睡大觉。
事故不在编译期,就在生产期,没有中间态。
差点忘了说:Rust 也不是没有自己的静默。查 docs.rs 的时候看到 BufWriter 的 Drop 不返回 Result,作用域一结束,最后一次 flush 的错误就被悄悄扔掉。而且是 flush 只到内核页缓存,真正落盘还得靠 sync_all。我当时愣了半天——原来这个整天大声说话的编译器,也有悄悄闭嘴的时候。
所以准确说法是:Rust 没有把静默消灭掉,它只是把静默清单摊在你面前,哪一段路没灯,提前告诉你。编译期拒绝不是什么道德胜利,只是把事故时间提前了。早一点知道,你还能骂编译器;晚一点知道,只能坐在生产日志前骂自己。
【关于把 NaN 排进有序世界的裁定书】
经审理查明:Python 和 JS 对 NaN 的排序行为属于"发生了但没人通知",C 属于"发生了但法律上不存在",Rust 属于"发生之前就把你拦在门口"。
裁定:拦在门口胜。但鉴于胜诉方自己也在 Drop 里静默丢过一次错误,本次执行为记过一次,限期补上 fsync。
(友情提示:本文不存在推荐任何人为了排序体验去重写 Redis 的意思。重写一时爽,上线火葬场。评论区扣个"同款被 total_cmp 救过",我们下条再见 🫠)