Why isn’t AnyHashable implemented in pure Swift?

The general rule is that Swift never guarantees that distinct types will generate matching hash values (e.g., see String vs NSString, Int vs Int8, T vs Optional<T>, etc.). This applies to conversions to/from AnyHashable, too.

AnyHashable could never guarantee to preserve hash values, because it always ensured that Objective-C and Swift counterparts of the same bridged value will compare the same when converted to AnyHashable -- even if the counterparts used different hashing algorithms. (It just didn't do a particularly good job of this before #17396; and some monsters may still be lurking in its murky depths.)

SE-0131, SE-0143, and SE-0170 certainly complicated things, but they just added to a preexisting pile of sadness. For example, we always needed String and NSString to have a consistent definition of equality+hashing under AnyHashable, even though they don't even agree on what equality means:

let s1 = "café"
let s2 = "cafe\u{301}"
let n1 = "café" as NSString
let n2 = "cafe\u{301}" as NSString

// String does Unicode normalization before its comparisons/hashing:
print(s1 == s2) // ⟹ true
print(s1.hashValue == s2.hashValue) // ⟹ true
// NSString doesn't:
print(n1 == n2) // ⟹ false
print(n1.hashValue == n2.hashValue) // ⟹ false

// However, we want all four of these strings to compare equal (and therefore,
// hash the same) under AnyHashable:
let set: Set<AnyHashable> = [s1, s2, n1, n2]
print(set.count) // ⟹ 1

print((s1 as AnyHashable) == (s2 as AnyHashable)) // ⟹ true
print((s2 as AnyHashable) == (n1 as AnyHashable)) // ⟹ true
print((n1 as AnyHashable) == (n2 as AnyHashable)) // ⟹ true

// Therefore, AnyHashable must not be using NSString's definitions 
// for equality and hashing.

There may be other cases, including some we may not be handling correctly yet. (For example, I know downcasting from AnyHashable does not always work the way it should.)

Note that AnyHashable's hash encodings aren't considered part of its ABI -- we may need to change them between any two releases. (Besides fixing bugs like [SR-9047] Optional needs to have a custom AnyHashable representation · Issue #51550 · apple/swift · GitHub, we may want to start using the value's underlying (canonical) type as a hash discriminator in the future, in which case AnyHashable would stop preserving hash values altogether.)

5 Likes