Skip to Content

Equality and Hashing

The rule for == and != is a language-wide split (ADR-0102): value-semantic types compare by value, reference-semantic types compare by identity. A type that overloads operator == follows the overload; when it does not, this page’s rule applies.

The Two-Sided Rule

TypeWhat == Compares
scalars (integer family / float / decimal / bool / char)numeric value
stringcontents (ordinal, code point by code point)
enum / flags (including payload variants)value
structfield-by-field value
datadeep structural equality
class instancereference identity
the five container families (List / array / Set / Map / frozen)reference identity
tuplereference identity (separate instances with the same contents are unequal; compare contents component by component)
function valuereference identity (named functions are interned singletons; each lambda evaluation creates a new closure)
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

Two Pt instances with identical contents are not equal: each new builds a separate object, and only aliases of the same object are equal. Sp is a struct: assignment copies, == compares field by field, and identical contents give True.

Payload variants of enums also compare by value: Opt.Some(5) == Opt.Some(5) is True. decimal equality is purely numeric: 1.10d64 == 1.1d64 is True (trailing zeros do not take part), and 0.1d64 + 0.2d64 == 0.3d64 also holds; decimal arithmetic is lossless at this precision.

Tuples and Function Values: Reference Identity

func Add1(x: int) -> int { return x + 1; } func Add2(x: int) -> int { return x + 2; } func main() -> int { val p = (1, "a"); val q = (1, "a"); 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 is two constructions, unequal despite identical contents; p == alias is the same instance. A function value’s identity is the (function, closure environment) instance: named functions are interned into singletons, so a == b is always True and a == c is False; each lambda evaluation creates a new closure, so the identically written f1 == f2 is False; named versus lambda is False. Nullable pairs (nullable against nullable, and mixed nullable / non-nullable) follow the same rule after unwrapping; comparing a non-nullable value against null raises MS3101 (a dead comparison, the rule for every non-nullable type in the language); mismatched-shape pairs (tuple against list) raise MS4201. Function pairs with differing signatures (parameter or return, either one differs) raise MS4201 at compile time, uniformly for annotated variables, inferred locals, and bare names; ordering comparisons and arithmetic still raise MS3104. The any channel is an exemption zone: dynamic value comparisons go by runtime object identity, so two function values compared through any compile fine and are always False, on a separate track from the typed surface.

Containers: Identity Equality + ContentEquals

For all five container families, == is reference identity, consistent with the reference-sharing semantics of assignment and argument passing. To compare by contents, use ContentEquals, which recurses element by element and is safe on cycles:

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["a"] = 1; val m2: map<string, int> = new map<string, int>(); m2["a"] = 1; print(m1.ContentEquals(m2));
False True True True
  • Set content equality is “same element set”, independent of order;
  • Map is “same key set plus per-key value equality”;
  • frozen == is identity too: freeze([1, 2]) == freeze([1, 2]) is False (each construction is a fresh deep copy); see Frozen for details.

The lookup and removal methods across the container pages all follow this page’s rule: list.Contains / IndexOf / Remove(v) compare struct elements by field values and class elements by identity; set / map go by the hashing rule.

Hashing and Keying

Map keys and Set elements hash by the default rule above: struct / scalar / string / enum by value, class instances by identity. With a class as key, only the very same object looks back up; with a struct as key, same contents means same key:

val a = new Pt() { x: 1, y: 2 }; val byClass: map<Pt, string> = new map<Pt, string>(); byClass[a] = "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] = "b"; print(byStruct[new Sp() { x: 1, y: 2 }]);
a False b

For “same contents, same key”, use a struct or reduce the key to a scalar first; the advantage of a class key is a hash that stays stable for the object’s lifetime (mutating fields does not affect it).

Edge Cases

  • NaN == NaN is False, and ordering comparisons involving NaN are all False as well (IEEE 754 semantics); see Floats;
  • Container null checks work as usual: xs == null is legal and tests the reference itself;
  • Comparisons of boxed values in any are an exemption zone, judged by runtime object identity (two function values compared through any compile fine and are always False), on a separate track from the typed surface; see Any;
  • How to declare an == overload and the full set of overloadable operators are covered in the custom operators section of Operators.
Last updated on October 11, 2026