4.1 概述

严格来说,Julia 属于动态类型语言。或者说,Julia 的类型系统是动态的。但是,我们却可以为变量附加类型标注,以使它的类型固化。虽然有些传统的动态类型语言(比如 Python)也可以为变量添加类型信息,但那最多也只能算是一种注释,并不属于其类型系统的一部分。相比之下,一旦我们为 Julia 程序中的变量添加了类型标注,那么julia命令就可以在程序真正运行之前检测出并及时报告类型不兼容的赋值。

4.1.1 三个要点

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

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

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

  1. primitive type Int64 <: Signed 64 end

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

  1. abstract type Signed <: Integer end

因此我们可以说Int64类型是Integer类型的间接子类型。

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

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

4.1.2 一个特点

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

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

  1. julia> function sum1(a, b)
  2. a + b
  3. end
  4. sum1 (generic function with 1 method)
  5. julia>

注意,在functionend之间的代码就是我们对sum1函数的定义。该函数的功能显而易见。它有两个参数ab,并且都没有类型标注。在这种情况下,我们使用两个整数值、两个浮点数值或者一个整数值及一个浮点数值作为参数值调用它都是可以的:

  1. julia> sum1(1, 2)
  2. 3
  3. julia> sum1(1.2, 2.3)
  4. 3.5
  5. julia> sum1(1.2, 4)
  6. 5.2
  7. julia>

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

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

  1. julia> function sum1(a::Real, b::Real)
  2. a + b
  3. end
  4. sum1 (generic function with 2 method)
  5. julia>

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

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

注意,我们到这里已经拥有了两个名为sum1的函数。更确切地说,这两个定义代表的都是sum1函数的衍生方法。第一个sum1函数的参数类型都是Any(我们稍后会讲到这个类型),而第二个sum1函数的参数类型都是Real。相比于前者,后者对参数的类型有了一定的约束。Julia 会根据我们调用这个函数时给予的参数值的类型来选择具体使用哪一个衍生方法。这依然是多重分派机制在起作用。别担心,你现在对此不理解并没有关系。我们会在后面用一章专门讲解函数、方法以及 Julia 的多重分派机制。对于抽象类型,我们在后面也会详细论述。

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