类型

类型 属于 Julia

1 个练习

关于 类型

到目前为止,教学大纲对类型谈得并不多,但类型在 Julia 中显然是存在的:

julia> vals = (42, 4.3, π, "hello", 'Q')
(42, 4.3, π, "hello", 'Q')

julia> typeof(vals)
Tuple{Int64, Float64, Irrational{:π}, String, Char}

我们从未指定过类型,但 Julia 还是给它们赋上了类型。

  1. Julia 有types,这是它设计的核心。
  2. Julia 通常能“猜出”类型,靠的是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,它们可以分为几类:

  1. 有子类型的节点称为abstract。
  2. 没有子类型的叶节点称为concrete。
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
通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 类型