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 View
s 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 toHashable
? - 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 thatHashable
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?