== Overloading for a custom class is not always called

I have a custom operator defined globally, for example:

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 user statement does not shoot. However, if I add this code to 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 true - I assume my user statement is running in this case.

I do not have other custom operators that could cause a conflict in the case without a playing field, and the Item class is the same in both cases, so why is my custom operator not called outside the playing area?

The Item class inherits from the Object class provided by Realm , which ultimately inherits from NSObject . I also noticed that if I define non-stationary inputs for overload, when the inputs are options, they do not start.

+5
swift
Feb 16 '17 at 20:13
source share
1 answer

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

1. Overload resolution favors supertypes for additional advertising

You have specified your overload == for Item! parameters Item! , not Item . Thus, the type checker weighs more in favor of statically sending overload NSObject for == , since it seems that the type checker supports a subclass for conversions over the superclass compared to additional advertising (I could not find a source to confirm this, although )

Usually you do not need to define your own overload to handle options. By matching this type with Equatable , you automatically get an overload == , which handles equality checking between optional instances of this type.

A simpler example demonstrating the advantage of superclass overloading over additional subclass overloading would be:

 // custom operator just for testing. infix operator <===> class Foo {} class Bar : Foo {} func <===>(lhs: Foo, rhs: Foo) { print("Foo overload") } func <===>(lhs: Bar?, rhs: Bar?) { print("Bar overload") } let b = Bar() b <===> b // Foo overload 

Should Overload Bar? changed to Bar - overload will be called instead.

Therefore, you must change your overload to use the Item parameters instead. Now you can use this overload to compare two instances of Item for equality. However, this will not completely solve your problem due to the following problem.

2. Subclasses cannot directly re-implement protocol requirements

Item does not directly Equatable . Instead, it inherits from NSObject , which already matches Equatable . Its implementation == just goes to isEqual(_:) - which by default compares memory addresses (i.e. checks if these two instances are exactly the same instance).

This means that if you overload == for Item , this overload cannot be dynamically dispatched. This is because Item does not get its own protocol witness table for Equatable - instead, it relies on NSObject PWT, which sends == to its overload, simply by calling isEqual(_:) .

(Protocol witness protocols are a mechanism used to provide dynamic sending with protocols - see this WWDC talk to them for more information.)

Thus, this will prevent your overload from being called in general contexts, including the aforementioned free == overload for options - an explanation of why it does not work when trying to compare Item? instances Item? .

This behavior can be seen in the following example:

 class Foo : Equatable {} class Bar : Foo {} func ==(lhs: Foo, rhs: Foo) -> Bool { // gets added to Foo protocol witness table. print("Foo overload") // for conformance to Equatable. return true } func ==(lhs: Bar, rhs: Bar) -> Bool { // Bar doesn't have a PWT for conformance to print("Foo 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 overload 

So, even if you have to change your overload in such a way that it accepts the entry Item , if == has ever been called from the general context in an instance of Item , your overload will not be called. NSObject overload will be.

This behavior is somewhat unobvious and was filed as an error - SR-1729 . The rationale for this, as Jordan Rose explained, is as follows:

[...] A subclass cannot provide new members to meet requirements. 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, since the module in which this subclass is located needs to be recompiled so that it can satisfy the requirements, which is likely to lead to problematic behavior.

However, it is worth noting that this restriction is indeed problematic with operator requirements, since other protocol requirements can usually be overridden by subclasses. In such cases, overriding implementations are added to the vtable subclass, which allows dynamic dispatch as expected. However, this is currently not possible for operators without using a helper method (for example, isEqual(_:) ).

Decision

So the solution is to override NSObject isEqual(_:) and the hash property instead of overload == (see this Q & A for how to do this). This ensures that your equality implementation will always be called regardless of context - since your override will be added to the vtable class, which will allow you to dynamically send messages.

The rationale for overriding hash as well as isEqual(_:) is that you need to keep the promise that if two objects are compared equal, their hashes must be the same. All sorts of oddities can occur differently if Item ever hashed.

Obviously, a solution for derived classes other than NSObject would have to define your own isEqual(_:) method, and subclasses override it (and then just have an overload chain == ).

+6
Feb 16 '17 at 22:47
source share
β€” -



All Articles