【问题标题】:== overload for custom class is not always called== 自定义类的重载并不总是被调用
【发布时间】:2017-07-06 03:22:34
【问题描述】:

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

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。我还注意到,如果我为重载定义非可选输入,当输入是可选时,它不会被触发。

【问题讨论】:

  • i1 == i2NSObject 调用== 方法,而不是你的方法。这会调用isEqual,默认情况下会比较内存地址。
  • @MartinR 所以我无法创建一个自定义运算符来执行此操作?
  • @shoe:对于NSObject 子类,您必须覆盖func isEqual(_ object: Any?) -> Boolstackoverflow.com/a/33320737/1187415 末尾有一个示例和文档链接。
  • @MartinR 但我注意到如果两个自定义运算符参数都不是隐式包装的可选参数,它工作得很好
  • @shoe: NSObject 符合Equatable 并具有func ==(lhs: NSObject, rhs: NSObject) -> Bool 功能。之所以调用它,是因为它比您的函数“更好”地匹配参数,可以在不将参数提升为可选项的情况下调用它。

标签: swift


【解决方案1】:

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

1。重载决议有利于超类型而不是可选提升

您已经为Item! 参数而不是Item 参数声明了== 重载。通过这样做,类型检查器更倾向于静态分派到NSObject== 重载,因为类型检查器似乎更喜欢子类到超类的转换而不是可选的提升(我无法找一个来源来确认这一点)。

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

一个更简单的例子表明超类重载优于可选子类重载:

// 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。相反,它继承自 NSObject,后者已经符合 Equatable。它对== 的实现只是转发到isEqual(_:)——默认情况下比较内存地址(即检查两个实例是否完全相同实例)。

这意味着,如果您为Item 重载==,则该重载无法被动态调度。这是因为Item 没有获得自己的协议见证表以符合Equatable - 它依赖于NSObject 的PWT,它将调度到 == 过载,只需调用isEqual(_:)

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

因此,这将防止您的重载在泛型上下文中被调用,包括上述免费的 == 可选重载 - 解释为什么当您尝试比较 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

因此,即使您更改您的重载以使其接受Item 输入,如果曾经从Item 实例上的通用上下文调用==,您的重载不会被调用。 NSObject的超载会。

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

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

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

值得注意的是,这种限制只对操作员要求有问题,因为其他协议要求通常可以被子类覆盖。在这种情况下,覆盖的实现被添加到子类的 vtable 中,允许动态调度按预期进行。但是,目前无法在不使用辅助方法(例如 isEqual(_:))的情况下使用运算符来实现此目的。

解决方案

因此,解决方案是覆盖 NSObjectisEqual(_:) 方法和hash 属性,而不是重载==(请参阅this Q&A 了解如何解决此问题)。这将确保无论上下文如何,始终都会调用您的相等实现——因为您的覆盖将被添加到类的 vtable 中,从而允许动态调度。

重写hashisEqual(_:) 背后的原因是,您需要保持这样的承诺,即如果两个对象比较相等,则它们的散列必须相同。否则,如果 Item 曾经被散列,则可能会发生各种奇怪的事情。

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

【讨论】:

  • 我无法重现您在操场上调用的 == 重载 - 但是,在测试 Swift 行为时,我会避开操场,它们是出了名的错误和不可靠。跨度>
猜你喜欢
  • 1970-01-01
  • 2013-05-29
  • 2014-08-23
  • 2015-12-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-15
  • 2020-11-16
相关资源
最近更新 更多