iosNudge

What are Equatable, Hashable and Comparable, and when does Swift synthesize them?

EasySwiftProtocols 2 min read

TL;DR

  • Equatable: you can compare two values with ==.
  • Hashable: the value can produce a hash. Needed for Set elements and Dictionary keys. It requires Equatable.
  • Comparable: you can order values with <. Needed for sorted(), min(), max().

For structs and enums, Swift writes Equatable and Hashable for you if all stored properties (or associated values) conform.

Equatable

struct Point: Equatable {
    var x: Int
    var y: Int
}

Point(x: 1, y: 2) == Point(x: 1, y: 2)   // true

You did not write ==. Swift compared every stored property.

Custom version when only some fields matter:

struct User: Equatable {
    let id: UUID
    var name: String
    var lastSeen: Date

    static func == (lhs: User, rhs: User) -> Bool {
        lhs.id == rhs.id
    }
}

Hashable

A hash is an Int that summarizes a value. Set and Dictionary use it to find items fast (O(1) on average).

struct Point: Hashable {
    var x: Int
    var y: Int
}

var visited: Set<Point> = [Point(x: 0, y: 0)]
visited.contains(Point(x: 0, y: 0))   // true

The one rule

If a == b, then a.hashValue == b.hashValue.

So if you write a custom ==, write a matching hash(into:) with the same properties.

struct User: Hashable {
    let id: UUID
    var name: String

    static func == (lhs: User, rhs: User) -> Bool { lhs.id == rhs.id }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)      // same field as ==
    }
}

If you hash name but compare only id, two equal users can land in different buckets. The Set then breaks in strange ways.

Do not save hash values

Swift uses a random seed for hashing on each app launch. hashValue changes between runs. Never store it on disk or send it to a server.

Comparable

Comparable needs <. Swift gives you >, <= and >= for free.

struct Version: Comparable {
    let major: Int
    let minor: Int

    static func < (lhs: Version, rhs: Version) -> Bool {
        (lhs.major, lhs.minor) < (rhs.major, rhs.minor)
    }
}

let versions = [Version(major: 2, minor: 0), Version(major: 1, minor: 5)]
versions.sorted()      // [1.5, 2.0]
versions.max()         // 2.0

Tuple comparison (up to six elements) is a neat trick for multi-field ordering.

Swift does not synthesize Comparable for structs. It does synthesize it for enums without raw values, in case order:

enum Priority: Comparable {
    case low, medium, high
}

Priority.low < Priority.high   // true

When synthesis happens

ProtocolStructEnumClass
EquatableYes, if all properties conformYes, if all associated values conformNo
HashableYes, same ruleYes, same ruleNo
ComparableNoYes, if no raw values (associated values must be Comparable)No

Enums without associated values get Equatable and Hashable automatically, even without writing the conformance.

For classes you always write it yourself. Think about whether equality means "same identity" (===) or "same values".

final class Session: Hashable {
    let token: String
    init(token: String) { self.token = token }

    static func == (lhs: Session, rhs: Session) -> Bool { lhs === rhs }
    func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) }
}

In SwiftUI

  • ForEach needs Identifiable or an id: key path. That is about identity, not equality.
  • NavigationStack(path:) and navigationDestination(for:) need Hashable values.
  • .onChange(of:) needs Equatable to know if a value changed.

Common follow-up questions

  • Why does Hashable inherit from Equatable? Hash tables first find a bucket by hash, then use == to confirm the match.
  • What happens on a hash collision? Two different values get the same bucket. The set still works, using ==, but gets slower if it happens a lot.
  • Is hashValue deprecated? Implementing it is deprecated. Implement hash(into:) instead.
  • Should Identifiable and Equatable use the same field? Not always. id says "same item". == can say "same item and same content", which helps UI updates.
  • Can you make a tuple Hashable? Tuples cannot conform to protocols yet, but == and < work on tuples up to six elements.