Julia 用类型系统的术语描述是动态(dynamic)、主格(nominative)和参数(parametric)的。泛型可以被参数化,并且类型之间的层次关系可以被显式地声明,而不是隐含地通过兼容的结构。
Julia 类型系统的一个特别显著的特征是具体类型相互之间不能是子类型:所有具体类型都是最终的类型,并且只有抽象类型可以作为其超类型。Julia中的继承是继承行为,而不是继承结构。
变量/表达式 :: 类型
抽象类型不能被实例化
abstract type «name» end
abstract type «name» <: «supertype» end
e.g. abstract type Integer :< Real end
:<意味新抽象类型是后面的子类型。默认为Any
Note.
function myplus(x,y)
x+y
end
上述的参数声明等价于 x::Any 和 y::Any。当函数被调用时,例如 myplus(2,5),派发器会选择与给定参数相匹配的名称为 myplus 的最具体方法。
假设没有找到比上述方法更具体的方法,Julia 接下来会在内部定义并编译一个名为 myplus 的方法,专门用于基于上面给出的泛型函数的两个 Int 参数,即它定义并编译:
function myplus(x::Int,y::Int)
x+y
end
最后,调用这个具体的方法。
因此,抽象类型允许程序员编写泛型函数,之后可以通过许多具体类型的组合将其用作默认方法。即使程序员依赖参数为抽象类型的函数,性能也不会有任何损失,因为它会针对每个调用它的参数元组的具体类型重新编译。(但在函数参数是抽象类型的容器的情况下,可能存在性能问题)
具体类型,可以实例化为对象的
primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end
Note. 目前支持的大小只能是 8 位的倍数。
e.g. primitive type Float64 <: AbstractFloat 64 end
<类型>(变量 / 表达式)
convert(类型, 变量 / 表达式)
Int8(10)
convert(Int8, 10)
这种方法仅限数字之间,且不能越界。
parse(类型, 字符串)
julia> 1 + 2.0
3.0
promote(tuple)
将tuple所有元素提升到它们之间的最大范围。
即自定义类型,用struct定义,struct是不可变类型,创建后无法更改。
Note. 不可变对象可以包含可变对象(比如数组)作为字段。那些被包含的对象将保持可变;只是不可变对象本身的字段不能更改为指向不同的对象。
struct Foo
x1
x2::Int
x3::Float64
end
julia> foo = Foo("Hello World", 10, 11.9)
julia> typeof(foo)
Foo
julia> foo.x1
"Hello World!"
两个默认构造:一个可以接受任意参数(如x1),另一个接受与字段类型完全匹配的参数,(如x2, x3)。
fieldnames()可以找到字段名列表。
没有字段的不可变复合类型是单态类型;这种类型只能有一个实例:
julia> struct NoFields
end
julia> NoFields() === NoFields()
true
在定义的struct前面加mutable。建立在堆上,有稳定的内存地址。
前面章节中讨论的三种类型(抽象、原始、复合)实际上都是密切相关的。它们共有相同的关键属性:
由于这些共有属性,它们在内部表现为相同概念 DataType 的实例,其是任何这些类型的类型。
DataType 可以是抽象的或具体的。它如果是具体的,就具有指定的大小、存储布局和字段名称(可选)。因此,原始类型是具有非零大小的 DataType,但没有字段名称。复合类型是具有字段名称或者为空(大小为零)的 DataType。
每一个具体的值在系统里都是某个 DataType 的实例。
类型共用体是一种特殊的抽象类型,它包含作为对象的任何参数类型的所有实例,使用特殊Union关键字构造:
Julia> IntOrString = Union{Int,AbstractString}
Union{Int64, AbstractString}
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
ERROR: TypeError: in typeassert, expected Union{Int64, AbstractString}, got a value of type Float64
Union 类型的一种特别有用的情况是 Union{T, Nothing}。通过将函数参数或字段声明为 Union{T, Nothing},可以将其设置为类型为 T 的值,或者 nothing 来表示没有值。
Julia 类型系统的一个重要和强大的特征是它是参数的:类型可以接受参数,因此类型声明实际上引入了一整套新类型——每一个参数值的可能组合引入一个新类型。
所有已声明的类型(DataType 类型)都可被参数化,在每种情况下都使用一样的语法。
类型名称后引入,用大括号扩起来:
julia> struct Point{T}
x::T
y::T
end
Point{Float64} 是一个具体类型,该类型等价于通过用 Float64 替换 Point 的定义中的 T 所定义的类型。因此,单独这一个声明实际上声明了无限个类型:Point{Float64},Point{AbstractString},Point{Int64},等等。这些类型中的每一个类型现在都是可用的具体类型。
Point 本身也是一个有效的类型对象,包括所有实例 Point{Float64}、Point{AbstractString} 等作为子类型。
Point 不同 T 值所声明的具体类型之间,不能互相作为子类型。i.e. 即使 Float64 <: Real 也没有 Point{Float64} <: Point{Real}。它们的内存表示也不同:
一种正确的方法来定义一个接受类型的所有参数的方法,Point{T}其中T是一个子类型Real:
function norm(p::Point{<:Real})
sqrt(p.x^2 + p.y^2)
end
(等效地,另一种定义方法 function norm(p::Point{T} where T<:Real) 或 function norm(p::Point{T}) where T<:Real。)
由于 Point{Float64} 类型等价于在 Point 声明时用 Float64 替换 T 得到的具体类型,它可以相应地作为构造函数使用:
julia> Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(ans)
Point{Float64}
在许多情况下,提供想要构造的 Point 对象的类型是多余的,因为构造函数调用参数的类型已经隐式地提供了类型信息。
julia> Point(1.0,2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(ans)
Point{Float64}
julia> Point(1,2)
Point{Int64}(1, 2)
julia> typeof(ans)
Point{Int64}
在 Point 的例子中,当且仅当 Point 的两个参数类型相同时,T 的类型才确实是隐含的。如果不是这种情况,构造函数将失败并出现 MethodError:
julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
julia> abstract type Pointy{T} end
在此声明中,对于每个类型或整数值 T,Pointy{T} 都是不同的抽象类型。与参数复合类型一样,每个此类型的实例都是 Pointy 的子类型:
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{1} <: Pointy
true
符号 Pointy{<:Real} 可用于表示协变类型的 Julia 类似物,而 Pointy{>:Int} 类似于逆变类型,但从技术上讲,它们都代表了类型的集合。
julia> Pointy{Float64} <: Pointy{<:Real}
true
julia> Pointy{Real} <: Pointy{>:Int}
true
正如之前的普通抽象类型用于在具体类型上创建实用的类型层次结构一样,参数抽象类型在参数复合类型上具有相同的用途。例如,我们可以将 Point{T} 声明为 Pointy{T} 的子类型:
julia> struct Point{T} <: Pointy{T}
x::T
y::T
end
julia> Point{Float64} <: Pointy{Float64}
true
julia> Point{Real} <: Pointy{Real}
true
julia> Point{AbstractString} <: Pointy{AbstractString}
true
元组类型类似于参数化的不可变类型,其中每个参数都是一个字段的类型。
struct Tuple2{A,B}
a::A
b::B
end
然而,有三个主要差异:
julia> Tuple{Int,AbstractString} <: Tuple{Real,Any}
true
julia> Tuple{Int,AbstractString} <: Tuple{Real,Real}
false
julia> Tuple{Int,AbstractString} <: Tuple{Real,}
false
元组类型的最后一个参数可以是特殊类型 Vararg,它表示任意数量的尾随参数:
julia> mytupletype = Tuple{AbstractString,Vararg{Int}}
Tuple{AbstractString,Vararg{Int64,N} where N}
julia> isa(("1",), mytupletype)
true
julia> isa(("1",1), mytupletype)
true
julia> isa(("1",1,2), mytupletype)
true
julia> isa(("1",1,2,3.0), mytupletype)
false
Vararg{T} 对应于零个或更多的类型为 T 的元素。变参元组类型被用来表示变参方法接受的参数。
类型 Vararg{T,N} 对应于正好 N 个类型为 T 的元素。NTuple{N,T} 是 Tuple{Vararg{T,N}} 的别名,即包含正好 N 个类型为 T 元素的元组类型。
具名元组是 NamedTuple 类型的实例,该类型有两个参数:一个给出字段名称的符号元组,和一个给出字段类型的元组类型。
julia> typeof((a=1,b="hello"))
NamedTuple{(:a, :b),Tuple{Int64,String}}
原始类型也可以参数化声明,例如,指针都能表示为原始类型
# 32-bit system:
primitive type Ptr{T} 32 end
# 64-bit system:
primitive type Ptr{T} 64 end
与典型的参数复合类型相比,此声明中略显奇怪的特点是类型参数 T 并未在类型本身的定义里使用——它实际上只是一个抽象的标记,定义了一整族具有相同结构的类型,类型间仅由它们的类型参数来区分。因此,Ptr{Float64} 和 Ptr{Int64} 是不同的类型,就算它们具有相同的表示。当然,所有特定的指针类型都是总类型 Ptr 的子类型:
julia> Ptr{Float64} <: Ptr
true
julia> Ptr{Int64} <: Ptr
true
这种类型表示某些参数的所有值的类型的迭代并集。
UnionAll通常使用关键字where编写:
struct Pos{T}
x::T
y::T
end
function f1(p::Pos{T} where T<:Real)
p
end
当有两个类型参数时:
struct Pos2{T1, T2}
x::T1
y::T2
end
function f2(p::Pos2{T1, T2} where T1<:Int64 where T2<:Float64)
p
end
where 也可以同时限定T的上下界
function f3(pos{T} where Int64<:T<:Real)
p
end
已经引入了一些对于使用或探索类型特别有用的函数,例如 <: 运算符,它表示其左操作数是否为其右操作数的子类型。
isa 函数测试对象是否具有给定类型并返回 true 或 false:
julia> isa(1, Int)
true
julia> isa(1, AbstractFloat)
false