Thursday, May 16, 2019

Swift: invocation of extension method defined in Protocol calls the static linked method

Polymorphism is a basic feature for object oriented design. Basically, it means when calling a method on an object, no matter the variable type that represents the object (base class, derived sub class, or protocol), the actual method implementation that will be invoked will always be the last override method available in the subclass tree. This is also called dynamic binding, that is why when calling objective c object's method, we never need to cast the object to a particular type, as polymorphism will also end up calling the last override method.

However, unlike objective c, which always uses dynamic binding, Swift uses both dynamic and static binding.

In swift, if a method is defined in protocol, or a class, then all struct, class, or subclass, which implements the protocol or inherits from the class will promise they will abided by the method signature, and may also provide an override implementation. In this aspect, the invocation on methods defined in protocol, or class use dynamic binding as objective c does.

However, things are different when a method is defined in extension for a protocol or class. as extension does not belong to the contract between  base class (or protocol) and sub class, so if same method is provided in a protocol extension and a subclass, the two methods do not have any connections. So in this case, Swift uses static binding for invocation of extension method, and the actual method that will be invoked depends on the variable type at compile time, instead of the actual object type at runtime.

The behavior can be demonstrated with the below code


protocol Movable {
    func move()
}

extension Movable {
    func move() {
       print("move implemented by Movable Protocol extension")
    }
    
    func pause() {
       print("pause implemented by Movable Protocol extension")
    }
}

struct Animal: Movable {
    func move() {
        print("move implemented by Animal structure")
    }
    
    func pause(){
         print("pause implemented by Animal structure")
    }
}

class Car: Movable {
    func move() {
        print("move implemented by Car class")
    }
    
    func pause(){
         print("pause implemented by Car class")
    }
}

extension Car {
    @objc func stop() {
        print("stop implemented by Class extension")
    }
}

class BMW: Car {
    override func move() {
        print("move implemented by BMW class")
    }
    
    override func pause(){
         print("pause implemented by BMW class")
    }
    
    override func stop() {
        print("stop implemented by BMW class")
    }
}


When calling the below code
      let a : Animal = Animal()
        a.move()
        a.pause()
        
        let ap : Movable = a
        ap.move()
        ap.pause()
        
        let c : Car = Car()
        c.move()
        c.pause()
        c.stop()
        
        let cp : Movable = c
        cp.move()
        cp.pause()
        
        
        let b : BMW = BMW()
        b.move()
        b.pause()
        b.stop()
        
        let bc : Car = b
        bc.move()
        bc.pause()
        bc.stop()
        
        let bcp : Movable = bc
        bcp.move()

        bcp.pause()


the output is shown below, and even if the object instance stays the same, the actual method invoked depends on the variable type it casts. The output shows few method calling into Movable protocol's implementation instead of actual Animal, Car or BMW's implementation.


move implemented by Animal structure
pause implemented by Animal structure
move implemented by Animal structure
pause implemented by Movable Protocol extension
move implemented by Car class
pause implemented by Car class
stop implemented by Class extension
move implemented by Car class
pause implemented by Movable Protocol extension
move implemented by BMW class
pause implemented by BMW class
stop implemented by BMW class
move implemented by BMW class
pause implemented by BMW class
stop implemented by BMW class
move implemented by BMW class
pause implemented by Movable Protocol extension


Note, this is an issue only for protocol extension, but not for class extension. As swift will report a compile error if a subclass (or subclass' extension) tries to add a method which has the same signature as defined in base class' extension. In that case, the base class extension method must be defined as @objc, which effectively change it to dynamic binding as objective c does.

No comments:

Post a Comment