Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift: Overriding == in subclass results invocation of == in superclass only

I've got a class A, which conforms to Equatable protocol and implements == function. In subclass B I override == with more checks.

However, when I do comparison between two arrays of instances of B (which both have type Array<A>), == for A is invoked. Of course if I change type of both arrays to Array<B>, == for B is invoked.

I came up with the following solution:

A.swift:

internal func ==(lhs: A, rhs: A) -> Bool {
    if lhs is B && rhs is B {
        return lhs as! B == rhs as! B
    }
    return ...
}

Which looks really ugly and must be extended for every subclass of A. Is there a way to make sure that == for subclass is invoked first?

like image 823
kovpas Avatar asked Mar 01 '15 11:03

kovpas


2 Answers

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.

like image 158
Airspeed Velocity Avatar answered Nov 13 '22 21:11

Airspeed Velocity


I ran into a similar issue because I wanted to use the difference(from:) on an MKPointAnnotation subclass (which inherits from NSObject). Even if I added func == to my annotation subclass, difference(from: would still call NSObject's implementation of ==. I believe this just compares the memory location of the 2 objects which is not what I wanted. To make difference(from: work properly, I had to implement override func isEqual(_ object: Any?) -> Bool { for my annotation subclass. In the body I would make sure the object was the same type as my subclass and then do comparison there.

like image 1
Tylerc230 Avatar answered Nov 13 '22 21:11

Tylerc230