【问题标题】:How to force usage of protocol default extension "override"?如何强制使用协议默认扩展“覆盖”?
【发布时间】:2021-12-31 00:35:24
【问题描述】:

我有一个带有 PAT 的协议,它在扩展中提供默认实现。 然后,符合该协议的类提供“覆盖”。 不调用此覆盖,编译器“首选”默认值。

//---- Definition of the protocol with a default implentation, because I am forced to.

protocol Bag: AnyObject {
    associatedtype BagObject
    
    func add(_ e: BagObject)
}

extension Bag where BagObject: Equatable {
    //    I here give a default implementation, because I can't do differently, as this
    //    is an extension, and it is an extension because it only applies to assoicated
    //    types that are Equatables
    func contains(_ e: BagObject) -> Bool { 
        print("Default implementation is called")
        return false 
    }
}

///---- Definition of a class that gives a concrete implementation of the protocol

class BagConcreteImplementation<BagObject>: Bag {
    var bag = Array<BagObject>()
    func add(_ e: BagObject) { bag.append(e) }
}

extension BagConcreteImplementation where BagObject: Equatable {
    //    I here give a concrete implementation when the generic type is Equatable
    func contains(_ e: BagObject) -> Bool { 
        print("Concrete implementation is called")
        return bag.contains(e) 
    }
}



///---- This is a class that encapsulate a bag, in real life, this class is adding some filtering to objects that can be added to the bag

class AClassThatHaveABag<BagType: Bag> {
    typealias BagObject = BagType.BagObject
    
    let myBag: BagType
    init(bag: BagType) { myBag = bag }
}

extension AClassThatHaveABag where BagType.BagObject: Equatable {
    func contains(_ e: BagObject) {
        //    The problem here is that the compiler only knows that myBag is a Bag
        // of Int and therefore calls the default implementation
        //    It does not call the concrete implementation of the concrete class that WILL be provided
        myBag.contains(e)
    }
}



let aBagOfInt    = BagConcreteImplementation<Int>()
let objectThatContainsABagOfInt = AClassThatHaveABag(bag: aBagOfInt)

aBagOfInt.contains(0)
objectThatContainsABagOfInt.contains(0)

// Prints
// Concrete implementation is called
// Default implementation is called

我们可以清楚地看到直接调用是调用具体实现,而封装调用是调用默认实现。

如何确保始终调用具体实现,即使是通过封装?

【问题讨论】:

  • 你能告诉我们BagConcreteImplementation.contains函数的调用地址吗
  • @EmilioPelaez 不确定你的问题。在aBagOfInt.contains(0) 调用BagConcreteImplementation.contains 并打印Concrete implementation is called
  • 我的错,没注意到滚动条????????‍♂️

标签: swift protocols


【解决方案1】:

我尝试了一些方法,但不知何故这有效:

在协议中声明contains,而不是在扩展中,像这样:

func contains<T>(_ e: T) -> Bool where T: Equatable, T == BagObject

如果您在类中执行 T == BagObject 之类的操作,则会显示编译器错误,指出这会使 T 变得多余,但显然这在协议中是可以的。

然后,像这样实现默认实现:

extension Bag {
    func contains<T>(_ e: T) -> Bool where T: Equatable, T == BagObject {
        print("Default implementation is called")
        return false
    }
}

然后,您可以直接在类中实现具体实现,作为非泛型方法

class BagConcreteImplementation<BagObject> : Bag {
    func contains(_ e: BagObject) -> Bool where BagObject : Equatable {
        print("Concrete implementation is called")
        return bag.contains(e)
    }
    
    var bag = Array<BagObject>()
    func add(_ e: BagObject) { bag.append(e) }
}

在不改变调用站点的情况下,代码会打印出来:

Concrete implementation is called
Concrete implementation is called

【讨论】:

  • 如果没有在协议中声明函数,编译器无法连接这两个实现(知道一个是“覆盖”另一个),因为AClassThatHaveABag 只知道@987654329 @ 正在实现 Bag 协议。所以,这绝对不是编译器问题。
  • @gcharita 我知道。对我来说奇怪的是,某种非泛型方法正在充当泛型协议要求的见证者。这总是可能的吗?
  • @Sweeper 只有当我使用默认实现删除扩展时,我才会得到正确的结果。如果我保留它,我仍然会调用默认实现。但这已经是一种前进的方式,因为现在有一种方法可以删除默认值,但事实并非如此。如果没有其他内容,我会接受答案。
  • @AirXygène 嗯,这很奇怪。你在使用 Swift 5.5 吗?
  • 你比我快。我的错,我错误地复制了您提供的默认实现。一切都好。谢谢!
【解决方案2】:

为了完整起见,我在这里发布另一个解决方案,它避免使用T == BagObject,我发现(我的拙见)不是那么干净。

这个想法是使用Bag 的专用版本,称为BagOfEquatables,并扩展类

protocol Bag: AnyObject {
    associatedtype BagObject
    func add(_ e: BagObject)
}

// Specialized version of Bag for Equatables
protocol BagOfEquatables: Bag where BagObject: Equatable{
    func contains(_ e: BagObject) -> Bool
}

extension BagOfEquatables {
    func contains(_ e: BagObject) -> Bool {
        print("Default implementation is called")
        return false
    }
}

///---- Definition of a class that gives a concrete implementation of the protocol

class BagConcreteImplementation<BagObject>: Bag {
    var bag = Array<BagObject>()
    func add(_ e: BagObject) { bag.append(e) }
}

extension BagConcreteImplementation: BagOfEquatables where BagObject: Equatable {
    func contains(_ e: BagObject) -> Bool  {
        print("Concrete implementation is called")
        return bag.contains(e)
    }
}

///---- This is a class that encapsulate a bag

class AClassThatHaveABag<BagType: Bag> {
    typealias BagObject = BagType.BagObject
    let myBag: BagType
    init(bag: BagType) { myBag = bag }
}

extension AClassThatHaveABag where  BagType: BagOfEquatables {
    func contains(_ e: BagObject) -> Bool  {
        myBag.contains(e)
    }
}



let aBagOfInt    = BagConcreteImplementation<Int>()
let objectThatContainsABagOfInt = AClassThatHaveABag(bag: aBagOfInt)

aBagOfInt.contains(0)
objectThatContainsABagOfInt.contains(0)


// Prints
// Concrete implementation is called
// Concrete implementation is called

【讨论】:

  • 在这种方法中,BagOfEquatables 不应该有默认实现。您希望编译器告诉您符合 BagOfEquatables 的类型是否无法实现 contains(_:),而不是运行时消息。
  • 好点! (也感谢您对其他答案的澄清)。
【解决方案3】:

你说:

不调用此覆盖,编译器“首选”默认值。

问题在于您在扩展中提供了该方法的默认实现,但从未将其声明为协议本身的一部分。结果,编译器将使用“静态调度”。

如果在协议中包含方法,编译器会使用“动态调度”来解析调用哪个方法。

有关静态与动态调度的讨论,请参阅 WWDC 2016 Understanding Swift Performance


例如:

protocol Foo {
    // without this declaration, this will use static dispatch;
    // uncomment the following line for dynamic dispatch

    // func foo()
}

extension Foo {
    func foo() {
        print("default")
    }
}

struct Bar: Foo {
    func foo() {
        print("bar implementation")
    }
}

let bar: Foo = Bar()
bar.foo()               // “default” with static dispatch; ”bar implementation“ with dynamic dispatch

【讨论】:

  • 我同意你的说法,但是只有当BagObjectEquatable 时,contains 方法才需要成为协议的一部分,因此它不能成为原始协议的一部分,并且我认为没有提供默认实现的扩展方法。看看接受的答案和我的其他答案,看看他们是如何解决问题的。感谢您花费了一些时间:-)。
  • 你误解了我的意思。 Sweeper 的回答提供了一种特殊的解决方案,即除了默认实现之外,还将contains 声明添加到协议定义中。在那个答案中,他描述了你应该做的什么,而不是为什么。我只是在澄清(并回答名义上的问题)来解释为什么默认实现被调用以及如何确保调用具体实现的再现(静态与动态调度问题)。你现在问为什么你需要那个默认实现,但这不是你最初的问题。
猜你喜欢
  • 2023-03-19
  • 1970-01-01
  • 2015-10-04
  • 1970-01-01
  • 1970-01-01
  • 2017-06-28
  • 1970-01-01
  • 2016-12-17
  • 1970-01-01
相关资源
最近更新 更多