到目前为止,教学大纲还没怎么提到类型,但类型在 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 编译器会通读(全部)代码,查看变量是如何使用的,然后_推断_出一个与该用法兼容的合适默认类型。
在解决简单的教程练习时,依赖类型推断是没问题的,但对更大的程序来说,你很可能需要更精确的控制。
在大多数现代处理器上,整数默认为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,而且永远都会是Int16。
因此,编译器知道要为它预留多少字节,并能优化其余代码,让它们依赖一个稳定的类型。
在这一点上,这个变量与 C 这类静态类型语言中的变量多少有些相似。
Int64、Int16、String、Char:这些类型_从何而来_。
在许多面向对象(OO)语言中,每个类型都是一个类,子类化把它们组织成一个类的层级结构,而类方法定义行为。
Java 和 Ruby 是这种模式的明显例子,但就连 Python 在内部也是类似的。
Julia 没有类。
文档给出的理由是,OO 功能会干扰 JIT 编译器,损害运行时性能。
然而,看看这段代码:
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结尾,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
我们看到,类型层级结构形成了一种树形结构(在计算机科学的意义上,根在顶部)。
树中的每一项都是一个node,它们可以分为几类:
abstract。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。
在这个练习中,你将通过预处理一些成绩数据来做一点数据工程。 一所拥有很多很多很多(是不是太多了?)学生的学校,想对学生的成绩做一些数据分析,并希望高效地完成,所以思路是尽量精简并整理这些数据。
评分范围通常是 0 到 10,所以最高效的做法是使用每个成绩只占 8 位的数据类型。
UInt8数据类型。Int8数据类型。实现转换成绩的函数之后,你还需要再写一个,用来处理存放这些成绩的集合:把成绩降级,并按降序返回。
Vector来存放成绩,因为他们对数据一丝不苟。Set来存放成绩,因为他们有点懒。在这两个函数中,你都需要通过抛出 MethodError 来处理无效输入。
异常处理会在后面的概念中介绍,所以就本练习而言,你可以使用下面这种语法:
throw(MethodError(f, args))
其中f是函数,args是传入该函数的实参构成的元组。
对于demote(n)函数,f是demote,args是(n,):
throw(MethodError(demote, (n,)))
实现demote(n)方法。
Float64时,应当向上取整到最接近的整数,并返回UInt8数据类型。Integer时,应当以Int8数据类型返回同一个整数。julia> demote(4.2)::UInt8
5
julia> demote(4)::Int8
4
julia> demote("hi")
MethodError: no method matching demote(::String) #output truncated
实现preprocess(coll)方法。
Vector时,应当把所有数字降级,并把该向量反转。Set时,应当把所有数字降级,并返回按降序排好的向量。julia> preprocess([1, 2, 3])
3-element Vector{Int8}:
3
2
1
julia> preprocess(Set([2.2, 5.8, 3.4]))
3-element Vector{UInt8}:
6
4
3
julia> preprocess(42)
MethodError: no method matching preprocess(::Int64) #output truncated