Skip to content

Instantly share code, notes, and snippets.

@hooman
Last active January 26, 2018 05:47
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hooman/cfe98f9ac55c93f15247 to your computer and use it in GitHub Desktop.
Save hooman/cfe98f9ac55c93f15247 to your computer and use it in GitHub Desktop.
Sample code on how to correctly implement `Equatable` protocol for class types.
// The correct implementation of `Equatable` (and `Comparable`) can be tricky for class
// hierarchies. To make it easier, it is better to follow a well-defined pattern. Here
// is my suggestion on how to do it:
// 1. Define a protocol for polymorphic test function for equality (or comparison):
// Note: operators are not polymorphic (not virtual in C++ terms)). The function to
// call is determined and hard-coded at compile time.
/// A protocol to ammend `Equatable` for use with `class` types.
protocol EquatableClass: class, Equatable {
/// Equality test function. Returns `true` if `other` is equal to `self`.
///
/// :param: `other` The target of equality test.
/// :returns: Returns `true` if `other` is equal to `self`.
func equals(other: Self) -> Bool
}
// 2. Implement the protocol in a very specific way. There are two cases:
// 2.1 Stateless base class:
class Base: EquatableClass {
// An identical function works correctly for *any* stateless base class:
func equals(other: Base) -> Bool {
// For a stateless object, any `other` with identical type is equal to `self`.
return other.dynamicType.self === self.dynamicType.self
}
}
// 2.2 Stateful base class:
class StatefulBase: EquatableClass {
var state = 0
func equals(other: StatefulBase) -> Bool {
// Similar to stateless, but we need to AND it with the equality of its state:
return other.dynamicType.self === self.dynamicType.self // Check identical type
&& other.state == state // AND THEN Check equality of states
}
}
// 3. Implement `Equatable` for the base class:
// To conform to `Equatable`, you have to define == operator for each class in the hierarchy, but the implementatrion will be
// identical to this, just change `Super` to the name of your (sub)class:
// 3.1 For stateless base class:
func == (lhs: Base, rhs: Base) -> Bool { return lhs.equals(rhs) }
// 3.2 For stateful base class, it is the same as any subclass of any of these two:
func == (lhs: StatefulBase, rhs: StatefulBase) -> Bool { return lhs.equals(rhs) }
// 4. Any subclass that adds to the object state should override `equals`, both of
// the above cases of base class should follow the following pattern:
class Sub: Base {
var subState = 0
// more state declarations...
override func equals(other: Base) -> Bool {
return super.equals(other) // ALWAYS call `super` first
&& subState == (other as Sub).subState // If we get here, cast will always succeed.
// && more state equality tests
}
}
// 5. As notedd above, you need to define == operator for each class in the hierarchy in an identical way:
func == (lhs: Sub, rhs: Sub) -> Bool { return lhs.equals(rhs) }
@mgrebenets
Copy link

How about using generic version of ==?

func ==<T where T: Base>(lhs: T, rhs: T) -> Bool {
    return lhs.equals(rhs)
}

This way there's no need to write separate version of == for each subclass.

I also tried an approach like this

class AA: Equatable {
    let a: Int
    init(a: Int) {
        self.a = a
    }

    func equals<T where T: AA>(other: T) -> Bool {
        return a == other.a
    }
}

func ==<T where T: AA>(lhs: T, rhs: T) -> Bool {
    return lhs.equals(rhs)
}

class BB: AA {
    let b: Int
    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }

    override func equals<T where T : BB>(other: T) -> Bool {
        return super.equals(other) && b == other.b
    }
}

This way I have to define == only once, same as before, but I also get rid of type casting like other as B (or other as! B if it's Swift 2.0). Though I must agree that in your initial version this type case is 100% safe at all times, unless someone does crazy stuff like

let a: Base
let b: Base
print((a as! Sub) == (b as! Sub))

Which is really just crazy. If you want to break the code, nothing can stop you :)

The only thing that I don't like about the version I have so far, is that there's no protocol like EquatableClass, which can enforce the equals function and which I can reuse for other class hierarchies. If only there was a way to combine both protocol and templates, but I couldn't figure it out. And then when I override the equals and change requirements for T, this is almost identical to doing other as! Sub anyway.

Anyways, thanks for sharing this useful code!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment