Not expected behavior in Generic Type's Protocol Extension

protocol MyProtocol {
    func log()
}

extension MyProtocol {
    func log() {
        print("MyProtocol default implementation")
    }
}

class MyGenericClass<T> : MyProtocol {
    let t: T
    init(_ t: T) {self.t = t}
}

extension MyProtocol where Self==MyGenericClass<Int> {
    func log() {
        print("MyProtocol where MyGenericClass<Int>")
    }
}

func logByProtocol(_ p: MyProtocol) {
    p.log()
}

let myGenericClassNumber = MyGenericClass(1)
let myGenericClassString = MyGenericClass("1")

myGenericClassNumber.log()//expect "MyGenericClass<Int>"   true
myGenericClassString.log()//expect "MyProtocol default implementation"   true

logByProtocol(myGenericClassNumber)//expect "MyGenericClass<Int>", But print "MyProtocol default implementation"   wrong

if we add type-cast:

func logByProtocol(_ p: MyProtocol) {
    p.log()
    (p as? MyGenericClass<Int>)?.log()
}

it will successfully prints:

MyProtocol default implementation
MyProtocol where MyGenericClass

Methods not declared in the protocol itself, but just in extensions, are called statically. The compiler chooses the most specific known overload.

Inside logByProtocol, all that can be known is that p conforms to MyProtocol, so the compiler dispatches to the method in the simple extension. In the global scope, the compiler knows that myGenericClassNumber is of type MyGenericClass<Int>, and since that satisfies the stricter constraints, it dispatches to the constrained extension method.

To get dynamic dispatch like you seem to want, declare func log() as a protocol requirement:

protocol MyProtocol {
    func log()
}
extension MyProtocol {
    func log() { /* ... */ }
}

Then the method will be looked up at run‐time instead, and it will dispatch to whichever method the instance itself wants to use. The trade‐off is that this look‐up takes extra work during execution.

As a general rule:

  • if you want something to be customizable by conformers, declare it in the protocol itself.
  • Otherwise declare it only in an extension.

Sorry. I miss the protocol define. Please see the new version. It still won't work.

Ah yes, I see.

The principle is the same though. When MyGenericClass is choosing (at compile time) what method to use for it’s conformance (the one it will report at run time), it only has the simple one to choose from. MyGenericClass, in general, does not satisfy the more specific constraints.

To make it work, sink the dynamism down into T:

protocol MyProtocol {
    func log()
}

extension MyProtocol {
    func log() {
        print("MyProtocol default implementation")
    }
}

protocol MyGenericClassT {
    static func reportSelf() -> String
}
extension MyGenericClassT {
    static func reportSelf() -> String {
        return "\(Self.self)"
    }
}
extension Int : MyGenericClassT {}
extension String : MyGenericClassT {}
class MyGenericClass<T : MyGenericClassT> : MyProtocol {
    let t: T
    init(_ t: T) {self.t = t}
    func log() {
        print("MyProtocol where MyGenericClass<\(T.reportSelf())>")
    }
}

func logByProtocol(_ p: MyProtocol) {
    p.log()
}

let myGenericClassNumber = MyGenericClass(1)
let myGenericClassString = MyGenericClass("1")

myGenericClassNumber.log()//expect "MyGenericClass<Int>"   true
myGenericClassString.log()//expect "MyGenericClass<String>"   true

logByProtocol(myGenericClassNumber)//expect "MyGenericClass<Int>", true

struct SomethingElse : MyProtocol {}
SomethingElse.log()//expect "MyProtocol default implementation"   true

So if Swift query the real type information in runtime. p.log() can print the expected string. But due to the implementation of Swift LLVM, logByProtocol choose the static dispatch? So p.log only can choose implementation form its original protocol extension instead of the " extension MyProtocol where Self==MyGenericClass"? Would it be better if Swift Query the real type instead of MyProtocol?

Let me start over:

This log() will always be statically dispatched:

protocol MyProtocol {}
extension MyProtocol {
    func log() {}
}

This log() will always* be dynamically dispatched:

protocol MyProtocol {
    func log()
}

Consider this use of log():

func logByProtocol(_ p: MyProtocol) {
    p.log()
}

If log() is a static method, the unconstrained variant will always be called, because at compile time, all that is known is that the instance conforms to MyProtocol. Adding constrained overloads won’t make a difference. Even if the type declares such a method itself it won’t make a difference.

On the other hand, If log() is a dynamic method, the method of the real type will be called. If the type did not declare one of its own, the compiler will have given it a specialized version of the best default implementation to treat as its own. This is where you run into your surprise:

MyGenericClass<T> needs to be compiled with a single log() method which works for any T. It is only allowed to select a method which is valid for every possible T. It cannot choose the variant constrained to where Self==MyGenericClass<Int> because that is not valid for all possible T.


*The compiler is free to take shortcuts if it can prove the result would be the same.

Great answer! Thanks a lot.

By the way: C++ can achieved same idea like:

    class Protocol {
    public:
        virtual void log() {
            std::cout << "default impl" << std::endl;
        }
    };

    
    template <typename T>
    class MyGenericClass: public Protocol {
    };
    
    
    template <>
    class MyGenericClass<int>: public Protocol {
    public:
        void log() override {
            std::cout << "int impl" << std::endl;
        }
    };

     auto a = MyGenericClass<int>();
        auto b = MyGenericClass<std::string>();
        
        a.log(); //int impl
        b.log(); //default impl