调用 Swift 协议扩展方法而不是子类中实现的方法

Posted

技术标签:

【中文标题】调用 Swift 协议扩展方法而不是子类中实现的方法【英文标题】:Swift protocol extension method is called instead of method implemented in subclass 【发布时间】:2017-11-25 22:59:13 【问题描述】:

我遇到了以下代码(Swift 3.1)中解释的问题:

protocol MyProtocol 
    func methodA()
    func methodB()


extension MyProtocol 
    func methodA() 
        print("Default methodA")
    

    func methodB() 
        methodA()
    


// Test 1
class BaseClass: MyProtocol 



class SubClass: BaseClass 
    func methodA() 
        print("SubClass methodA")
    



let object1 = SubClass()
object1.methodB()
//

// Test 2
class JustClass: MyProtocol 
    func methodA() 
        print("JustClass methodA")
    


let object2 = JustClass()
object2.methodB()
//
// Output
// Default methodA
// JustClass methodA

所以我希望 "SubClass methodA" 文本应该在object1.methodB() 调用之后打印。但是由于某种原因,调用了来自协议扩展的methodA() 的默认实现。但是object2.methodB()call 可以正常工作。

这是协议方法调度中的另一个 Swift 错误,还是我遗漏了一些东西并且代码可以正常工作?

【问题讨论】:

【参考方案1】:

一位朋友与我分享的一个非常简短的答案是:

只有声明一致性的类才能获得协议见证表

意味着具有该功能的子类对协议见证表的设置方式没有影响。

协议见证只是协议、它的扩展以及实现它的具体类之间的契约。

【讨论】:

【参考方案2】:

这就是协议当前调度方法的方式。

协议见证表(请参阅this WWDC talk 了解更多信息)用于在协议类型实例上调用时动态调度协议要求的实现。它实际上只是一个函数实现的列表,以调用给定的符合类型的协议的每个要求。

每种声明其符合协议的类型都有自己的协议见证表。你会注意到我说的是“声明它的一致性”,而不仅仅是“符合”。 BaseClass 获得自己的协议见证表以符合 MyProtocol。然而SubClass 确实没有获得自己的表以符合MyProtocol - 相反,它只是依赖BaseClass 的。如果您将: MyProtocol 移至SubClass 的定义,它将拥有自己的 PWT。

所以我们在这里只需要考虑BaseClass 的 PWT 是什么样子的。好吧,它没有提供协议要求methodA()methodB() 的实现——因此它依赖于协议扩展中的实现。这意味着符合MyProtocolBaseClass 的PWT 只包含到扩展方法的映射。

因此,当扩展 methodB() 方法被调用并调用 methodA() 时,它会通过 PWT 动态调度该调用(因为它是在协议类型的实例上调用的;即 self) .因此,当 SubClass 实例发生这种情况时,我们将通过 BaseClass 的 PWT。所以我们最终调用了methodA() 的扩展实现,而不管SubClass 提供了它的实现。

现在让我们考虑JustClass 的 PWT。它提供了methodA() 的实现,因此其PWT 与MyProtocol 的一致性具有那个 实现作为methodA() 的映射,以及methodB() 的扩展实现。因此,当 methodA() 通过其 PWT 动态分派时,我们最终会进入 实现。

正如我所说的in this Q&A,这种子类没有为它们的超类所遵循的协议获取自己的 PWT 的行为确实有点令人惊讶,并且一直是filed as a bug。正如 Swift 团队成员 Jordan Rose 在错误报告的 cmets 中所说,其背后的原因是

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

因此,如果这是这种行为,那么已经编译的子类将缺少任何来自超类一致性的 PWT,这些 PWT 是事后添加到另一个模块中的,这将是有问题的。


正如其他人已经说过的,在这种情况下,一种解决方案是让BaseClass 提供自己的methodA() 实现。此方法现在将位于 BaseClass 的 PWT 中,而不是扩展方法中。

当然,因为我们在这里处理,所以它不仅仅是BaseClass 对所列方法的实现——而是thunk然后通过类的 vtable(类实现多态性的机制)动态调度。因此,对于SubClass 实例,我们最终将调用它对methodA() 的覆盖。

【讨论】:

长话短说,是否正确地说,当您直接 符合扩展时,PWT会更新协议要求?在这种情况下是:extension MyProtocol func methodA() print("Default methodA"); func methodB() methodA(); class BaseClass: MyProtocol 。话虽如此,一旦您子类化,PWT 就不会重新映射,而是 per 每次重写(class SubClass : BaseClass func methodA() print("subClass methodA") 我说重写是因为它似乎既不是实际的一致性也不是方法要求的覆盖,它会更新 仅此而已。我很想知道它的正确术语是什么 至少在某些情况下,这看起来像是一种可能的解决方法,可以将“类层次结构”替换为“协议层次结构”(使用协议扩展中提供的实现)。有关切换到协议的原始示例,请参阅gist.github.com/grigorye/fa4fce6f0ca63cfb97b3c48448a98239。 在这种情况下(参见我上面的评论),在子类的情况下,我们将 PWT 的“实例化”推迟到类定义,因为它声明了一致性本身,同时“继承”“基础”来自基协议而不是基类的实现。 另一种解决方法是将协议中的默认实现替换为提供它们的虚拟“默认”类。它的解决方案非常有限,但可能值得考虑。恕我直言,它使整个事情更加清晰/易于理解,因为它对默认实现的基类和子类覆盖强制“覆盖”。见gist.github.com/grigorye/27e0f6e4f50a7650768ccd1761f6587a【参考方案3】:

在您的代码中,

let object1 = SubClass()
object1.methodB()

您从SubClass 的实例调用了方法B,但SubClass 没有任何名为methodB 的方法。但是它的超类BaseClass 符合MyProtocol,它有一个methodB 方法B。

因此,它将从MyProtocal 调用methodB。因此它将执行extesion MyProtocol中的methodA

要达到您的期望,您需要在BaseClass 中实现methodA 并在SubClass 中覆盖它,如以下代码

class BaseClass: MyProtocol 
    func methodA() 
        print("BaseClass methodA")
    


class SubClass: BaseClass 
    override func methodA() 
        print("SubClass methodA")
    

现在,输出将变为

//Output
//SubClass methodA
//JustClass methodA

虽然该方法可以达到您的期望,但我不确定这种代码结构是否值得推荐。

【讨论】:

【参考方案4】:

好吧,我认为子类方法 A 不是多态的,因为您不能在其上放置 override 关键字,因为该类不知道该方法是在协议的扩展中实现的,因此不允许您覆盖它。扩展方法可能会在运行时踩到您的实现,就像 2 个确切的类别方法在目标 C 中以未定义的行为相互胜过对方。您可以通过在模型中添加另一层并在类中实现方法而不是在类中实现方法来修复此行为协议扩展,从而从中获得多态行为。缺点是你不能在这一层留下未实现的方法,因为没有对抽象类的原生支持(这实际上是你试图用协议扩展做的事情)

protocol MyProtocol 
    func methodA()
    func methodB()


class MyProtocolClass: MyProtocol 
    func methodA() 
        print("Default methodA")
    

    func methodB() 
        methodA()
    


// Test 1
class BaseClass: MyProtocolClass 



class SubClass: BaseClass 
    override func methodA() 
        print("SubClass methodA")
    



let object1 = SubClass()
object1.methodB()
//

// Test 2
class JustClass: MyProtocolClass 
    override func methodA() 
        print("JustClass methodA")
    


let object2 = JustClass()
object2.methodB()
//
// Output
// SubClass methodA
// JustClass methodA

这里也有相关答案:Swift Protocol Extensions overriding

【讨论】:

以上是关于调用 Swift 协议扩展方法而不是子类中实现的方法的主要内容,如果未能解决你的问题,请参考以下文章

Swift如何兼顾协议中默认和自定义方法的调用?

如何覆盖子类的 swift 协议函数(例如 UIView 中的 UILabel)

Swift - 必须被子类覆盖的类方法

Java中实现多态的机制

使用 Swift 协议和扩展覆盖对象方法

在 Swift 中,如何测试一个对象是不是实现了一个可选的协议方法,该方法在签名上有所不同,而无需实际调用该方法?