I have created a custom type DayInterval:
public struct Day: Hashable {
public let year: Int
public let month: Int
public let day: Int
}
public struct DayInterval: Hashable {
public let first: Day
public let last: Day
}
to handle calendar ranges safely in my app (I find the standard types quite lacking, Date is not a calendar type and DateComponents are.. not helpful).
My expectation is that since this is a Hashable type with all Hashable properties, Swift will auto-synthesize == and hash(into:), both for Day and DayInterval. This all seems to work great.
Later on in the app's lifecycle, the type was evolved and conformance to Comparable was added:
extension DayInterval: Comparable {
public static func < (lhs: Self, rhs: Self) -> Bool {
lhs.last < rhs.first
}
}
extension DayInterval: Strideable {
... // delegates to Calendar APIs
}
Granted, this type is slightly ambiguous in its comparability, since we need to be able to compare intervals of different lengths with each other.
Generally, this all works well, however it was later discovered that the app has started crashing randomly.
There was very little to indicate the source of the crash, all we saw is:
Fatal error: Duplicate keys of type 'AnyHashable' 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.
No indication of what dictionary or hashable values are involved, no backtrace to indicate the code path at fault.
Since this crash was relatively random, it took us a lot of effort to trace down the cause until we finally landed on SwiftUI using a DayInterval as the tag of a bunch of Views in a ForEach. There doesn't appear to be any duplication in the actual day intervals involved; they are clean linear 7-day intervals.
However, it turns out that adding this code in prevents the crashing from reproducing:
extension DayInterval: Comparable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.first == rhs.first && lhs.last == rhs.last
}
Now, I have some serious questions:
- Why has adopting
: Comparable voided the synthesized == conformance to Hashable?
- Why has it done so entirely silently?
- How is
Comparable changing my default == if I don't specify it explicitly in the exact same way that Hashable would synthesize it?
- Why does
Comparable's documentation not indicate/warn about this?
- Why are we allowing such a dangerous situation to develop entirely hidden from developer involvement?