Nondeterministic behaviour when hashing a class

There is an interesting discussion going on here in this pitch: [Pitch] Default implementation of Hashable for AnyObject.

When playing with the example code posted by @mbrandonw, I noticed something unusual: nondeterministic program behaviour.

// it.swift

import Foundation

@main
enum Driver {
    static func main() async throws {
        hash_my_class ()
    }
}

// [https://forums.swift.org/t/pitch-default-implementation-of-hashable-for-anyobject/88853/9]
//

class MyClass: Hashable {
    var i: Int

    init(i: Int) {
        self.i = i
    }

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

func hash_my_class() {
    print (#function, "...")
    var dictionary: [MyClass: Bool] = [:]
    let object = MyClass (i: 0)
    dictionary [object] = true
    object.i = 1
    dictionary [object] = false
    // 🛑 Fatal error: Duplicate keys of type 'MyObject' were found in a Dictionary.
    print (#function, "finished")
}

When I build and run it from the command line (also in Xcode (Version 26.5 (17F42))), I get the following nondeterministic result: the program sometimes crashes and sometimes doesn't.

> swiftc -parse-as-library it.swift

> ./it
hash_my_class() ...
Fatal error: Duplicate keys of type 'MyClass' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or
that members of such a dictionary were mutated after insertion.
Trace/BPT trap

> ./it
hash_my_class() ...
hash_my_class() finished

> ./it
hash_my_class() ...
Fatal error: Duplicate keys of type 'MyClass' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or
that members of such a dictionary were mutated after insertion.
Trace/BPT trap

> ./it
hash_my_class() ...
hash_my_class() finished

> ./it
hash_my_class() ...
Fatal error: Duplicate keys of type 'MyClass' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or
that members of such a dictionary were mutated after insertion.
Trace/BPT trap

> ./it
hash_my_class() ...
hash_my_class() finished

> ./it
hash_my_class() ...
Fatal error: Duplicate keys of type 'MyClass' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or
that members of such a dictionary were mutated after insertion.
Trace/BPT trap

Why this nondeterministic behaviour?

1 Like

From the Hasher docs:

Note

Do not save or otherwise reuse hash values across executions of your program. Hasher is usually randomly seeded, which means it will return different values on every new execution of your program. The hash algorithm implemented by Hasher may itself change between any two versions of the standard library.

On every invocation of your program, Hasher is randomly seeded so it produces a different result for hashing object. Sometimes, the hash value after your mutation coincides compatibly with the value from before, sometimes not.

e.g., imagine finding the index of object inside of dictionary's internal storage by taking the hash modulus the number of buckets in the table, and pretend the number of buckets is rounded up to the next power of 2 (so, hash % 2); an even hash would return an index of 0 and an odd hash an index of 1. Hasher could happen to return two even numbers (hitting the assertion), or one even and one not (silently inserting the same object twice into the dictionary).

(Not saying that this is exactly what's happening here, but as an illustrating example.)

7 Likes

You should not change the hash of a reference type while it is used as a key in a Dictionary. See this similar issue.

4 Likes

This is correct. The hash value of a reference type (i.e. class or actor) must only depend on immutable properties. This also means that the Equatable implementation must use those same values, since == returning true implies that both hash values are also equal. [1]


  1. Source: Adopting Common Protocols | Apple Developer Documentation ↩︎

4 Likes

Classic problem of changing EQ/hash in things stored by reference... In obj-c that was partially worked around – NSDictionary keys were always copied, NSSet elements were not.

1 Like

One way to create a discipline around the issue is to have a hashKey property (that’s Hashable) and not making the class itself Hashable. This detaches the stable part of the class identity used for using the object as a key, from its transient “value”. Still you need to make sure when two instances are equal, they return the same hashKey which means using ObjectIdentifier(_) is ruled out. I didn’t get into inheritance and immutable objects.

2 Likes