4

Why do I have to add != to make the comparison correct?

import UIKit

class Person: NSObject {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

extension Person {
    static func ==(lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
    static func !=(lhs: Person, rhs: Person) -> Bool {
        return !(lhs == rhs)
    }
}

let first = Person(name: "John", age: 26) 
let second = Person(name: "John", age: 26)

/**
 * return false (which is correct) when we implement != function. But,
 * it will return true if we don't implement the != function.
 */
first != second 

Update: So I got why I had to add != function to make it work. it's because the class inherit the NSObject which uses isEqual method behind the scene. But why does adding != function make it work? Any explanation here?

Edward Anthony
  • 3,354
  • 3
  • 25
  • 40
  • Please provide a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) – Alexander Aug 09 '17 at 03:05
  • @Alexander Sorry the class code is to complex to be put in here. But the answer might be simple, because I read it's just a new requirement for new version of Swift that you have to implement `!=` to make it work. – Edward Anthony Aug 09 '17 at 03:08
  • Well that assumption is just false. A default `!=` implementation is provided which calls `!(a == b)`. I'm not exactly sure what kind of answer you're looking for, if you don't even give us a way to recreate the issue – Alexander Aug 09 '17 at 03:09
  • Sure give me time, I'll find a way to make the class simpler and see if I can reproduce it using the simpler class, and then put it here. Thanks for the feedback :) – Edward Anthony Aug 09 '17 at 03:11
  • I updated the question. – Edward Anthony Aug 09 '17 at 03:31
  • 1
    While I haven't an answer, I appreciate the updated question. You have my upvote. –  Aug 09 '17 at 03:33
  • @EdwardAnthony Now that's a better question. This is odd. I suspect it has something to do with how `Equatable` interacts with `NSObject` – Alexander Aug 09 '17 at 04:24
  • 1
    Compare point #2 of https://stackoverflow.com/a/42286148/2976878. In short, subclasses cannot directly reimplement a superclass' protocol conformance. While your implementation of `==` can be *statically* dispatched to, it cannot be *dynamically* dispatched to (such as from within the standard library's `!=` operator implementation that invokes `==` and negates the result). As others have already said, you want to override `isEqual` (*and* `hash`!) – Hamish Aug 09 '17 at 08:50

3 Answers3

4

NSObject conforms to Equatable but uses its own isEqual method and in terms of isEqual both instances are not equal. NSObject calls == only if your form of != is implemented, which contains ==.

If you delete NSObject (and add Equatable) the implementation of == works as expected.

The recommended way for NSObject is to override isEqual with a custom implementation and omit == (and !=).

vadian
  • 274,689
  • 30
  • 353
  • 361
3

Sorry, this is not a direct answer to your question.

As Alexander commented, Swift Standard Library has this default implementation of !=:

Equatable.swift

  @_transparent
  public static func != (lhs: Self, rhs: Self) -> Bool {
    return !(lhs == rhs)
  }

I cannot explain this behavior well, but the == operator in the default implementation above is solved to the default == operator for NSObject, as NSObject (and also its descendants) is already Equatable and has an == operator to conform to Equatable. So, even if the explicit representation is exactly the same as your != definition, the == operators are solved to different implementations.


A general guidline to define your own equality to an NSObject-descendant class:

Make == and isEqual(_:) consistent

You may store your class's instance inside NSArray or NSDictionary (in many cases implicitly). Inside their methods, isEqual(_:) is used when equality check is needed, not the == operator.

So, just defining the == operator without giving a consistent override to isEqual(_:), such methods will generate unexpected result.

To make consistent == and isEqual(_:),

just override only isEqual(_:) and do not define == and != explicitly.

The default implementation of == for NSObject (and also !=) uses isEqual(_:).

class Person: NSObject {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    override func isEqual(_ object: Any?) -> Bool {
        if let other = object as? Person {
            return self.name == other.name && self.age == other.age
        }
        return false
    }
}

(See ONE MORE THING at the bottom.)


ADDITION

Similar behavior can be found on non-NSObject classes.

class BaseClass {
    var a: Int

    init(a: Int) {
        self.a = a
    }
}
extension BaseClass: Equatable {
    static func == (lhs: BaseClass, rhs: BaseClass) -> Bool {
        print("`==` of BaseClass")
        return lhs.a == rhs.a
    }
}
let b1 = BaseClass(a: 0)
let b2 = BaseClass(a: 0)
print(b1 != b2) //->`==` of BaseClass, false ### as expected

class DerivedClass: BaseClass {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}
extension DerivedClass {
    static func == (lhs: DerivedClass, rhs: DerivedClass) -> Bool {
        print("`==` of DerivedClass")
        return lhs.a == rhs.a && lhs.b == rhs.b
    }
}
let d1 = DerivedClass(a: 0, b: 1)
let d2 = DerivedClass(a: 0, b: 2)
print(d1 != d2) //->`==` of BaseClass, false ### `==` of DerivedClass and true expected

Seems we need extra care when overriding == for already Equatable classes.


ONE MORE THING

(Thanks for Hamish.)

You know you need to implement == and hashValue consistently when creating a type conforming to Hashable. NSObject is declared as Hashable, and its hashValue needs to be consistent with hash. So, when you override isEqual(_:) in your NSObject-descendent, you also should override hash consistent with your overridden isEqual(_:).

So, your Person class should be something like this:

class Person: NSObject {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    override func isEqual(_ object: Any?) -> Bool {
        if let other = object as? Person {
            return self.name == other.name && self.age == other.age
        }
        return false
    }

    override var hash: Int {
        //### This is just an example, but not too bad in practical use cases.
        return name.hashValue ^ age.hashValue
    }
}
OOPer
  • 47,149
  • 6
  • 107
  • 142
  • Yeah it seems there is no other way than overriding `isEqual`. Because if I use `==` for swift class and `isEqual` for `NSObject`, it's only a matter of time before I forget that it's an NSObject, write `==`, and breaking everything. By overriding `isEqual` I think it can make the code more consistent, so we can just use `==` everywhere. And btw, where do you find the implementation of `!=` method? – Edward Anthony Aug 09 '17 at 04:39
  • @EdwardAnthony, sorry I'v forgotten to include the link for it. Updated soon. – OOPer Aug 09 '17 at 04:50
  • Thank you so much. – Edward Anthony Aug 09 '17 at 05:00
  • Note if you're overriding `isEqual`, you should really also override `hash` to ensure that if two objects are determined equal, they have the same hash value. And the behaviour you're seeing is due to subclasses not being to directly reimplement a superclass' conformance to a protocol (compare point #2 of https://stackoverflow.com/a/42286148/2976878 :) ) – Hamish Aug 09 '17 at 09:10
  • @Hamish, I should have mentioned it! (Frankly, i've been almost forgotten it when I wrote my answer.) Thank you so much!. – OOPer Aug 09 '17 at 09:39
2

What you are doing is wrong. You should not implement == or !=. An NSObject subclass automatically implements == as isEqual:. You are disrupting that. You should implement isEqual: and that's all.

matt
  • 515,959
  • 87
  • 875
  • 1,141