==自定义类的重载并不总是被调用
我有一个全球定义的运算符,如下所示:
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
类 ,它最终从NSObject
inheritance。 我还注意到,如果我为超载定义了非可选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
NSObject
inheritance。 它的==
实现只是转发到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
所以,即使你要改变你的重载,使得它需要一个Item
input,如果==
从一个Item
实例上的通用上下文被调用,你的重载将不会被调用。 NSObject
的超负荷将会。
这种行为有些不明显,并已作为错误提交 – SR-1729 。 乔丹·罗斯(Jordan Rose)解释的理由是:
[…]子类没有得到提供新的成员来满足一致性。 这很重要,因为协议可以添加到一个模块中的基类和另一个模块中创build的子类中。
这是有道理的,因为子类所在的模块将不得不重新编译,以便能够满足一致性 – 这可能会导致问题行为。
但是值得注意的是,这个限制对于运营商的要求只是真正的问题,因为其他的协议要求通常可以被子类所覆盖 。 在这种情况下,重写的实现被添加到子类“vtable”,允许dynamic调度按预期进行。 但是,如果不使用辅助方法(比如isEqual(_:)
),目前还不可能实现这一点。
解决scheme
因此,解决scheme是重写 NSObject
的isEqual(_:)
方法和hash
属性,而不是重载==
(请参阅此Q&A了解如何去做)。 这将确保您的平等执行将始终被调用,无论上下文 – 因为您的覆盖将被添加到类的虚拟表,允许dynamic调度。
重写hash
和isEqual(_:)
背后的原因是你需要保持这样的承诺:如果两个对象比较相等,它们的哈希必须是相同的。 如果一个Item
被散列,那么会发生各种各样的怪异现象。
显然,非NSObject
派生类的解决scheme将是定义您自己的 isEqual(_:)
方法,并有子类覆盖它(然后只有==
重载链)。