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

sho*_*hoe 9 swift

我有一个自定义运算符全局定义如下:

func ==(lhs: Item!, rhs: Item!)->Bool {
    return lhs?.dateCreated == rhs?.dateCreated
}
Run Code Online (Sandbox Code Playgroud)

如果我执行此代码:

let i1 = Item()
let i2 = Item()
let date = Date()
i1.dateCreated = date
i2.dateCreated = date
let areEqual = i1 == i2
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

areEqual 是的 - 我假设我的自定义运算符在这种情况下被触发了.

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

Item类从继承Object的领域提供一流的,它最终从继承NSObject.我还注意到,如果我为重载定义非可选输入,当输入是选项时,它不会被触发.

Ham*_*ish 9

你在这里尝试做什么有两个主要问题.

1.重载决议有利于超类型而非可选促销

您已声明参数的==重载Item!而不是Item参数.通过这样做,类型检查器更加有利于静态调度到NSObject超载==,因为看起来类型检查器有利于子类通过可选促销超类转换(我无法找到确认这一点的来源)虽然).

通常,您不需要定义自己的重载来处理选项.通过使给定类型符合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
Run Code Online (Sandbox Code Playgroud)

如果Bar?重载更改为Bar- 将调用该重载.

因此,您应该更改过载以取代Item参数.您现在可以使用该重载来比较两个Item实例的相等性.但是,由于下一个问题,这不能完全解决您的问题.

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

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

这意味着,如果你超载==Item,超载那是能够被动态分派到.这是因为Item没有得到它自己的协议见证表的一致性Equatable- 它依赖于NSObjectPWT,它将调度到它的 ==重载,只是调用isEqual(_:).

(协议见证表是用于通过协议实现动态调度的机制 - 有关更多信息,请参阅此WWDC上的谈话.)

因此,这将防止在泛型上下文中调用重载,包括前面提到==的可选项的自由重载 - 解释为什么当您尝试比较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
Run Code Online (Sandbox Code Playgroud)

所以,即使你改变你的过载,例如,它需要一个Item输入,如果==是以往任何时候都从一个通用的上下文中被调用Item例如,你的过载不会被调用.NSObject超载会.

这种行为有点不明显,并已作为错误提交 - SR-1729.正如Jordan Rose所解释的那样,其背后的原因是:

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

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

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

解决方案

因此,解决的办法是重写 NSObjectisEqual(_:)方法和hash属性,而不是超载==(见本Q&A为如何去了解这一点).这将确保始终调用您的相等实现,而不管上下文如何 - 因为您的覆盖将被添加到类'vtable,允许动态分派.

背后压倒一切的理由hash,以及isEqual(_:)是,你需要维护的承诺,如果两个比较对象相等,则其哈希值必须是相同的.如果Item是一个哈希,那么各种各样的怪异都会发生.

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