Home > Back-end >  How to force usage of protocol default extension "override"?
How to force usage of protocol default extension "override"?

Time:12-31

I have a protocol with PAT that provides a default implementation in an extension. Then, a class conforming that protocol provides an "override". This override is not called, and the compiler "prefers" the default.

//---- 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

We can clearly see the direct call is call the concrete implementation, while the encapsulated call is calling the default implementation.

How to do to make sure the concrete implementation is always called, even through an encapsulation ?

CodePudding user response:

I tried a few things, and somehow this works:

Declare contains in the protocol, not in an extension, like this:

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

If you do things like T == BagObject in a class, a compiler error will show up saying that this makes T redundant, but apparently this is okay in a protocol.

Then, implement the default implementation like this:

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

Then, you can just implement the concrete implementation directly in the class, as a non-generic method.

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) }
}

Without changing the call site, the code would print:

Concrete implementation is called
Concrete implementation is called

CodePudding user response:

For sake of completeness, I post here another solution, which avoids the use of T == BagObject which I find (my humble opinion) not that clean.

The idea is to use a specialized version of Bag, called BagOfEquatables, and extend the classes

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

CodePudding user response:

You said:

This override is not called, and the compiler "prefers" the default.

The issue is that you have supplied the default implementation of the method in the extension, but never declared it as part of the protocol, itself. As a result, the compiler will use “static dispatch”.

If you include the method in the protocol, the compiler will use “dynamic dispatch” to resolve which method to call.

For a discussion of static vs. dynamic dispatch see WWDC 2016 Understanding Swift Performance.


For example:

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
  • Related