The reason the equality for A
is being invoked for an Array<A>
that contains B
is that overloading of free functions is resolved statically, not dynamically – that is, at compile time based on the type, not at runtime based on the pointed-to value.
This is not surprising given ==
is not declared inside the class and then overridden in the subclass. This might seem very limiting but honestly, defining polymorphic equality using traditional OO techniques is extremely (and deceptively) difficult. See this link and this paper for more info.
The naïve solution might be to define a dynamically dispatched function in A
, then define ==
to just call that:
class A: Equatable {
func equalTo(rhs: A) -> Bool {
// whatever equality means for two As
}
}
func ==(lhs: A, rhs: A) -> Bool {
return lhs.equalTo(rhs)
}
Then when you implement B
, you’d override equalTo
:
class B: A {
override func equalTo(rhs: A) -> Bool {
return (rhs as? B).map { b in
return // whatever it means for two Bs to be equal
} ?? false // false, assuming a B and an A can’t be Equal
}
}
You still have to do one as?
dance, because you need to determine if the right-hand argument is a B
(if equalTo
took a B
directly, it wouldn’t be a legitimate override).
There’s also still some possibly surprising behaviour hidden in here:
let x: [A] = [B()]
let y: [A] = [A()]
// this runs B’s equalTo
x == y
// this runs A’s equalTo
y == x
That is, the order of the arguments changes the behaviour. This is not good – people expect equality to be symmetric. So really you’d need some of the techniques described in the links above to solve this properly.
At which point you might feel like all this is getting a bit unnecessary. And it probably is, especially given the following comment in the documentation for Equatable
in the Swift standard library:
Equality implies substitutability. When x == y
, x
and y
are interchangeable in any code that only depends on their values.
Class instance identity as distinguished by triple-equals ===
is
notably not part of an instance's value. Exposing other non-value
aspects of Equatable
types is discouraged, and any that are
exposed should be explicitly pointed out in documentation.
Given this, you might seriously want to reconsider getting fancy with your Equatable
implementation, if the way you’re implementing equality is not in a way where you’d be happy with two values being equal being substituted with each other. One way to avoid this is to consider object identity to be the measure of equality, and implement ==
in terms of ===
, which only needs to be done once for the superclass. Alternatively, you could ask yourself, do you really need implementation inheritance? And if not, consider ditching it and using value types instead, and then using protocols and generics to capture the polymorphic behaviour you’re looking for.