到目前为止,教学大纲对类型谈得并不多,但类型在 Julia 中显然是存在的:
julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')
julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}
我们从未指定过类型,但 Julia 还是给它们赋上了类型。
types,这是它设计的核心。Type Inference。JIT 编译器会检查(全部)代码,看一个变量是如何被使用的,然后_推断_出一个与该用法兼容的合适默认类型。
在之前的概念里,已经有许多把数值类型统一起来的例子,从最简单的算术运算就开始了。
julia> 2 + 1.3
3.3
我们把一个整数(Int64)和一个浮点数(Float64)相加,得到的结果是 Float64。类似地:
julia> nums = (3, 4.1, 1//4)
(3, 4.1, 1//4)
julia> typeof(nums)
Tuple{Int64, Float64, Rational{Int64}}
julia> [nums...]
3-element Vector{Float64}:
3.0
4.1
0.25
在上面的例子里,元组保留了每个元素的类型,但转换成向量后,它们全都变成了统一的 Float64。
Julia 编译器知道哪些类型转换是可行的:整数转浮点数没有问题,浮点数转整数会丢失精度,因此会抛出InexactError。
类型提升会把表达式中的所有值转换成一种公共类型,这种类型足够通用,能与所有输入兼容。
同样的转换也可以用promote()函数显式完成:
julia> promote(nums...)
(3.0, 4.1, 0.25)
在解决简单的教程练习时,依赖类型推断和类型提升没问题,但对于更大的程序,你很可能需要更精确的控制。
在大多数现代处理器上,整数默认是Int64。
我们在Numbers概念中看到,一个值可以被转换成某个非默认的类型。
julia> x = Int16(42)
42
julia> typeof(x)
Int16
不过,变量x仍然可以被重新赋值为另一种类型:
julia> x = "changed"
"changed"
julia> typeof(x)
String
这就是type instability,它:
反过来,我们可以用::运算符来设定x的类型:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> y = "changed"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int16
The function `convert` exists, but no method is defined for this combination of argument types.
现在y的类型是Int16,而且永远都会是。
这样一来,编译器就知道要为它预留多少字节,并且可以优化其余代码,让它们依赖一个稳定的类型。
在这一点上,这种变量与 C 这类静态类型语言中的变量大致相似。
上面介绍的类型赋值用于变量赋值的左侧,用来约束该变量的类型。
把::运算符用在一个值,或任何能求值得到值的东西上,通常是一种_断言_:该值必须是这个类型,否则就应该抛出错误。
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
常见的用法是把它用在函数的返回值上,作为检查函数是否按预期运行的最后一关。
注意,还有其他形式的断言,可以用@assert宏来实现。
Int64、Int16、String、Char:这些类型是_从哪儿来的_?
在许多面向对象(OO)语言中,每种类型都是一个类,子类化把它们安排进一个类层级中,而类方法定义了行为。
Java 和 Ruby 是这种模式的明显例子,但就连 Python 在内部也与之类似。
Julia 里没有类。
官方给出的原因是,面向对象的特性会干扰 JIT 编译器,损害运行时性能。
谁知道呢,也许他们也读过这句名言:
“面向对象编程是一个极其糟糕的主意,只可能起源于加利福尼亚。”
这句话出自Edsger Dijkstra,他是一位从 20 世纪 50 年代起活跃了数十年的杰出计算机科学家(不过他一般不以乐观开朗或委婉的外交辞令著称)。
然而,看看这段代码:
julia> y::Int16 = 42
42
julia> typeof(y)
Int16
julia> supertype(Int16)
Signed
julia> supertypes(Int16)
(Int16, Signed, Integer, Real, Number, Any)
补充一些细节:
Int16是一种类型,我们可以创建这种类型的变量。Int16是Signed的子类型,supertype()函数能让我们看到这一点。Integer、Real和Number,一直到顶端的Any,而subtypes()会为我们列出层级中的这一分支。所有分支最终都归于Any,它的特别之处在于它是自己的父类型。
julia> supertypes(String)
(String, AbstractString, Any)
julia> supertype(Any)
Any
所以,Julia 没有class层级,但它确实有type层级。
想把整个层级展示出来,会得到一棵巨大的树,根本没法真正看清楚。
至于查看其中的局部,是一个在网上讨论过的话题。
最终我们会努力弄清楚它是如何运作的,但在此之前还有很多东西要探索。
我们可以用typeof()按通常的方式测试相等性。
julia> typeof(11)
Int64
julia> typeof(11) == Int64
true
julia> typeof(11) == Number
false
类型相等必须是精确的,因为这种比较方式完全不了解类型层级。
更灵活的是,isa能告诉我们,一个值是否与比较对象类型相同,或者是它的子类型。
它既可以用中缀形式,也可以用函数形式。
julia> 12 isa Int64
true
julia> 12 isa Number
true
julia> isa(12, Number)
true
julia> 12 isa String
false
注意,isa左边要的是value,而不是type。
用这种方式比较两个_类型_会得到意想不到的结果。
正确的运算符是<:,在以后的概念里我们会见到更多。
julia> Int64 isa Number ## Don't do this!
false
julia> Int64 <: Number
true
测试类型可以用来控制函数内部的流程,但这在惯用的 Julia 代码中相对少见。
我们会在多重分派概念中看到,给函数的实参加上类型,让 Julia 的分派机制处理这类逻辑,往往更高效。 不过,在讲到多重分派之前,我们还有好几个与类型有关的概念要讨论。
我们看到,类型层级构成了一种树形结构(计算机科学意义上的树,根在顶部)。
树中的每一项都是一个node,它们可以分为几类:
julia> subtypes(Integer) # an abstract type
3-element Vector{Any}:
Bool
Signed
Unsigned
julia> subtypes(Int64) # a concrete type
Type[]
这个区分很重要,因为只有具体类型才能被instantiated成变量。
julia> a::Int16 = 42
42
julia> typeof(a)
Int16
julia> b::Integer = 42
42
julia> typeof(b)
Int64
注意,使用抽象类型(在这个例子里)并不会报错,但编译器会创建一个合适的具体类型:Int64,而不是Integer。
至少它能防止变量被赋值为非整数:
julia> f::Integer = "hello"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
The function `convert` exists, but no method is defined for this combination of argument types.
因此,用抽象类型做类型赋值,是对变量类型的一种_约束_,把它限制为(在上面的例子里)Integer的任意子类型。
这在编程语言的世界里很特别:它比 C 中的类型赋值弱,又比近期版本的 Python 中的类型提示强。
isabstracttype()和isconcretetype()这两个函数可以用来测试。
注意,它们并不只是彼此的反面:我们会在后面的某个概念中看到,有些类型既不是抽象的,也不是具体的。
julia> isconcretetype(Integer), isabstracttype(Integer)
(false, true)
julia> isconcretetype(Int64), isabstracttype(Int64)
(true, false)
# Vector is neither
julia> isconcretetype(Vector), isabstracttype(Vector)
(false, false)
虽然Vector不是具体类型,但它的元素是具体的。
eltype()(元素类型)函数可以提取出这个类型:
julia> eltype([1, 2.3])
Float64