【问题标题】:Why Swift call too shallow here为什么 Swift 在这里调用太浅
【发布时间】:2021-03-20 10:07:51
【问题描述】:

有点学术,但看这段代码

// Some protocol
protocol R : Equatable
{
}

// Some class
class E : R
{
    // Default implementation
    static func == ( lhs : E, rhs : E ) -> Bool
    {
        print ( "Comparing E ===" )
        return lhs === rhs
    }
}

// Some class with a string
class F : E
{
    let f : String

    init ( f : String )
    {
        self.f = f
    }

    // Copare strings
    static func == ( lhs : F, rhs : F ) -> Bool
    {
        print ( "Comparing F ==" )
        return lhs.f == rhs.f
    }
}

// Some generic container type
class G < T : R > : R
{
    let g : T

    init ( g : T )
    {
        self.g = g
    }

    // Compare
    static func == ( lhs : G, rhs : G ) -> Bool
    {
        print ( "Comparing G ==" )
        return lhs.g == rhs.g
    }
}

let f1 = F ( f : "abc" )
let f2 = F ( f : "abc" )

print ( "f1 == f2 ? \( f1 == f2 ) (expect true)" )

let g1 = G < F > ( g : f1 )
let g2 = G < F > ( g : f2 )

print ( "g1 == g2 ? \( g1 == g2 ) (expect true)" )

这给了

Comparing F ==
f1 == f2 ? true (expect true)
Comparing G ==
Comparing E ===
g1 == g2 ? false (expect true)

代码定义了一些协议,然后是一个实现它并提供通用比较器的类E。接下来是F,它会覆盖这个比较器来比较它所包含的字符串f。最后是一些容器类G,其中包含实现协议的东西。

到目前为止一切顺利。现在让我们比较一下东西。与 F 的实例进行比较,因为使用了正确的比较器。然而,比较两个容器类是行不通的,而且看起来 Swift 调用太浅了。它调用E 中提供的通用实现,而不是F 中覆盖的更深层次的实现。如果这是 Objective-C,即使 lhsrhs 是不同的类,它也会进行更深入的调用,然后它通常会崩溃。 Swift 不允许这样做,这很容易通过创建另一个类来查看,例如包含 Int 而不是 String 但同时它似乎没有使用正确的比较器。

编辑

这使情况变得更糟。下面的变化是f1f2g1g2 被更一般地定义为(或包含)超类类型E。即使F 覆盖了比较器,它也不会在任何比较中使用。

let f1 : E = F ( f : "abc" )
let f2 : E = F ( f : "abc" )

print ( "f1 == f2 ? \( f1 == f2 ) (expect true)" )

let g1 = G < E > ( g : f1 )
let g2 = G < E > ( g : f2 )

print ( "g1 == g2 ? \( g1 == g2 ) (expect true)" )

怀疑这与运算符重载与函数重载有关......但任何光线都会受到赞赏。

编辑 2

玩弄了一点some,想如果我使用它,也许它可以让它按我想要的方式工作,但目前不允许some 用作函数参数。

【问题讨论】:

  • 看看bugs.swift.org/browse/SR-1729,好像有关系。
  • @msbit 看起来像错误,最好的解决方法是实现自己的比较器并避免重载运算符,因为这种想法会严重毒害您的代码
  • @MartinR 刚刚尝试将比较器移动到全局范围作为该帖子中的一个建议,但它保持不变
  • @skaak 很烦,是的,我同意。如果您来自其他 OOP 语言,那么解析路径会非常令人惊讶。

标签: swift


【解决方案1】:

我查看了上述精简版本的编译输出:

class E: Equatable {
  static func ==(lhs: E, rhs: E) -> Bool { return false }
}

class F: E {
  static func ==(lhs: F, rhs: F) -> Bool { return false }
}

class G <T: Equatable>: Equatable {
  let g: T

  init(_ g: T) {
    self.g = g
  }

  static func ==(lhs: G, rhs: G) -> Bool {
    return lhs.g == rhs.g
  }
}

并且缺少的链接是继承类没有协议见证。协议见证的使用详解here

如果你看一下符号表:

$ nm main | swift demangle | grep 'protocol witness'
00000001000024a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.E : Swift.Equatable in main
00000001000028e0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.G<A> : Swift.Equatable in main

您可以看到 EG 的一个,但不是 F

如果您修改代码以添加与Equatable 的一致性,那么您会毫不意外地获得额外的协议见证:

$ nm main | swift demangle | grep 'protocol witness'
00000001000024a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.E : Swift.Equatable in main
00000001000025a0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.F : Swift.Equatable in main
00000001000028d0 t protocol witness for static Swift.Equatable.== infix(A, A) -> Swift.Bool in conformance main.G<A> : Swift.Equatable in main

所以,是的,可以非常强烈地支持这是一个错误,但正如相关的 bug report 中所述,事情就是这样,所以如果没有重要的仪式和流程,不太可能改变。

【讨论】:

  • 哇,谢谢 - 感谢您的努力和洞察力。我认为这个(OP)示例是相当标准的,在典型代码中你可以很容易地做到这一点,所以它很可怕,特别是因为它与 Equatable 协议有关。运算符重载应该正确完成或根本不完成,我更喜欢后者。
  • 我阅读您的答案的方式是,它可能是与优化有关的错误?
  • @skaak 考虑到这一点,我想“迅速”的做法是直接在任何地方指定一致性,而不是依赖继承。我听说 Swift 比 OOP 更像是一种面向协议的编程范式,所以我想这很合适。
  • @skaak 本身不是优化;更多的是选择限制角色继承。对该链接错误的评论有一些上下文:bugs.swift.org/browse/…
  • 问题是相同的方法不用于“正常”覆盖的函数。我测试并(并不惊讶)发现如果您使用正常功能,它会按预期工作 - 那么您可以依赖继承而无需指定一致性。
【解决方案2】:

@msbit 很好地解释了幕后发生的事情,我会尝试从另一个角度解决这个问题。

我们看一下协议声明:

public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

一旦协议在其声明中使用Self,或具有关联类型,它就会离开动态调度世界,并进入静态调度世界。而且这种静态调度通过继承进入了动态方法解析的方式。

另一方面是== 方法是static 要求,当涉及到类时,static 成员接收静态调度,而不是动态调度。这意味着当通过对基类的引用来引用派生类时,方法调用将始终分派给基类。

您需要使用class func 才能进行动态调度,但是协议声明不允许将要求指定为class func,即使协议是类类型之一。

因此,在这种情况下,运行时行为是正确的,甚至不是预期的,因为它遵循语言设计。我看到的问题是编译器允许您在不覆盖静态方法(在这种情况下为==)的情况下重新实现,因为常规、非运算符、静态方法不允许这样做。

class Base {
    static func sayMyName() {
        print("My name is Base")
    }

    static func ==(lhs: Base, rhs: Base) -> Bool {
        true
    }

    static func >>(lhs: Base, rhs: Base) -> Bool {
        true
    }
}

class Derived: Base {
    // this not allowed by the compiler
    static func sayMyName() {
        print("My name is Derived")
    }

    // this override is allowed, though `Base` doesn't conform
    // to Equatable
    static func ==(lhs: Derived, rhs: Derived) -> Bool {
        true
    }

    // Other operator-like static overrides are also allowed
    static func >>(lhs: Derived, rhs: Derived) -> Bool {
        true
    }
}

【讨论】:

  • 谢谢这澄清了很多。我同意你的观点,这是有问题的。 Equatable 可能会在整个库中激增,如果你有比 WWDC 演示更复杂的东西,这会毒害你的代码。 @msbit 解释了为什么这是一个错误,您现在解释为什么这不是一个错误,但至少它应该是安全优先 Swift 中的编译器错误。对于 Swift 的人来说,让它成为一个错误可能更容易做到,但它也需要考虑一下在集合中比较时的影响,从长远来看,也许正确的做法是允许覆盖。
  • 这比我的回答更好地解释了原因;我的主要是症状而不是原因:)出于好奇,您是否有指向此规范相关部分的链接?
  • @msbit static/classofficial documentation 描述它的一部分,但是我找不到具有自我要求的协议静态调度的显式链接,尽管我知道信息至少在我最初发现的 Swift 论坛上。
猜你喜欢
  • 2012-09-09
  • 2017-05-19
  • 2017-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-31
  • 2012-04-23
相关资源
最近更新 更多