Search code examples
swiftprotocols

Why does a mutating method in a Swift protocol infinitely recurse unless only an extension method?


I came across the following code in SR-142 on bugs.swift.org.

If a protocol has an extension method that's mutating, a class instance can call the mutating function without any problem.

// protocol definition
protocol P { }

extension P {
    mutating func m() { }
}

// class conforming to P
class C : P {
    // redeclare m() without the mutating qualifier
    func m() {
        // call protocol's default implementation
        var p: P = self 
        p.m()
    }
}

let c = C()
c.m()

If I make a small change to add the method to the protocol declaration:

protocol P {
  mutating func m()  // This is what I added.
}

extension P { 
  mutating func m() { } 
}

class C : P { 
  func m() { 
    var p: P = self 
    p.m() 
  }
}

let c = C() 
c.m()         // This one is calling itself indefinitely; why?

Why does c.m() keep calling itself again and again?


Solution

  • With your change in the second example, by including the m in the protocol definition, that instructs Swift to employ dynamic dispatch. So when you call p.m(), it dynamically determines whether the object has overridden the default implementation of the method. In this particular example, that results in the method recursively calling itself.

    But in the first example, in the absence of the method being part of the protocol definition, Swift will employ static dispatch, and because p is of type P, it will call the m implementation in P.


    By way of example, consider where the method is not part of the protocol definition (and therefore not in the “protocol witness table”):

    protocol P {
        // func method()
    }
    
    extension P {
        func method() {
            print("Protocol default implementation")
        }
    }
    
    struct Foo: P {
        func method() {
            print(“Foo implementation")
        }
    }
    

    Because the foo is a P reference and because method is not part of the P definition, it excludes method from the protocol witness table and employs static dispatch. As a result the following will print “Protocol default implementation”:

    let foo: P = Foo()
    foo.method()              // Protocol default implementation
    

    But if you change the protocol to explicitly include this method, leaving everything else the same, method will be included in the protocol witness table:

    protocol P {
        func method()
    }
    

    Then the following will now print “Foo implementation”, because although the foo variable is of type P, it will dynamically determine whether the underlying type, Foo, has overridden that method:

    let foo: P = Foo()
    foo.method()              // Foo implementation
    

    For more information on dynamic vs static dispatch, see WWDC 2016 video Understanding Swift Performance.