How to properly implement the Equatable protocol in a class hierarchy?
Asked Answered
B

4

17

I'm trying to implement the == operator (from Equatable) in a base class and its subclasses in Swift 3. All of the classes will only be used in Swift so I do not want to involve NSObject or the NSCopying protocol.

I started with a base class and a subclass:

class Base {
    var x : Int
}

class Subclass : Base {
    var y : String
}

Now I wanted to add Equatable and the == operator to Base. Seems simple enough. Copy the == operator signature from the documentation:

class Base : Equatable {
    var x : Int

    static func == (lhs: Base, rhs: Base) -> Bool {
        return lhs.x == rhs.x
    }
}

So far so good. Now for the subclass:

class Subclass : Base {
    static override func == (lhs: Base, rhs: Base) -> Bool {
        return true
    }
}

But this results in an error:

Operator function overrides a 'final' operator function

OK. After some research (still learning Swift 3) I learn that static can be replaced with class to indicate the type method can be overridden.

So I attempt to change static to class in Base:

class Base : Equatable {
    var x : Int

    class func == (lhs: Base, rhs: Base) -> Bool {
        return lhs.x == rhs.x
    }
}

But that results in a new error:

Operator '==' declared in non-final class 'Base' must be 'final'

Ugh. This is far more complicated than it should be.

How do I implement the Equatable protocol and the == operator properly in a base class and a subclass?

Broyles answered 7/10, 2016 at 5:8 Comment(0)
B
21

After lots of research and some trial and error I finally came up with a working solution. The first step was moving the == operator from inside the class to the global scope. This fixed the errors about static and final.

For the base class this became:

func == (lhs: Base, rhs: Base) -> Bool {
    return lhs.x == rhs.x
}

class Base : Equatable {
    var x : Int
}

And for the subclass:

func == (lhs: Subclass, rhs: Subclass) -> Bool {
    return true
}

class Subclass : Base {
    var y : String
}

Now the only part left is figuring out how to call the == operator of the base class from the == operator of the subclass. This led me to the final solution:

func == (lhs: Subclass, rhs: Subclass) -> Bool {
    if lhs.y == rhs.y {
        if lhs as Base == rhs as Base {
            return true
        }
    }

    return false
}

That first if statement results in a call to the == operator in the base class.


The final solution:

Base.swift:

func == (lhs: Base, rhs: Base) -> Bool {
    return lhs.x == rhs.x
}

class Base : Equatable {
    var x : Int
}

Subclass.swift:

func == (lhs: Subclass, rhs: Subclass) -> Bool {
    if lhs.y == rhs.y {
        if lhs as Base == rhs as Base {
            return true
        }
    }

    return false
}

class Subclass : Base {
    var y : String
}
Broyles answered 7/10, 2016 at 5:8 Comment(1)
Wow. Clever work-around, but is this REALLY what Swift makes us do?Leaden
M
5

I know it's been a while since the question is posted, but I hope my answer helps.

TLDR -- Instead of trying to override ==, you provide a custom comparing method, make == call it, and override the custom comparing method if needed.


So you said

All of the classes will only be used in Swift so I do not want to involve NSObject or the NSCopying protocol.

But if you were to subclass NSObject, how will you write your custom comparison method? You will override isEqual(Any?), right? And if you try to conform to Equatable protocol in your subclass, compiler will complain about "Redundant conformance to protocol Equatable" because NSObject already conformed to Equatable.

Now that gives us some hints about how NSObject handles this problem -- it provides a custom comparing method isEqual(Any?), call it inside ==, and its subclasses can override it if needed. You can do the same in your own base class.

Without further ado, let's do some experiments(in Swift 4). Define some classes

class Grandpa: Equatable {
    var x = 0

    static func ==(lhs: Grandpa, rhs: Grandpa) -> Bool {
        return lhs.isEqual(to: rhs)
    }

    func isEqual(to object: Any?) -> Bool {
        guard object != nil && type(of: object!) == Grandpa.self else {
            return false
        }
        let value = object as! Grandpa
        return x == value.x
    }
}

class Father: Grandpa {
    var y = 0

    override func isEqual(to object: Any?) -> Bool {
        guard object != nil && type(of: object!) == Father.self else {
            return false
        }
        let value = object as! Father
        return x == value.x && y == value.y
    }
}

class Son: Father {
    var z = 0

    override func isEqual(to object: Any?) -> Bool {
        guard object != nil && type(of: object!) == Son.self else {
            return false
        }
        let value = object as! Son
        return x == value.x && y == value.y && z == value.z
    }
}

And write some test code

let grandpa1 = Grandpa()
let grandpa2 = Grandpa()
let grandpa3: Grandpa? = nil
let grandpa4: Grandpa? = nil
let father1 = Father()
let father2 = Father()
let father3 = Father()
father3.y = 1
let son1 = Son()
let son2 = Son()
let son3 = Son()
son3.z = 1

print("grandpa1 == grandpa2: \(grandpa1 == grandpa2)")
print("grandpa1 == grandpa3: \(grandpa1 == grandpa3)")
print("grandpa3 == grandpa4: \(grandpa3 == grandpa4)")
print("grandpa1 == father1: \(grandpa1 == father1)")
print("father1 == father2: \(father1 == father2)")
print("father1 == father3: \(father1 == father3)")
print("son1 == son2: \(son1 == son2)")
print("son1 == son3: \(son1 == son3)")

Run it and you should get

grandpa1 == grandpa2: true
grandpa1 == grandpa3: false
grandpa3 == grandpa4: true
grandpa1 == father1: false
father1 == father2: true
father1 == father3: false
son1 == son2: true
son1 == son3: false
Maturity answered 9/1, 2018 at 10:3 Comment(1)
1. Your implementation of isEqual in the subclasses should be calling super.isEqual after checking the properties of just the subclass. A subclass should not do any checking of any properties of its parent classes. 2. Not really related to the question but your GrandPa, Father, Son class hierarchy is backwards. Logically, a Son is not a Father and a Father is not a GrandPa. The Son class should be root class. Father should extend Son and GrandPa should extend Father.Broyles
W
5

Following the other answers I came up with this:

class Base : Equatable {
    var x : Int
    static func == (lhs: Base, rhs: Base) -> Bool {
        return lhs.x == rhs.x
    }
}

class Subclass : Base {
    var y : String
    static func == (lhs: Subclass, rhs: Subclass) -> Bool {
        return lhs.y == rhs.y && (lhs as Base) == (rhs as Base)
    }
}
Walden answered 13/1, 2019 at 14:54 Comment(1)
Have you tested this? We tried this and our unit tests failed because the superclass's == was called.Homomorphism
P
0

Following rmaddy's answer, I came up with a guard approach for testing equality:

Base.swift


static func ==(lhs: Base, rhs: Base) -> Bool {
    // ensure class properties match
    guard lhs.x == rhs.x else {
        return false
    }

    return true

}

Subclass.swift


static func ==(lhs: Subclass, rhs: Subclass) -> Bool {
    // ensure base class properties match
    guard lhs as Base == rhs as Base else {
        return false
    }

    // ensure class properties match
    guard lhs.y == rhs.y else {
        return false
    }

    return true
}

```

Ptosis answered 24/5, 2018 at 1:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.