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

2023-12-06

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

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两种情况下的类都是相同的,那么为什么我的自定义运算符没有在操场之外被调用呢?

The Item类继承自ObjectRealm 提供的类,最终继承自NSObject。我还注意到,如果我为重载定义非可选输入,则当输入是可选时,它不会被触发。


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

1. 重载解决方案有利于超类型而不是可选的升级

你已经宣布了你的==过载Item!参数而不是Item参数。通过这样做,类型检查器更倾向于静态分派到NSObject的过载为==,因为看起来类型检查器更喜欢子类到超类的转换而不是可选的升级(尽管我无法找到来源来确认这一点)。

通常,您不需要定义自己的重载来处理选项。通过使给定类型符合Equatable,你会自动得到an ==超载它处理该类型的可选实例之间的相等性检查。

演示超类重载优于可选子类重载的一个更简单的示例是:

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

If the Bar?过载改为Bar– 将会调用该重载。

因此你应该改变你的超负荷以采取Item参数代替。您现在可以使用该重载来比较两个Item平等的实例。然而,这不会fully解决您因下一个问题而产生的问题。

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

Itemdirectly符合Equatable。相反,它继承自NSObject,已经符合Equatable。其实施==只是转发到isEqual(_:)– 默认情况下比较内存地址(即检查两个实例是否是完全一样实例)。

这意味着如果你超载== for Item,该过载是not能够动态调度到。这是因为Item没有自己的协议见证表来符合Equatable– 相反,它依赖于NSObject的 PWT,将发送至its ==重载,只需调用isEqual(_:).

(Protocol witness tables are the mechanism used in order to achieve dynamic dispatch with protocols – see this WWDC talk on them for more info.)

因此,这将防止您的重载在通用上下文中被调用,包括上述免费的==选项的重载 - 解释为什么当你尝试比较时它不起作用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("Bar'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

所以,即使你were改变你的超载,这样就需要Item输入,如果==曾经从通用上下文中调用过Item例如,您的重载不会被调用。NSObject的超载将会。

这种行为有些不明显,已被归档为错误 –SR-1729。正如乔丹·罗斯所解释的,其背后的原因是:

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

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

然而值得注意的是,这种限制只会对运营商要求产生真正的问题,因为其他协议要求通常可以是被覆盖通过子类。在这种情况下,覆盖的实现将添加到子类的 vtable 中,从而允许按预期进行动态分派。但是,目前如果不使用辅助方法(例如isEqual(_:)).

解决方案

因此解决方案是override NSObject's isEqual(_:)方法和hash属性而不是重载== (see 本次问答了解如何进行此操作)。这将确保您的相等实现将始终被调用,无论上下文如何 - 因为您的覆盖将被添加到类的 vtable 中,从而允许动态分派。

覆盖背后的原因hashisEqual(_:)是你需要维持这样的承诺:如果两个对象比较相等,则它们的哈希值must是相同的。否则,可能会发生各种奇怪的情况,如果Item曾经被散列过。

显然,对于非NSObject派生类将定义您的own isEqual(_:)方法,并让子类重写它(然后只需==过载链)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

== 自定义类的重载并不总是被调用 的相关文章

随机推荐