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

时间:2017-02-16 20:13:56

标签: swift

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

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类继承自Object class provided by Realm,最终继承自NSObject。我还注意到,如果我为重载定义非可选输入,当输入是选项时,它不会被触发。

1 个答案:

答案 0 :(得分:8)

您在这里尝试做的事情有两个主要问题。

1。重载决议有利于超级选项而不是可选促销

您已为==参数而不是Item!参数声明了Item重载。通过这样做,类型检查器更有利于静态调度NSObject的{​​{1}}重载,因为看起来类型检查器倾向于子类通过可选促销超类转换(我没有'但是能够找到确认这一点的来源。

通常,您不需要定义自己的重载来处理选项。通过将给定类型与==相符,您将自动获得an == overload来处理该类型的可选实例之间的相等性检查。

一个更简单的例子,演示了对可选子类重载的超类重载的偏好:

Equatable

如果// 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实例的相等性。但是,由于下一个问题,这不会完全解决您的问题。

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

Item直接符合Item。相反,它继承自Equatable,已经符合NSObject。它的Equatable实现只转发到isEqual(_:) - 它默认比较内存地址(即检查两个实例是否是完全相同的实例)。

这意味着,如果您为==重载==,那么 可以动态调度。这是因为Item没有获得自己的协议见证表来表示与Item的一致性 - 而是依赖于Equatable的PWT,它将发送到 NSObject重载,只需调用==

(协议见证表是用于通过协议实现动态调度的机制 - 有关详细信息,请参阅this WWDC talk。)

因此,这将阻止在通用上下文中调用重载,包括上述选项的免费isEqual(_:)重载 - 解释为什么当您尝试比较{{}时它不起作用1}}实例。

在以下示例中可以看到此行为:

==

所以,即使你 要更改你的重载,它需要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 从{{1}的通用上下文调用实例,你的重载不会被调用。 Item超负荷会。

这种行为有点不明显,并已作为错误提交 - SR-1729。正如乔丹·罗斯所解释的那样,背后的原因是:

  

[...]子类无法提供满足一致性的新成员。这很重要,因为协议可以添加到一个模块中的基类和另一个模块中创建的子类。

这是有道理的,因为子类所在的模块必须重新编译才能使其满足一致性 - 这可能会导致有问题的行为。

值得注意的是,这种限制只对操作员的要求有问题,因为其他协议要求通常可以由子类重写。在这种情况下,覆盖实现被添加到子类'vtable,允许动态分派按预期发生。但是,如果不使用辅助方法(例如==),操作员目前无法实现此目的。

解决方案

因此,解决方案是覆盖 Item的{​​{3}}方法和NSObject属性,而不是重载isEqual(_:)(请参阅isEqual(_:)如何解决这个问题)。这将确保始终调用您的相等实现,而不管上下文如何 - 因为您的覆盖将被添加到类'vtable中,允许动态调度。

覆盖NSObject以及hash背后的原因是你需要保持这样的承诺:如果两个对象比较相等,它们的哈希必须是相同的。如果==被散列,则会发生各种奇怪的现象。

显然,非hash派生类的解决方案是定义自己的 isEqual(_:)方法,并让子类覆盖它(然后只有{{} 1}}重载链到它)。