我有一个全局定义的自定义运算符,如下所示:
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
。我还注意到,如果我为重载定义非可选输入,当输入是选项时,它不会被触发。
答案 0 :(得分:8)
您在这里尝试做的事情有两个主要问题。
您已为==
参数而不是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
实例的相等性。但是,由于下一个问题,这不会完全解决您的问题。
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}}重载链到它)。