
Effect 补丁解析Equal.equals与Hash.hash如何优雅处理 Invalid Date 和 DataView【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect本篇解读 Effect 仓库中一条针对effect包的 patch 级变更记录changeset它修复了Equal.equals和Hash.hash在处理无效日期Invalid Date与DataView值时会抛出异常的缺陷。读完本文你将理解 Effect 结构化相等性与哈希机制的边界处理策略、为什么这类不抛异常语义对HashMap/HashSet至关重要并能结合仓库源码定位到每一处修复的具体实现位置。1. 这条变更记录说了什么补丁文件 .changeset/pre/calm-dates-view.md 的完整内容如下--- effect: patch --- Fix Equal.equals and Hash.hash to handle invalid dates and DataView values without throwing.它包含两部分信息变更级别effect: patch即只提升effect主包的补丁号x.y.z 中的 z符合语义化版本中向后兼容的缺陷修复的定义变更内容Equal.equals结构化深比较与Hash.hash结构化哈希两个 API 此前遇到无效日期和DataView两类值时会直接抛异常现在改为正常返回结果而不抛异常。该文件位于.changeset/pre/目录说明仓库当前处于 changesets 的pre-release 阶段——从 .changeset/pre.json 可以看到mode: pre且tag: rc意味着这批变更将随下一个 rcrelease candidate版本统一发布。目录中还有大量同批次的变更文件如eff-*.md、fix-*.md等本条只是其中之一。2. 问题背景两类危险值为什么会抛异常要理解这个修复先看这两个值在 JavaScript 中的特殊行为。2.1 无效日期getTime()返回 NaNtoISOString()直接抛错const bad new Date(not-a-date); bad.getTime(); // NaN bad.toISOString(); // RangeError: Invalid time value一个典型的旧实现如果写成string(date.toISOString())那么对无效日期哈希时就会抛出RangeError——而Hash.hash的设计契约是任何值都能返回一个 number一旦抛异常所有以它为底层依赖的HashMap、HashSet操作插入、查找、遍历都会连带崩溃。2.2 DataView非可迭代的 TypedArrayDataView是ArrayBuffer的通用字节视图它本身不可迭代、也没有length语义而是通过byteOffsetbyteLength描述其在底层 buffer 中的窗口。任何按数组元素遍历方式处理 ArrayBuffer 视图的代码遇到DataView都可能越界或抛错。这两类值恰好都落在结构化比较/哈希这类需要递归展开内部数据的代码路径上因此是回归测试的常见盲点。3. 源码级修复细节3.1Hash.hash的 Date 分支NaN 短路修复后的实现位于 Hash.ts} else if (self instanceof Date) { if (Number.isNaN(self.getTime())) { return string(Invalid Date) } return string(self.toISOString()) }逻辑非常直接先检查getTime()是否为NaN若是则将所有无效日期统一映射到常量字符串Invalid Date的哈希值只有合法日期才调用toISOString()。这带来两个确定的性质无效日期永不抛异常所有无效日期哈希值相同Hash.hash(new Date(x)) Hash.hash(new Date(y))恒为 true与无效日期之间视为相等的比较语义保持一致——哈希与相等性在语义上对齐这是HashMap正确性的前提哈希不同的值必须不相等但相等的值允许哈希不同此处选择直接相同。3.2Hash.hash的 DataView 分支包一层Uint8Array再哈希在对象分支内部DataView在通用数组路径之前被单独拦截见 Hash.ts} else if (self instanceof DataView) { return array(new Uint8Array(self.buffer, self.byteOffset, self.byteLength)) } else if (Array.isArray(self) || ArrayBuffer.isView(self)) { return array(self as any) }关键点它构造了一个仅覆盖DataView自身窗口的Uint8Array注意传入了byteOffset与byteLength而非整个底层 buffer然后复用已有的array哈希路径。这样哈希值只反映DataView实际可见的字节而不受共享 buffer 中其余区域的影响——从源码结构看这正是防止两个指向同一 buffer 但窗口不同的 DataView 被错误地哈希/比较的必要设计。3.3Equal.equals的 Date 分支NaN 等价判定对应修复位于 Equal.ts 的compareObjects函数中} else if (self instanceof Date) { if (!(that instanceof Date)) return false const selfTime self.getTime() const thatTime that.getTime() return selfTime thatTime || (Number.isNaN(selfTime) Number.isNaN(thatTime)) }由于NaN NaN为 false代码显式补上了双 NaN 视为相等的分支。于是Equal.equals(new Date(bad1), new Date(bad2))返回true而不是false或异常与 3.1 节中无效日期哈希相同的语义严格呼应。3.4Equal.equals的 ArrayBuffer 视图分支DataView 按字节窗口比较Equal.ts 中对所有 ArrayBuffer 视图TypedArray 与 DataView统一处理} else if (ArrayBuffer.isView(self)) { const selfIsDataView self instanceof DataView if ( !ArrayBuffer.isView(that) || self.byteLength ! that.byteLength || selfIsDataView ! (that instanceof DataView) ) { return false } if (selfIsDataView) { const thatDataView that as DataView return compareTypedArrays( new Uint8Array(self.buffer, self.byteOffset, self.byteLength), new Uint8Array(thatDataView.buffer, thatDataView.byteOffset, thatDataView.byteLength) ) } return compareTypedArrays(self as Uint8Array, that as Uint8Array) }设计要点有三类型对齐检查DataView与TypedArray互不相等selfIsDataView ! (that instanceof DataView)时直接返回 falseUint8Array与DataView即便字节相同也不等价长度前置byteLength不同立即短路避免无谓的逐字节比较窗口化字节比较对DataView同样以byteOffset/byteLength截取自身窗口的Uint8Array再逐字节比对比较的是视图内容而非底层 buffer。该分支被包裹在withVisitedTracking与Hash.hash快筛Equal.ts 中先比较两侧哈希值不同则直接返回 false之中因此在实际使用HashMap/HashSet时DataView作为键或元素的哈希与比较都不会再触及旧实现中会抛异常的代码路径。4. 为什么不抛异常是重要语义在 Effect 的类型系统里Equal与Hash是所有结构化容器HashMap、HashSet、Data的相等性等的基石。这两个函数是纯查询操作对任意输入值给出相等哈希值是多少的确定回答。一旦对某类合法构造的 JS 值new Date()与new DataView(new ArrayBuffer(4))都是完全合法的构造抛出异常就违反了这一契约且异常会在容器遍历、序列化对比等深层调用栈中冒出难以定位。本补丁把边界情况的语义收敛为输入Equal.equalsHash.hash两个无效日期true双 NaN 相等相同常量哈希无效日期 vs 合法日期false不同哈希两个同窗口的DataViewtrue相同哈希DataViewvs 同窗口的Uint8Arrayfalse类型不同不保证相同这也与 migration/equality.md 中关于 Effect 相等性语义的迁移文档方向一致相等性是可组合、可预测的而不是遇到奇怪值就崩。5. 本地验证方式无需修改仓库可直接在 scratchpad 这类示例入口或任意 Effect 项目中运行以下代码验证补丁后的行为需安装包含该 patch 的effect版本即.changeset/pre.json中 rc 阶段发布之后的版本import * as Equal from effect/Equal import * as Hash from effect/Hash const bad1 new Date(not-a-date) const bad2 new Date() console.log(Equal.equals(bad1, bad2)) // true不再抛异常 console.log(Hash.hash(bad1) Hash.hash(bad2)) // true const buf new ArrayBuffer(4) const view new DataView(buf, 0, 2) const view2 new DataView(new ArrayBuffer(2), 0, 2) console.log(Equal.equals(view, view2)) // 按窗口字节比较不抛异常 console.log(Hash.hash(view)) // number不抛异常6. 小结与延伸阅读这条 patch 级变更虽只有一句话背后体现的是 Effect 对基础原语全输入域健壮性的要求结构化比较/哈希函数必须对 JS 的所有合法值——包括最反直觉的Invalid Date和非可迭代的DataView——给出确定、一致、不抛异常的结果。核心参考文件变更记录.changeset/pre/calm-dates-view.md、pre 模式配置 .changeset/pre.json相等性实现packages/effect/src/Equal.tsDate 分支 L234-L238ArrayBuffer 视图分支 L258-L274哈希实现packages/effect/src/Hash.tsDate 分支 L117-L121DataView 分支 L136-L137相关背景文档migration/equality.md【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考