swift · memo
In one line: Equatable = value equality (==); Hashable adds
hash(into:) under one rule — a == b ⇒ equal hashes
(never the converse); Comparable adds a strict total order from only
<. Set/Dictionary find a key by hash first, then ==
— break the contract or mutate a key and elements vanish.
Download PDF Print view LaTeX source
How it works
- Equatable: reflexive, symmetric, transitive;
!=is free. Synthesized (SE-0185, Swift 4.1) when you declare it and every stored property / associated value conforms; payload-less enums get it without declaring. Never for classes. Must be declared in the type’s own file. - Hashable: Equatable. Implement
hash(into: inout Hasher)andcombinethe same fields==reads (SE-0206, Swift 4.2); implementinghashValueis deprecated.Hasher= SipHash-1-3, randomly seeded per process (HashDoS resistance);SWIFT_DETERMINISTIC_HASHING=1fixes the seed for tests. Never persist a hash value. - Comparable: Equatable. Only
<is required;>,<=,>=,min/max,sort(), rangesa...bcome free. Must be a strict total order consistent with==(!(a<b) && !(b<a)⇒a == b). Synthesized only for enums without raw values whose payloads are Comparable — declaration order (SE-0266, 5.3). Tuples (arity ≤ 6) compare lexicographically: delegate to them. - Identifiable (SE-0261, 5.1):
var id: IDwhereID: Hashable; classes getObjectIdentifier(self)by default. Identity (which row is this?) ≠ equality (did its content change?). SwiftUI keys state, animation and diffing onid. - NSObject bridges: Swift
==callsisEqual(_:),hashValuereturnshash. DefaultisEqualis identity. Subclass: overrideisEqual(_:)+hash— a customstatic ==orhash(into:)is not whatSet,NSSetor Foundation call.
Example
struct User: Hashable { // identity = id
let id: Int; var name: String
static func == (l: User, r: User) -> Bool { l.id == r.id }
func hash(into h: inout Hasher) { h.combine(id) } // same field
}
struct Version: Comparable {
let major, minor, patch: Int
static func < (l: Self, r: Self) -> Bool { // only <
(l.major, l.minor, l.patch) < (r.major, r.minor, r.patch)
}
}
final class Node: Hashable { // identity
static func == (l: Node, r: Node) -> Bool { l === r }
func hash(into h: inout Hasher) {
h.combine(ObjectIdentifier(self)) } // stable
}
Which equality for which type?
| Type | Choose |
|---|---|
| value struct | synthesize; hand-write only to exclude fields (cache, timestamps) |
entity (User) | ==/hash on id; Identifiable for SwiftUI |
| class, identity | === + ObjectIdentifier (safe to mutate) |
| class, value-based | only if the hashed fields are let |
NSObject subclass | isEqual(_:) + hash, together |
| normalised keys | == and hash on the SAME normal form (lowercased) |
Interview traps
hashcombining a field==ignores (panel B) — equal values, different buckets.- Mutable class with value-based hash inside a
Set/as a dict key (panel C). isEqualoverridden,hashforgotten: default hash is identity → brokenNSSet/Set.Set/Dictionaryorder differs per launch — never assert or persist it.Double.nan == .nanisfalse: breaks reflexivity,contains(.nan)fails,sortwith NaN is unspecified.ForEach(items, id: \.self)= identity is the whole value: editing a row changes its identity (state reset, wrong animation); duplicates collide. Array index as id breaks on insert/reorder.- Hash collisions are legal: unequal values may share a hash.
Remember
“Equal ⇒ same hash; hash ⊆ equality; never mutate a key;
< buys all of Comparable; id is who, == is what.”
Likely questions
- The Hashable contract? —
a == b⇒ equal hashes. - Why
hash(into:)overhashValue? — seeded SipHash, no hand mixing. - Minimum for Comparable? —
<(plus==from Equatable). - Identifiable vs Hashable? — stable identity vs value equality.
- NSObject in a
Set? — overrideisEqual(_:)andhash. - Mutable class as a dict key? — only with identity hashing or
lethashed fields. - Why does
Setorder change per launch? — per-process randomHasherseed.