Swift 中的静态Dispatch VS 动态Dispatch

C++ VS Swift

虽然我很早就了解了Swift(2014年的WWDC),但是在粗略看了一下Swift的语法后,我认为这不过是许多语言语法的大杂烩,感觉和C++没有啥区别。但是实际上,在我使用Swift的这几个月中,我发现了许多问题值得注意的地方,比如

  • 函数的返回值可以作为推断函数签名的依据。
  • Swift中的函数静态Dispatch VS 动态函数Dispatch

而第二点,也是本文要阐述的重点。

在展开本文的内容前,如果你曾经有C++的开发背景,不妨回忆下C++中RTTI机制,这也是多态发生的先决条件。简单来说,就是C++的多态函数是基于运行时的,我们可以看看下面这个例子:

class A
{
    void print(){cout << "I'm A";}
}

class B: public A
{
    void print(){cout << "I'm B";}
}

A *b = new B();
b->print();

相信大家一眼就能知道这个答案,会输出I'm B。那么,Swift中也存在class,那么对于Swift中的函数调用是否和C++一致呢?

Swift Class

首先我们先来验证下最基本的class中的行为。我们采用和C++中相同例子,定义如下:

class A
{
    func printInfo()
    {
        print("I'm A")
    }
}

class B:A
{
    override func printInfo()
    {
        print("I'm B")
    }
}

根据Swift的Type Inference 我们分别验证了如下几种调用方式:

let b1:B = B()
b1.printInfo() // I'm B

let b2:A = B()
b2.printInfo() // I'm B

let a1:A = A()
a1.printInfo() // I'm A

let a2 = A()
a2.printInfo() // I'm A

let b3 = B()
b3.printInfo() // I'm B

如果你自己的思考结果和这个一模一样,至少你理解了运行期和编译期的概念,恭喜你,你的C++是过关了。以class B举例,无论是b1, b2, b3中的哪一个,尽管其中有部分声明的类型是A,但是在实际运行时还是会走类似virtual function那套确认实际类型为B。但是,事情在Swift中真是这么简单吗?让我们接着往下看。

Protocol Extension

去年,Swift 2.0发布,随之而来,一个概念悄然兴起:面向协议的编程。而这种编程范式不可或缺的必要条件就是Protocol Extension。在Swift < 2.0 时代,Protocol的作用更类似于一种表征特征的约束。而有了Protocol Extension以后,Protocol更类似于一种插件装配的概念(写过Ruby的人相信会有体会),可以在无须编写代码的情况下,更自定义的元素添加行为能力。

哎?你上面说了这么一大段废话,和我们的文章主题有啥关系?

好,首先我们先看如下定义:

protocol Testable
{
    func dynamicInfo()
}

extension Testable
{
    func dynamicInfo()
    {
        print("I'm Testable")
    }
}

class A:Testable
{
    func dynamicInfo()
    {
        print("I'm A")
    }
}

class B:Testable
{
    func dynamicInfo() {
        print("I'm B")
    }
}

然后,我们进行如下调用:

let a1 = A()
a1.dynamicInfo() // I'm A

let b1 = B()     
b1.dynamicInfo() // I'm B

let a2:Testable = A()
a2.dynamicInfo() // I'm A

let b2:Testable = B()
b2.dynamicInfo() // I'm B

到这里,事情还是还是按照C++那套逻辑在走,如果你把class B中的dynamicInfo删除,那么对应B类型的dynamicInfo函数调用就会输出I’m Testable

好,现在问题来了,如果我们将Testable Protocol Extension添加一下东西,同时保持protocol Testable不变,如下所示:

protocol Testable
{
    func dynamicInfo()
}

extension Testable
{
    func dynamicInfo()
    {
        print("I'm Testable")
    }

    // ### 新添加的 ###
    func staticInfo()
    {
        print("I'm Testable Static")
    }
}

如果这个时候,我们进行如下代码的测试:

class A:Testable
{
    func dynamicInfo()
    {
        print("I'm A")
    }

    func staticInfo()
    {
        print("I'm A Static")
    }
}

let a1 = A()     
a1.staticInfo() // I'm A Static

let a2:A = A()
a2.staticInfo() // I'm A Static

let a3:Testable = A()
a3.staticInfo() // I'm Testable Static

看到没?最后一行的输出是不是出乎了大家的意料,竟然输出了I’m Testable Static

这是咋回事?回顾下之前我们改变的地方,发现我们在Protocol Extension中添加了一个func staticInfo(),但是却没在对应的Testable Protocol进行声明。但是这还不够,我们必须将调用staticInfo的地方的类型显式的声明成let a3:Testable

也就是说,Swift方法的静态Dispatch必须严格满足如下条件:

  • 方法在Extension中提供了实现,但是在对应的protocol中没有声明。
  • 调用方法的时候必须显示的声明成protocol的类型。

如果不好理解,我画了张图帮助大家加深印象:

还有一点需要注意的是,Swift中的静态Dispatch不以类的层级和override而转移,也就是说,如下这种定义:

class B:A
{
    override func staticInfo()
    {
        print("I'm B Static")
    }
}

当我们使用

let a4:Testable = B()
a4.staticInfo()

一样会输出I'm Testable Static