从常规方法调用协议默认实现

92

我想知道是否有可能实现这样的事情。
我有一个像这样的Playground:

protocol Foo {
    func testPrint()
}

extension Foo {
    func testPrint() {
        print("Protocol extension call")
    }
}

struct Bar: Foo {
    func testPrint() {
        // Calling self or super go call default implementation
        self.testPrint()
        print("Call from struct")
    }
}


let sth = Bar()
sth.testPrint()

我可以在extension中提供默认实现,但是如果Bar需要默认实现中的所有内容以及其他附加内容怎么办?
这有些类似于在class中调用super.方法来满足实现每个属性等要求,但我看不到在structs中达到相同效果的可能性。


我会使用Foo.testPrint(self)() - 问题在于它由于分段错误而失败(在7.0 GM和7.1 beta上进行了测试)。 - Antonio
1
那是一个奇怪的结构,你提供的。 - cojoj
4
每个实例方法都是一个静态的柯里化方法,以其实例作为第一个参数。 - Antonio
然而,我尝试删除扩展名,但它仍然抛出相同的分段错误。可能这不应该与协议一起使用。 - Antonio
唉,真可惜我必须在代码中重复自己,而这个问题可以轻松通过使用默认实现来解决... - cojoj
这里所选择和投票最高的答案,正如它上面得到最多赞同的评论指出的那样,将极大地改变了语义。除非你确切地了解正在发生的事情并且这真的是你的意图,否则它几乎肯定会导致隐匿的、意想不到的、潜在的恶性错误。这里的另一个答案是一个很好的解决方案。它提供了一个默认的协议函数实现,并允许非默认实现来调用它。也许楼主可以考虑更改所选择的答案? - Benjohn
6个回答

107

我不知道您是否仍在寻找对此的答案,但要做到这一点的方法是从协议定义中删除函数,将您的对象转换为 Foo,然后在其上调用该方法:

protocol Foo { 
    // func testPrint() <- comment this out or remove it
}

extension Foo {
    func testPrint() {
        print("Protocol extension call")
    }
}

struct Bar: Foo {
    func testPrint() {
        print("Call from struct")
        (self as Foo).testPrint() // <- cast to Foo and you'll get the  default
                                  //    function defined in the extension
    }
}

Bar().testPrint()

// Output:    "Call from struct"
//            "Protocol extension call"

由于某些原因,只有在函数不作为协议的一部分声明,而是在协议的扩展中定义时才能工作。随它去吧,但确实可以工作。


6
在我看来,这似乎是一个bug,通过对同一实例进行强制转换不应该得到不同的方法实现。 - MANIAK_dobrii
2
@RomanSausarnes,我不确定你为什么不同意。你可以从子类的内部访问超类的实现-这是可以的。如果你将子类实例向下转换为超类并调用重写的方法,你仍然会得到子类的实现。但是,在Bar实现之外,你可以调用(self as Foo).testPrint(),对吗?所以你可以从子类实现的外部访问超类(在这种情况下是默认协议扩展imp)的实现,这就是我发现错误的地方。在这种情况下,Bar展示了Foo的行为,这是错误的。 - MANIAK_dobrii
22
如果在协议中省略该声明,那么调用符合该协议的类型上的函数时将会得到 静态 分发 - 这就是您可以转换并从扩展中获取实现的原因,这实际上极大地改变了协议+扩展的语义。如果将声明添加到协议中,则对该函数的调用将会是 动态 分派。 - Thorsten Karrer
2
这只适用于 Foo 协议没有继承任何其他协议的情况。 - iyuna
9
这难道不会导致无限循环吗? - stan liu
显示剩余11条评论

10

你可以创建一个符合协议的嵌套类型,实例化它,并在其中调用方法(即使您无法访问类型数据,因为协议扩展内部的实现也无法引用它)。但这不是我会称之为优雅的解决方案。

struct Bar: Foo {
    func testPrint() {
        // Calling default implementation
        struct Dummy : Foo {}
        let dummy = Dummy()
        dummy.testPrint()
        print("Call from struct")
    }
}

1
现在看起来这似乎是唯一的可能性(由苹果确认)...我将为此提交一个功能雷达,因为它可能会有用。 - cojoj

8
你对这种修复方法有什么看法?
protocol Foo {
    func testPrint()
}

extension Foo {
    func testPrint() {
        defaultTestPrint()
    }

    func defaultTestPrint() {
        print("Protocol extension call")
    }
}

struct Bar: Foo {
    func testPrint() {
        // Calling self or super go call default implementation
        defaultTestPrint()
        print("Call from struct")
    }
}


let sth = Bar()
sth.testPrint()

这是一个答案吗? - Anh Pham
@AnhPham,就是另一种执行默认功能的方法。 - Amin Madani
聪明而简单的解决方案 - Stephan Januar
最具表现力和灵活性的答案。 - DawnSong
很遗憾这个(或类似的)回答没有被选为最佳回答并获得最高投票数。几乎可以肯定,这是几乎每个人都需要的答案。正如评论中指出的那样,排名靠前的回答彻底改变了语义以实现静态派发;这很可能不是人们想要的,而且很可能会导致隐形错误,甚至更糟糕的是,笨拙的解决方法。 - Benjohn
这是一个好答案 @AminMadani 它说明了 S.O. 中有问题的信息信号,你非常好的答案获得的票数比语义可疑的被选答案少了10倍以上。作者的声誉比也非常大幅度地提升了错误观点。 - Benjohn

4
感谢您的帖子!如果将函数定义放在协议中,那么当对象被强制转换为该协议时,它只会看到对象版本的函数,并且由于你在其内部调用它自己,因此你得到的是苹果的新地址... 我尝试了这样一个版本:
import UIKit
protocol MyProc
{
}

protocol MyFuncProc
{
    func myFunc()
}

extension MyProc
{
    func myFunc()
    {
        print("Extension Version")
    }
}

struct MyStruct: MyProc, MyFuncProc
{
    func myFunc()
    {
        print("Structure Version")
        (self as MyProc).myFunc()
    }
}

(MyStruct() as MyFuncProc).myFunc()

这将输出以下内容:
Structure Version
Extension Version

3
如果你的协议具有associatedTypeSelf要求,则此强制转换将无法正常工作。为了解决这个问题,创建一个"shadow"默认实现,它既可以供常规默认实现调用,也可以供符合类型调用。
protocol Foo { 
    associatedType Bar
}

extension Foo {
    func testPrint() {
        defaultTestPrint()
    }
}

fileprivate extension Foo { // keep this as private as possible
    func defaultTestPrint() {
        // default implementation
    }
}

struct Bar: Foo {
    func testPrint() {
        // specialized implementation
        defaultTestPrint()
    }
}

再同意你不过了。defaultXX()比其他答案更具表现力和可读性。 - DawnSong
我认为Amin Madani改进了你的答案。 - DawnSong

3

我已经想出了一个解决方案。

问题

当您在扩展中具有默认实现时,如果您在另一个类/结构体中实现协议,则如果您实现该方法,则会失去此默认实现。这是按设计方式进行的,这就是协议的工作方式。

解决方案

  • 创建协议的默认实现,并将其设置为协议的属性。
  • 然后,在类中实现此协议时,用getter提供默认实现。
  • 需要时调用默认实现。

示例


protocol Foo {
    var defaultImplementation: DefaultImpl? { get }
    func testPrint()
}

extension Foo {
    // Add default implementation
    var defaultImplementation: DefaultImpl? {
        get {
            return nil
        }
    }
}

struct DefaultImpl: Foo {
    func testPrint() {
        print("Foo")
    }
}


extension Foo {
    
    func testPrint() {
        defaultImplementation?.testPrint()
    }
}

struct Bar: Foo {
    
    var defaultImplementation: DefaultImpl? {
        get { return DefaultImpl() }
    }
    func testPrint() {
        if someCondition {
            defaultImplementation?.testPrint() // Prints "Foo"
        }
    }
}

struct Baz: Foo {
    func testPrint() {
        print("Baz")
    }
}


let bar = Bar()
bar.testPrint() // prints "Foo"

let baz = Baz()
baz.testPrint() // prints "Baz"


缺点

在实现此协议的结构体/类中,您将失去必需的实现错误。


1
请确保解释你的代码为什么有效。这有助于其他访问你答案的人学习。 - AlexH

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