==自定义类的重载并不总是被调用

我有一个全球定义的运算符,如下所示:

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

如果我执行这个代码:

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

areEqual是错误的。 在这种情况下,我确定我的自定义操作符不会触发。 但是,如果我将这些代码添加到操场中:

 //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是真的 – 我假设我的自定义运算符在这种情况下被解雇。

我没有定义其他定制操作符会导致非操场情况下发生冲突,并且Item类在两种情况下都是相同的,那么为什么我的定制操作符不会在操场外被调用?

Item类inheritance自Realm提供的Object类 ,它最终从NSObjectinheritance。 我还注意到,如果我为超载定义了非可选input,当input是可选项时,它不会被触发。

这里有两个主要的问题要做。

1.超载parsing有利于超types而非可选促销

你已经声明你的==过载的Item! 参数而不是Item参数。 通过这样做,types检查器正在权衡更多的赞成静态调度到NSObject的重载为== ,因为它似乎types检查偏好子类到超类转换通过可选的促销(我还没有能够find一个来源以确认这一点虽然)。

通常,您不应该定义自己的重载来处理选项。 通过将给定types与Equatable ,您将自动获得一个==重载 ,该重载处理该types的可选实例之间的相等性检查。

一个简单的例子说明了超类重载对可选子类重载的支持:

 // 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 

如果Bar? 过载被更改为Bar – 重载将被调用。

所以你应该改变你的重载来取代Item参数。 您现在可以使用该重载来比较两个Item实例的相等性。 但是,由于下一个问题,这不会完全解决您的问题。

2.子类不能直接重新实现协议要求

Item直接符合Equatable 。 相反,它从已经符合Equatable NSObjectinheritance。 它的==实现只是转发到isEqual(_:) – 默认情况下比较内存地址(即检查两个实例是否是完全相同的实例)。

这意味着如果你为Item重载== ,那么这个重载就不能被dynamic的分配给。 这是因为Item没有得到自己的协议见证表来符合Equatable – 它依赖于NSObject的PWT,它将调度到它的 ==重载,简单地调用isEqual(_:)

(协议见证表是为了实现协议的dynamic调度而使用的机制 – 请参阅此WWDC在其中讨论以获取更多信息。)

因此,这将防止在generics上下文中调用超载, 包括前面提到的free == overload – 解释为什么当您尝试比较Item?时,它不起作用Item? 实例。

这个行为可以在下面的例子中看到:

 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("Foo'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 

所以,即使你要改变你的重载,使得它需要一个Iteminput,如果==从一个Item实例上的通用上下文被调用,你的重载将不会被调用。 NSObject的超负荷将会。

这种行为有些不明显,并已作为错误提交 – SR-1729 。 乔丹·罗斯(Jordan Rose)解释的理由是:

[…]子类没有得到提供新的成员来满足一致性。 这很重要,因为协议可以添加到一个模块中的基类和另一个模块中创build的子类中。

这是有道理的,因为子类所在的模块将不得不重新编译,以便能够满足一致性 – 这可能会导致问题行为。

但是值得注意的是,这个限制对于运营商的要求只是真正的问题,因为其他的协议要求通常可以被子类所覆盖 。 在这种情况下,重写的实现被添加到子类“vtable”,允许dynamic调度按预期进行。 但是,如果不使用辅助方法(比如isEqual(_:) ),目前还不可能实现这一点。

解决scheme

因此,解决scheme是重写 NSObjectisEqual(_:)方法和hash属性,而不是重载== (请参阅此Q&A了解如何去做)。 这将确保您的平等执行将始终被调用,无论上下文 – 因为您的覆盖将被添加到类的虚拟表,允许dynamic调度。

重写hashisEqual(_:)背后的原因是你需要保持这样的承诺:如果两个对象比较相等,它们的哈希必须是相同的。 如果一个Item被散列,那么会发生各种各样的怪异现象。

显然,非NSObject派生类的解决scheme将是定义您自己的 isEqual(_:)方法,并有子类覆盖它(然后只有==重载链)。