轨道
/
Julia
Julia
/
练习
/
评分大师
评分大师

评分大师

学习练习

简介

到目前为止,教学大纲还没怎么提到类型,但类型在 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 编译器会通读(全部)代码,查看变量是如何使用的,然后_推断_出一个与该用法兼容的合适默认类型。

类型赋值

在解决简单的教程练习时,依赖类型推断是没问题的,但对更大的程序来说,你很可能需要更精确的控制。

在大多数现代处理器上,整数默认为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,它们可以分为几类:

  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。

说明

在这个练习中,你将通过预处理一些成绩数据来做一点数据工程。 一所拥有很多很多很多(是不是太多了?)学生的学校,想对学生的成绩做一些数据分析,并希望高效地完成,所以思路是尽量精简并整理这些数据。

评分范围通常是 0 到 10,所以最高效的做法是使用每个成绩只占 8 位的数据类型。

  • 有些教授使用实数(他们对精度非常讲究),你需要把这些数转换成UInt8数据类型。
  • 另一些教授使用带负数的整数(用来惩罚特别不听话的学生),在这些情况下你需要把它们转换成Int8数据类型。

实现转换成绩的函数之后,你还需要再写一个,用来处理存放这些成绩的集合:把成绩降级,并按降序返回。

  • 有些教授使用按升序排好的Vector来存放成绩,因为他们对数据一丝不苟。
  • 另一些教授使用无序的Set来存放成绩,因为他们有点懒。

在这两个函数中,你都需要通过抛出 MethodError 来处理无效输入。

Note

异常处理会在后面的概念中介绍,所以就本练习而言,你可以使用下面这种语法:

throw(MethodError(f, args))

其中f是函数,args是传入该函数的实参构成的元组。 对于demote(n)函数,f是demote,args是(n,):

throw(MethodError(demote, (n,)))

1. 降级成绩

实现demote(n)方法。

  • 输入为Float64时,应当向上取整到最接近的整数,并返回UInt8数据类型。
  • 输入为任意Integer时,应当以Int8数据类型返回同一个整数。
  • 其他所有输入都应当抛出 MethodError。
julia> demote(4.2)::UInt8
5

julia> demote(4)::Int8
4

julia> demote("hi")
MethodError: no method matching demote(::String)     #output truncated

2. 预处理数据

实现preprocess(coll)方法。

  • 输入为Vector时,应当把所有数字降级,并把该向量反转。
  • 输入为Set时,应当把所有数字降级,并返回按降序排好的向量。
  • 其他所有输入都应当抛出 MethodError。
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
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Julia Exercism

准备好开始 评分大师 了吗?

注册 Exercism,借助 35 个概念128 个练习 和真人导师指导,学习并掌握 Julia,全部免费。