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

31

我遇到了一个问题,下面的代码(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

我预期"子类的methodA"文字应该在调用object1.methodB()之后打印出来。但由于某些原因,协议扩展的默认实现methodA()被调用了。然而object2.methodB()的调用效果与预期相同。

这是Swift协议方法分派中的另一个bug还是我遗漏了什么,代码正常工作?

4个回答

49

这就是协议当前如何分派方法的方式。

为了在运行时调用符合协议的实例的方法,使用协议见证表(请参阅此WWDC演讲了解更多信息)。它实际上只是列出了给定遵循类型的每个协议要求的函数实现的列表。

声明其符合协议的每种类型都有自己的协议见证表。你可能会注意到我说“声明其符合”,而不仅是“符合”。BaseClass 为符合 MyProtocol 而拥有自己的协议见证表。然而,SubClass 并没有因为符合 MyProtocol 而拥有自己的表 - 相反,它简单地依赖于 BaseClass 的表。如果你将
: MyProtocol 移到 SubClass 的定义中,它将有自己的 PWT。

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

因此,当调用扩展方法methodB()并调用methodA()时,它通过PWT(因为它是在协议类型的实例上调用;即self)动态分派该调用。 因此,当使用SubClass实例发生这种情况时,我们将通过BaseClass的PWT进行。 因此,无论SubClass是否提供其实现,我们最终都会调用methodA()的扩展实现。
现在考虑一下JustClass的PWT。它提供了methodA()的实现,因此其遵守MyProtocol的PWT将该实现作为methodA()的映射,以及methodB()的扩展实现。 因此,当通过其PWT动态分派methodA()时,我们最终进入其实现。
正如我在这个Q&A中所说的那样,子类不会获得适用于其超类遵守的协议的自己的PWT,这确实有些令人惊讶,并且已经被报告为错误。 Swift团队成员Jordan Rose在错误报告的评论中说,背后的原因是:
  

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

因此,如果这是行为方式,则已经编译的子类将缺少从其他模块后添加的超类遵守所需的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不会重新映射,而是每次重写(class SubClass : BaseClass{ func methodA() { print("subClass methodA") } }`)。 - mfaani
我说重写是因为它似乎既不是实际的符合性,也不是方法要求的_覆盖_,它只会进行_更新_。我很想知道这个术语的正确用法是什么。 - mfaani
1
在某些情况下,可能的解决方法似乎是用“协议层次结构”替换“类层次结构”(使用协议扩展提供实现)。请参见https://gist.github.com/grigorye/fa4fce6f0ca63cfb97b3c48448a98239,了解原始示例切换到协议的详细信息。 - Grigory Entin
1
在这种情况下(请参见我上面的评论),对于子类,我们将PWT的“实例化”推迟到类定义时,因为它本身就声明了一致性,而是从基协议而不是基类“继承”“基础”实现。 - Grigory Entin
1
另一个解决方法是用一个虚拟的“Defaults”类替换协议中的默认实现,该类将提供它们。这是一种相当有限的解决方案,但值得考虑。在我看来,这使整个事情更加清晰/易于理解,因为它强制执行了默认实现的基类和子类覆盖的“override”。请参见https://gist.github.com/grigorye/27e0f6e4f50a7650768ccd1761f6587a。 - Grigory Entin
显示剩余6条评论

2

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

只有声明符合协议的类才会得到协议见证表

这意味着具有该函数的子类对协议见证表的设置没有影响。

协议见证仅在协议本身、它的扩展和实现它的具体类之间建立合约。


0

我想子类方法A不是多态的,因为你不能在其上使用override关键字,因为类不知道该方法是在协议扩展中实现的,因此不允许你覆盖它。协议扩展方法可能会在运行时干扰你的实现,就像Objective C中2个完全相同的类别方法会互相干扰一样。你可以通过在模型中添加另一层并在类中实现方法来修复此行为,从而获得多态行为。缺点是你不能在这一层中留下未实现的方法,因为没有原生支持抽象类(这实际上是你试图用协议扩展实现的)。

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协议扩展覆盖


0

在你的代码中,

let object1 = SubClass()
object1.methodB()

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

因此,它将从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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接