10

I have a custom operator defined globally like so:

func ==(lhs: Item!, rhs: Item!)->Bool {
    return lhs?.dateCreated == rhs?.dateCreated
}

And if I execute this code:

let i1 = Item()
let i2 = Item()
let date = Date()
i1.dateCreated = date
i2.dateCreated = date
let areEqual = i1 == i2

areEqual is false. In this case I know for sure that my custom operator is not firing. However, if I add this code into the playground:

//same function
func ==(lhs: Item!, rhs: item!)->Bool {
    return lhs?.dateCreated == rhs?.dateCreated
}

//same code
let i1 = Item()
let i2 = Item()
let date = Date()
i1.dateCreated = date
i2.dateCreated = date
let areEqual = i1 == i2

areEqual is true -- I'm assuming my custom operator is fired in this case.

I have no other custom operators defined that would cause a conflict in the non-playground case, and the Item class is the same in both cases, so why is my custom operator not being called outside the playground?

The Item class inherits from the Object class provided by Realm, which eventually inherits from NSObject. I also noticed that if I define non-optional inputs for the overload, when the inputs are optionals it's not fired.

Hamish
  • 78,605
  • 19
  • 187
  • 280
shoe
  • 952
  • 1
  • 20
  • 44
  • `i1 == i2` calls the `==` method from `NSObject`, not yours. And that calls `isEqual` which by default compares memory addresses. – Martin R Feb 16 '17 at 20:26
  • @MartinR so i cannot create a custom operator that does this? – shoe Feb 16 '17 at 20:28
  • @shoe: For `NSObject` subclasses you have to override `func isEqual(_ object: Any?) -> Bool`. There is an example and a link to the documentation at the end of http://stackoverflow.com/a/33320737/1187415. – Martin R Feb 16 '17 at 20:29
  • @MartinR but i noticed it works perfectly fine if both the custom operator parameters aren't implicitly wrapped optionals – shoe Feb 16 '17 at 20:37
  • @shoe: `NSObject` conforms to `Equatable` and has a `func ==(lhs: NSObject, rhs: NSObject) -> Bool` function. That is called because it matches the arguments "better" than your function, it can be called without promoting the arguments to optionals. – Martin R Feb 16 '17 at 20:42
  • @MartinR so then it isn't necessary to override `func isEqual(_ object: Any?) -> Bool`? – shoe Feb 16 '17 at 20:49
  • @shoe: Not for a direct call to `==`. But if you expect that `array.contains()` and similar container operations call your operator for comparison then you have to. – Martin R Feb 16 '17 at 20:51
  • @MartinR in the answer you linked to, it says you have to override `hash` and `isEqual`. but i only overrode `isEqual` and it works perfectly fine. is it still necessary to override `hash`? – shoe Feb 16 '17 at 21:13
  • @shoe: The documentation says so. It may work for you by chance. Also hash values will only be used in sets and dictionary keys. – Martin R Feb 16 '17 at 21:15
  • @Hamish: I just wonder: Methods which are declared in a protocol are *dynamically* dispatched (https://oleb.net/blog/2016/06/kevin-ballard-swift-dispatch/). That seems not apply to operators (static functions). Otherwise even for NSObject subclasses the custom implementation of `==` should be called. – Martin R Feb 16 '17 at 21:19
  • @MartinR actually in the documentation you linked to in your answer it says the `the default implementation of the == invokes the isEqual method`, so it doesn't seem like there's any chance involved. that's just how the `==` operator works by default – shoe Feb 16 '17 at 21:22
  • @MartinR also, my globally defined custom operators were definitley being called. it's just when the parameters were implicitly wrapped optionals like above, it wouldn't catch the case where both operands were non-optionals. adding a globally defined custom operator with both non-optional parameters caught this case. – shoe Feb 16 '17 at 21:24
  • @Hamish: I could reproduce it with a custom operator and non-NSObject subclasses. – Perhaps because operators are essentially global functions? – Martin R Feb 16 '17 at 21:29
  • @shoe: There are two aspects which were perhaps mixed up in the discussion. 1) NSObject provides a `==` method which is more specific than your method taking optionals, therefore it is called. – 2) In order to implement the Equatable protocol correctly, you have to override `isEqual` and `hash`. – Martin R Feb 16 '17 at 21:31
  • @Hamish ok, but if i'm only comparing the objects using direct calls to `==` then only overriding `isEquals` will be satisfactory, correct? – shoe Feb 16 '17 at 21:33
  • @Hamish, shoe: I have reopened the question. If dozens of comments are necessary for clarification then it deserves its own answer :) Also the aspect of overriding with an operator taking optional arguments it not covered in the "duplicate". – Hamish: Perhaps you want to post an answer? – Martin R Feb 16 '17 at 21:37

1 Answers1

10

There are two main problems with what you're trying to do here.

1. Overload resolution favours supertypes over optional promotion

You've declared your == overload for Item! parameters rather than Item parameters. By doing so, the type checker is weighing more in favour of statically dispatching to NSObject's overload for ==, as it appears that the type checker favours subclass to superclass conversions over optional promotion (I haven't been able to find a source to confirm this though).

Usually, you shouldn't need to define your own overload to handle optionals. By conforming a given type to Equatable, you'll automatically get an == overload which handles equality checking between optional instances of that type.

A simpler example that demonstrates the favouring of a superclass overload over an optional subclass overload would be:

// custom operator just for testing.
infix operator <===>

class Foo {}
class Bar : Foo {}

func <===>(lhs: Foo, rhs: Foo) {
    print("Foo's overload")
}

func <===>(lhs: Bar?, rhs: Bar?) {
    print("Bar's overload")
}

let b = Bar()

b <===> b // Foo's overload

If the Bar? overload is changed to Bar – that overload will be called instead.

Therefore you should change your overload to take Item parameters instead. You'll now be able to use that overload to compare two Item instances for equality. However, this won't fully solve your problem due to the next issue.

2. Subclasses can't directly re-implement protocol requirements

Item doesn't directly conform to Equatable. Instead, it inherits from NSObject, which already conforms to Equatable. Its implementation of == just forwards onto isEqual(_:) – which by default compares memory addresses (i.e checks to see if the two instances are the exact same instance).

What this means is that if you overload == for Item, that overload is not able to be dynamically dispatched to. This is because Item doesn't get its own protocol witness table for conformance to Equatable – it instead relies on NSObject's PWT, which will dispatch to its == overload, simply invoking isEqual(_:).

(Protocol witness tables are the mechanism used in order to achieve dynamic dispatch with protocols – see this WWDC talk on them for more info.)

This will therefore prevent your overload from being called in generic contexts, including the aforementioned free == overload for optionals – explaining why it doesn't work when you attempt to compare Item? instances.

This behaviour can be seen in the following example:

class Foo : Equatable {}
class Bar : Foo {}

func ==(lhs: Foo, rhs: Foo) -> Bool { // gets added to Foo's protocol witness table.
    print("Foo's overload")           // for conformance to Equatable.
    return true
}

func ==(lhs: Bar, rhs: Bar) -> Bool { // Bar doesn't have a PWT for conformance to
    print("Bar's overload")           // Equatable (as Foo already has), so cannot 
    return true                       // dynamically dispatch to this overload.
}

func areEqual<T : Equatable>(lhs: T, rhs: T) -> Bool {
    return lhs == rhs // dynamically dispatched via the protocol witness table.
}

let b = Bar()

areEqual(lhs: b, rhs: b) // Foo's overload

So, even if you were to change your overload such that it takes an Item input, if == was ever called from a generic context on an Item instance, your overload won't get called. NSObject's overload will.

This behaviour is somewhat non-obvious, and has been filed as a bug – SR-1729. The reasoning behind it, as explained by Jordan Rose is:

[...] The subclass does not get to provide new members to satisfy the conformance. This is important because a protocol can be added to a base class in one module and a subclass created in another module.

Which makes sense, as the module in which the subclass resides would have to be recompiled in order to allow it to satisfy the conformance – which would likely result in problematic behaviour.

It's worth noting however that this limitation is only really problematic with operator requirements, as other protocol requirements can usually be overridden by subclasses. In such cases, the overriding implementations are added to the subclass' vtable, allowing for dynamic dispatch to take place as expected. However, it's currently not possible to achieve this with operators without the use of a helper method (such as isEqual(_:)).

The Solution

The solution therefore is to override NSObject's isEqual(_:) method and hash property rather than overloading == (see this Q&A for how to go about this). This will ensure that your equality implementation will always be called, regardless of the context – as your override will be added to the class' vtable, allowing for dynamic dispatch.

The reasoning behind overriding hash as well as isEqual(_:) is that you need to maintain the promise that if two objects compare equal, their hashes must be the same. All sorts of weirdness can occur otherwise, if an Item is ever hashed.

Obviously, the solution for non-NSObject derived classes would be to define your own isEqual(_:) method, and have subclasses override it (and then just have the == overload chain to it).

Hai Feng Kao
  • 5,219
  • 2
  • 27
  • 38
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • 1
    I wasn't able to reproduce your `==` overload getting called in a playground – however, I would steer well clear of playgrounds when testing Swift behaviour, they're notoriously buggy and unreliable. – Hamish Feb 16 '17 at 22:52