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.)