Is this possible while inserting a String into Set<String>
I’m not 100% sure. Historically it might’ve been possible to get into this situation if you had a custom subclass of NSString, but I tried that here in my office today and I’m not able to trigger it any more [1].
The most common source of errors like this is folks not maintaining the Hashable invariant. I explain that in detail below. Are you sure that frame 4 is working with Set<String> rather than some custom type?
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] For custom NSString subclasses, the compiler seems to eagerly bridge the contents over to a native String.
One of the fundamental requirements of Hashable is that, if you values are equal, they must have the same hash [1]. This is what allows containers like Set and Dictionary to use hashing to speed up things up:
- They put each value in a bucket based on its hash.
- To search for a value, they hash the value to find the right bucket and then just search that bucket.
If your implementation of Hashable doesn’t meet this requirement then you’ll see a variety of weird behaviours. For example, consider this code:
import Foundation
struct Person {
var id: String
}
extension Person: Equatable {
}
extension Person: Hashable {
}
This code works as expected, relying on the compiler to synthesise the implementation of Equatable and Hashable. Now imagine that you want to ignore case when checking a person’s ID. So you add your own implementation of Equatable:
extension Person: Equatable {
static func ==(_ lhs: Person, _ rhs: Person) -> Bool {
lhs.id.caseInsensitiveCompare(rhs.id) == .orderedSame
}
}
This seems to work; the following code never traps:
let u = UUID().uuidString
let p1 = Person(id: u)
let p2 = Person(id: u.lowercased())
assert(p1 == p2)
However, it’s not correct, because it’s possible for two equal values to have different hashes. This code will likely print hash mismatch:
if p1.hashValue != p2.hashValue { print("hash mismatch") }
Note I say “likely” because in the Swift standard library hashing include some degree of randomness to prevent collision attacks.
You’ll also see this bug cause problems with Set. For example, this code will sometimes print count mismatch:
var s = Set<Person>()
s.insert(p1)
s.insert(p2)
if s.count != 1 { print("count mismatch") }
And if you run a full test in a loop, it’ll eventually trap. For example this code:
func test() {
let u = UUID().uuidString
let p1 = Person(id: u)
let p2 = Person(id: u.lowercased())
assert(p1 == p2)
if p1.hashValue != p2.hashValue { print("hash mismatch") }
var s = Set<Person>()
s.insert(p1)
s.insert(p2)
if s.count != 1 { print("count mismatch") }
}
for _ in 1...10_000 {
test()
}
printed this:
hash mismatch
count mismatch
hash mismatch
hash mismatch
hash mismatch
hash mismatch
hash mismatch
hash mismatch
count mismatch
hash mismatch
count mismatch
hash mismatch
hash mismatch
Fatal error: Duplicate elements of type 'Person' were found in a Set.
This usually means either that the type violates Hashable's requirements, or
that members of such a set were mutated after insertion.
and then trapped with this backtrace:
(lldb) bt
…
frame #0: … libswiftCore.dylib`_swift_runtime_on_report
frame #1: … libswiftCore.dylib`_swift_stdlib_reportFatalError + 176
frame #2: … libswiftCore.dylib`closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, flags: Swift.UInt32) -> Swift.Never + 140
frame #3: … libswiftCore.dylib`Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, flags: Swift.UInt32) -> Swift.Never + 140
frame #4: … libswiftCore.dylib`Swift.ELEMENT_TYPE_OF_SET_VIOLATES_HASHABLE_REQUIREMENTS(Any.Type) -> Swift.Never + 4896
frame #5: … libswiftCore.dylib`Swift._NativeSet.insertNew(_: __owned τ_0_0, at: Swift._HashTable.Bucket, isUnique: Swift.Bool) -> () + 616
frame #6: … libswiftCore.dylib`Swift.Set._Variant.insert(__owned τ_0_0) -> (inserted: Swift.Bool, memberAfterInsert: τ_0_0) + 1024
* frame #7: … test`test() at main.swift:24:7
frame #8: … test`main at main.swift:29:5
frame #9: … dyld`start + 6076
In general, the fix for this is obvious: Always update your Equatable and Hashable conformances in unison to maintain this invariant. However, to offer specific advice about your code I’d need more details about how it’s implementing Hashable.
[1] Note that the reverse is not true.