Effect v3 到 v4 如何适应 Equal.equals 默认结构相等变化并在需要时用 byReference
2026/9/15 14:57:57 网站建设 项目流程

Effect v3 到 v4 如何适应 Equal.equals 默认结构相等变化并在需要时用 byReference

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

Effect v3 升级到 v4 时,Equal.equals的默认比较语义发生了改变:v3 对普通对象和数组使用引用相等,而 v4 默认改为结构相等。如果你的代码里有用Equal.equals比较对象/数组的地方,或者依赖Equal.equivalence,升级后行为会变化,需要逐处确认;只有个别对象仍必须按引用比较时,才用 v4 新增的Equal.byReference/Equal.byReferenceUnsafe显式退回引用语义。本文基于仓库中的迁移文档 migration/equality.md 与源码 packages/effect/src/Equal.ts 给出完整的对照与验证方式。

前提:你正在把effect从 v3 升到 v4。MIGRATION.md 说明 Effect v4 目前处于 beta 阶段、API 可能在不同 beta 版本间变动,v4 所有生态包共享同一个版本号(例如使用effect@4.0.0-beta.0时,配套的@effect/sql-pg也是@effect/sql-pg@4.0.0-beta.0);本仓库当前packages/effect/package.json中的版本号为4.0.0-rc.115effect/Equal模块在 v3 到 v4 的导入映射中保持不变(见 migration/v3-to-v4.md 的 Import Map:effect/Equal -> effect/Equal),所以导入路径不用改,要改的是对比较行为的预期。

v3 与 v4 的默认比较行为对照

在 v3 中,Equal.equals对普通对象和数组使用引用相等,结构比较只能在structuralRegion内临时启用;区域外两个内容相同的对象不相等(引自 migration/equality.md):

// v3 import { Equal } from "effect" Equal.equals({ a: 1 }, { a: 1 }) // false — reference equality Equal.equals([1, 2], [1, 2]) // false — reference equality

在 v4 中,Equal.equals默认就是结构相等。普通对象、数组、MapSetDateRegExp都按值比较,不需要任何额外开启:

// v4 import { Equal } from "effect" Equal.equals({ a: 1 }, { a: 1 }) // true Equal.equals([1, [2, 3]], [1, [2, 3]]) // true Equal.equals(new Map([["a", 1]]), new Map([["a", 1]])) // true Equal.equals(new Set([1, 2]), new Set([1, 2])) // true

v4 的源码中已经没有structuralRegion,以上示例中的结果即为 v4 的默认行为,可直接作为升级后的预期值。另外两点需要注意的行为变化:

  • 实现了Equal接口的对象不受影响:仍然走自定义相等逻辑,与 v3 一致。
  • NaN相等性变了:v3 中Equal.equals(NaN, NaN)返回false(遵循 IEEE 754),v4 中NaN等于NaN
Equal.equals(NaN, NaN) // v3: false, v4: true

packages/effect/src/Equal.ts 的文档进一步描述了 v4 结构比较的细节:Date按 ISO 字符串比较,RegExp按字符串表示比较,数组逐元素比较,MapSet按条目比较且与顺序无关(例如new Map([["a", 1], ["b", 2]])new Map([["b", 2], ["a", 1]])相等),普通对象递归比较可枚举键,没有Equal实现的函数按引用比较。

Equal.equivalence改成Equal.asEquivalence

equals包装成Equivalence的函数在 v4 中更名了(引自 migration/equality.md):

// v3 Equal.equivalence<number>() // v4 Equal.asEquivalence<number>()

源码示例展示了 v4 的用法(文档示例):

import { Array, Equal } from "effect" Array.dedupeWith([1, 2, 2, 3, 1], Equal.asEquivalence<number>()) // => [1, 2, 3]

升级时对全项目检索Equal.equivalence,把命中的调用整体替换为Equal.asEquivalence即可;asEquivalence在源码中标注@since 4.0.0,v3 中没有这个 API,不存在向后兼容问题。

需要引用相等时:byReferencebyReferenceUnsafe

默认改成结构相等后,如果你的某些对象必须保持"同一引用才算相等"的 v3 行为(例如用对象身份做缓存键、监听器去重等),v4 提供两个 opt-out(引自 migration/equality.md 与 packages/effect/src/Equal.ts):

import { Equal } from "effect" const obj = Equal.byReference({ a: 1 }) Equal.equals(obj, { a: 1 }) // false — reference equality

两者区别:

  • byReference(obj)— 返回一个使用引用相等的Proxy,原对象不变。文档强调每次调用都会创建一个新的proxy,所以byReference(x) !== byReference(x);属性读取会透传回原对象(aRef.x仍是1)。
  • byReferenceUnsafe(obj)— 不创建 proxy,直接把原对象自身标记为引用相等,返回的就是原对象(byReferenceUnsafe(x) === x)。文档说明它性能更好,但标记对对象生命周期内是不可逆的,会永久改变该对象的比较方式。

源码文档给出的完整示例(文档示例结果):

const a = { x: 1 } const b = { x: 1 } Equal.equals(a, b) // => true const aRef = Equal.byReference(a) Equal.equals(aRef, b) // => false Equal.equals(aRef, aRef) // => true aRef.x // => 1

以及byReferenceUnsafe的版本:

const obj1 = { a: 1, b: 2 } const obj2 = { a: 1, b: 2 } const marked = Equal.byReferenceUnsafe(obj1) Equal.equals(obj1, obj2) // => false Equal.equals(obj1, obj1) // => true marked === obj1 // => true

从实现看,byReference内部就是byReferenceUnsafe(new Proxy(obj, {})),两者都通过一个内部WeakSet生效:equals只要发现任一操作数在该集合里(且两者不是同一引用)就返回false。选择原则以文档措辞为准:不想动原对象就用byReference,接受永久标记以换取免 proxy 开销就用byReferenceUnsafe

验证升级后的行为

升级完成后,把下面这段 v4 行为检查粘到你的项目里跑一遍(vitest或任何能执行 TS 的运行环境均可),预期结果就是文档示例中的值:

import { Equal } from "effect" // 默认结构相等 console.log(Equal.equals({ a: 1 }, { a: 1 })) // 期望 true console.log(Equal.equals([1, [2, 3]], [1, [2, 3]])) // 期望 true console.log(Equal.equals(new Map([["a", 1]]), new Map([["a", 1]]))) // 期望 true console.log(Equal.equals(NaN, NaN)) // 期望 true // 引用相等 opt-out const ref = Equal.byReference({ a: 1 }) console.log(Equal.equals(ref, { a: 1 })) // 期望 false console.log(Equal.equals(ref, ref)) // 期望 true

如果上面任何一项与期望不符,说明依赖的effect版本仍是 v3 或版本没有对齐(v4 生态包要求同版本号,见 MIGRATION.md)。另外注意Equal.ts源码中的 doctest(带import.meta.vitest标记的示例块)本身就覆盖了这些断言,可以参考 packages/effect/src/Equal.ts 中equalsbyReferencebyReferenceUnsafe的文档示例对照。

限制与注意事项

来自 packages/effect/src/Equal.tsequals文档的 Gotchas,使用 v4 结构相等时要知道:

  • 比较结果按对象对缓存在 WeakMap 中:对象在第一次比较之后不应再被修改,否则后续比较结果不可信。
  • MapSet的比较是 O(n²) 量级,大集合上要注意开销。

这两条是 v4 默认结构相等的固有代价:v3 时代按引用比较时不存在这些问题。

参考

  • migration/equality.md — v3 到 v4 的相等性迁移说明(本文主要依据)
  • migration/v3-to-v4.md — 完整的 v3 到 v4 导入映射与 API 差异
  • MIGRATION.md — v3 到 v4 迁移总指南与版本约定
  • packages/effect/src/Equal.ts —equalsbyReferencebyReferenceUnsafeasEquivalence的源码与 doctest

【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询