Skip to Content

判等与哈希(Equality)

== 与 != 的口径全语言二分(ADR-0102):值语义类型比值,引用语义类型比身份。类型用 operator == 重载后按重载走,未重载时按本页口径。

二分口径

类型== 比什么
标量(整数族 / float / decimal / bool / char)数值
string内容(序数逐码点)
enum / flags(含载荷变体)值
struct逐字段值
data深结构相等
class 实例引用同一性
容器五族(List / 数组 / Set / Map / frozen)引用同一性
元组引用同一性(同内容异实例不等;比内容逐分量)
函数值引用同一性(具名 intern 单例恒等;lambda 每次求值新闭包)
class Pt { public var x: int = 0; public var y: int = 0; } struct Sp { public var x: int = 0; public var y: int = 0; } func main() -> int { val a = new Pt() { x: 1, y: 2 }; val b = new Pt() { x: 1, y: 2 }; print(a == b, a == a); val q = new Sp() { x: 1, y: 2 }; val q2 = new Sp() { x: 1, y: 2 }; print(q == q2); return 0; }
False True True

两个内容相同的 Pt 不相等:各自 new 出来是两个对象,同一对象的别名才相等。Sp 是 struct,赋值即拷贝,== 逐字段比,内容同即 True。

enum 的载荷变体同样按值:Opt.Some(5) == Opt.Some(5) 为 True。decimal 的相等是纯数值:1.10d64 == 1.1d64 为 True(尾零不参与),0.1d64 + 0.2d64 == 0.3d64 也成立,十进制运算在此精度无损。

元组与函数值:引用同一性

func Add1(x: int) -> int { return x + 1; } func Add2(x: int) -> int { return x + 2; } func main() -> int { val p = (1, "甲"); val q = (1, "甲"); val alias = p; val a = Add1; val b = Add1; val c = Add2; val f1 = (x: int) => x * 2; val f2 = (x: int) => x * 2; print(p == q, p == alias, a == b, a == c, f1 == f1, f1 == f2); return 0; }
False True True False True False

p == q 是两次构造,内容相同也不等;p == alias 是同一实例。函数值的身份是(函数, 闭包环境)实例:具名函数 intern 成单例,a == b 恒 True,a == c 为 False;lambda 每次求值产生新闭包,写法相同的 f1 == f2 为 False;具名与 lambda 相比为 False。可空对(可空比可空、可空与非空混对)解包后同律,非可空值与 null 的比较报 MS3101(死比较,全语言一切非可空类型同律);异形对(元组比列表)报 MS4201。跨签名的函数对(参数或返回任一不同)编译期一律报 MS4201,注解变量、推断局部与裸名同律;序比较与算术仍报 MS3104。any 通道是豁免域:动态值比较按运行时对象身份,两个函数值经 any 相比编译通过、恒 False,与类型面分轨。

容器:身份判等 + ContentEquals

容器五族的 == 一律引用同一性,与赋值/传参的引用共享语义一致。按内容比较用 ContentEquals,逐元素递归、环安全:

val l1: list<int> = [1, 2, 3]; val l2: list<int> = [1, 2, 3]; print(l1 == l2, l1.ContentEquals(l2)); val s1: set<int> = [1, 2]; val s2: set<int> = [2, 1]; print(s1.ContentEquals(s2)); val m1: map<string, int> = new map<string, int>(); m1["甲"] = 1; val m2: map<string, int> = new map<string, int>(); m2["甲"] = 1; print(m1.ContentEquals(m2));
False True True True
  • Set 的内容判等是「同元素集」,与顺序无关;
  • Map 是「同键集 + 逐键值等」;
  • frozen 的 == 同样是身份:freeze([1, 2]) == freeze([1, 2]) 为 False(每次构造都是新的深拷副本),详见深冻。

容器各页的查找与删除方法全部走本页口径:list.Contains / IndexOf / Remove(v) 对 struct 元素按字段值比、对 class 元素按身份比;set / map 按哈希口径。

哈希与作键

Map 的键与 Set 的元素按上面的默认口径取哈希:struct / 标量 / string / enum 按值,class 实例按身份。class 作键时只有同一个对象查得回来,struct 作键则同内容即同键:

val a = new Pt() { x: 1, y: 2 }; val byClass: map<Pt, string> = new map<Pt, string>(); byClass[a] = "甲"; print(byClass[a], byClass.Contains(new Pt() { x: 1, y: 2 })); val q = new Sp() { x: 1, y: 2 }; val byStruct: map<Sp, string> = new map<Sp, string>(); byStruct[q] = "乙"; print(byStruct[new Sp() { x: 1, y: 2 }]);
甲 False 乙

想要「同内容即同键」,用 struct 或把键先规约成标量;class 作键的优势是对象存续期内哈希稳定(改字段不影响)。

边界

  • NaN == NaN 为 False,含 NaN 的序比较也全是 False(IEEE 754 语义),见浮点;
  • 容器判空照旧:xs == null 合法,判的是引用本身;
  • any 装箱值的比较是豁免域,按运行时对象身份判(两个函数值经 any 相比编译通过、恒 False),与类型面分轨,详见 任意类型(Any);
  • 重载 == 的声明法与可重载运算符全集见运算符的自定义运算符一节。
最后更新于 2026年10月9日