【问题标题】:Strange behavior with Swift generics and protocol extensionsSwift 泛型和协议扩展的奇怪行为
【发布时间】:2020-05-12 22:04:06
【问题描述】:

我在协议扩展和泛型的接口上看到了一些奇怪的行为。我是 Swift 新手,所以我可能会误解,但我不明白这怎么可能是正确的行为。

首先让我们定义一个协议,并使用默认函数实现对其进行扩展:

protocol Foo {
}

extension Foo {
    static func yo() {
        print("Foo.yo")    
    }
}

现在定义几个符合要求的类型:

struct A: Foo {
}

struct B: Foo {
    static func yo() {
        print("B.yo")
    }
}

A.yo()
B.yo()

正如所料,A.yo() 使用yo 的默认实现,而B.yo() 使用B 提供的显式实现:输出为

Foo.yo
B.yo

现在让我们创建一个简单的泛型类型:

struct C<T: Foo> {
    static func what() {
        T.yo()
    }
}

C<A>.what()
C<B>.what()

C&lt;A&gt;.what() 打印 Foo.yo,正如预期的那样。但是C&lt;B&gt;.what() 也会打印出Foo.yo

C&lt;B&gt; 的含义肯定只是C 的模板,用B 代替了类型参数T?然而Byo 版本没有被调用。

我错过了什么?我正在使用 Swift 5.2.2。

现在,事实证明,您可以通过在 Foo 的原始定义中声明 yo 来解决此问题。如果我们这样做:

protocol Foo {
    static func yo()
}

然后C&lt;B&gt;.what() 按我的预期工作,打印B.yo。一开始我无法理解最初的行为,但我更无法理解这将如何改变它。

在我的实际应用程序中,我无法使用此修复程序,因为我正在扩展一个预先存在的协议,其中包含一个我想专注于特定符合类型的函数。

【问题讨论】:

  • 泛型在编译时解析。它们不像类层次结构或协议上的方法调用那样动态调度。静态是他们的重点,这就是性能获胜的根源。你应该让yo成为协议的要求,然后它会被动态调度。
  • 是的,我明白了。但是肯定 C 应该在编译时解决以它的什么方法调用 B.yo ?
  • 我的评论太长了,我写了一个完整的答案。希望这会有所帮助
  • 这可能是泛型无关紧要,这是stackoverflow.com/questions/31431753/… 的情况,但我不会担心。
  • @matt 是的,看起来确实很相似,除了在这种情况下,它显然是静态与动态调度的问题:我仍然想念它是如何产生的。在我看来,这是struct C&lt;T: Foo&gt; 的语义问题,其中T 在泛型主体中被解释为Foo,而不是简单地将这个泛型限制为满足TFoo,如我会预料到的。

标签: swift


【解决方案1】:

泛型在编译时解析。它们不像类层次结构或协议上的方法调用那样动态调度。这种静态是他们的观点,这就是性能获胜的根源。

据我所知,Foo.yo()B.yo() 是完全不相关的函数。调用Foo.yo() 会静态调度调用Foo,同样,调用B.yo() 会静态调度调用B

然而,如果您将 B.self 向上转换为 Foo.Type,并在其上调用 yo(),则最终会静态调度调用 Foo

(B.self as Foo.Type).yo()

要获得动态调度(实现您所追求的那种多态性),您需要将yo 定义为协议的要求。这在B.yo()(现在是协议一致性的一部分)和Foo.yo()(对于不提供自己的类型的默认实现)之间建立了关系。

protocol Foo {
//  static func yo() // uncomment this
}

extension Foo {
    static func yo() {
        print("Foo.yo")    
    }
}

struct A: Foo {
}

struct B: Foo {
    static func yo() {
        print("B.yo")
    }
}

struct C<T: Foo> {
    static func what() {
        T.yo()
    }
}
A.yo()
B.yo()
(B.self as Foo.Type).yo()
C<A>.what()
C<B>.what()

之前的结果:

Foo.yo
B.yo
Foo.yo
Foo.yo
Foo.yo

yo 设为要求后的结果:

Foo.yo
B.yo
B.yo
Foo.yo
B.yo

【讨论】:

  • 谢谢。我觉得我懂了。当我说struct C&lt;T: Foo&gt; 时,编译器以类似于向上转换的方式处理类型变量T。不过,我仍然没有真正看到我对动态调度的期望。编译器知道 B 有一个yo 方法,可以在编译时生成相应的静态调用。
  • 也许这里的问题是我从 C++ 程序员的角度来看待泛型,泛型只是在编译时用类型变量的特定实例实例化的模板。显然 Swift 泛型的工作方式不同。
  • @BobHearn C++ 模板相当“愚蠢”,因为它们只是基于符号、运算符等名称的鸭式类型。它们的工作方式完全不同于我所知道的其他语言(Java、C#、 Ruby、Python 和 Swift),所以我不会尝试过多的期望,以免混淆。我对为什么会发生这种情况有一个粗略的理解,虽然这真的很难用语言表达,所以请耐心等待。
  • 首先,尝试添加一个新协议protocol Ancestor {},并通过将其声明更改为protocol Foo: Ancestor {} 来使Foo 需要Ancestor。当你再次尝试运行代码时,你会得到error: type 'T' has no member 'yo'
  • Swift 的泛型有两种主要的操作模式。假设我们有一个List&lt;T&gt;。第一种操作模式是编译时特化,它的工作方式类似于模板在 C++ 中的工作方式。 List&lt;T&gt; 的定义用于消除诸如List&lt;Int&gt;List&lt;String&gt; 等特化。编译程序中所需的每一组通用参数都有一个。正如您可能对 C++ 所熟悉的那样,这适用于本地代码,但是当您尝试将泛型类型作为库出售时就不起作用...
【解决方案2】:

如果没有确切情况的更多详细信息,很难针对您的确切情况提出解决方案 - 您无法提供这些吗?可以说这是预期的行为,它与编译器所做的一些优化和假设有关。

您可能想查看这篇关于 Swift 中静态与动态调度的文章:https://medium.com/@PavloShadov/https-medium-com-pavloshadov-swift-protocols-magic-of-dynamic-static-methods-dispatches-dfe0e0c85509

【讨论】:

  • 谢谢;该链接很有帮助。我能够找到针对我的特定情况的解决方法。我对这种行为感到非常震惊,我需要理解它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多