如果只用三个词来概括 Julia 的类型系统的话,那么就应该是动态的(dynamic)、记名的(nominative)和参数化的(parametric)。

    我们已经解释过什么叫做“动态的”。简单来说就是,变量的类型是可以被改变的。如果我们不为变量添加类型标注,那么只有到了程序运行的时候,Julia 才能知道该变量的类型是什么。

    所谓的“记名的”是指,Julia 中的每一个类型都是有名称的。并且,即使两个类型的含义和结构都是相同的,只要它们的名称不同,那么它们就是两个不同的类型。另外,类型之间的层次关系一定是有显式的声明的。例如,Int64类型的定义是这样的:

    这里应该重点关注的是Int64 <: Signed。操作符<:的含义是,其左侧的类型是其右侧类型的直接子类型。因此,Int64类型是Signed类型的直接子类型,或者说Int64类型直接继承了Signed类型。当然,两个类型之间的关系也可以是间接的。例如,Signed类型的定义如下:

    对于类型的参数化,我们也多次提到过。还记得我们在上一章定义过的那个类型的常量吗?Julia 中的参数化类型(如Ref{T})类似于其他一些编程语言(比如 Haskell、Java 等)中的泛型。不过,各种编程语言实现泛型的方式都会有所不同,最起码在实现细节上都会有自己的特点。对于 Julia 来说更是如此,别忘了它可是动态类型的编程语言。

    我们会在后面专门讲类型的参数化。你现在只需要知道,参数化类型相当于一种对数据结构的泛化定义。更具体地说,我们可以借此在不指定具体类型的情况下用代码去描绘泛化的(或者说更加通用的)数据结构和算法。

    4.1.2 一个特点

    Julia 类型系统的最大特点当属它的多重分派机制。正因为有了多重分派机制,Julia 才能够对多态提供强大的支持。

    当我们没有为变量或参数添加类型标注的时候,原则上它们可以被赋予任何类型的值。至于后续的操作是不是支持这样的值,那就需要以多重分派的结果为准了。例如,有这样一个函数sum1

    这是由于 Julia 的多重分派机制根据在操作符+两侧的值的类型,把相加的操作委派给了不同的内部代码(操作符+实际上也代表着一个函数,且针对其参数类型的不同还有着很多衍生方法)。这就自动地让我们的代码成为了多态性代码,即:对不同类型的值实现同一种操作的代码。

    即使我们为sum1函数的参数添加了类型标注,情况也是类似的。我们可以对这个函数稍加改造:

    这里的代表了实数类型,同时它也属于抽象类型。简单来说,抽象类型代表着一个类型范围。比如,我们之前讲过的Int64UInt32以及未曾碰到过的Float32Float64都在Real这个范围之内。这与数学中的概念是一样的,即:整数和浮点数都属于实数。

    因此,即便是在这样的类型约束之下,我们在前面写的那几种调用方式也依然是有效的。也即是说,sum1函数在如此的类型约束下仍然是多态的。

    你现在只需要知道,类型标注和多重分派机制都已经被内置在了 Julia 的类型系统中,并且它们都是这个系统的核心功能。它们能够帮助我们产出富有表现力且可高效运行的代码。由于它们的共同作用,我们的代码才可以在各种约束之下灵活地实现多态。