传统上,类型系统分为两个截然不同的阵营:静态类型系统和动态类型系统,在静态类型系统中,每个程序表达式必须在执行程序之前具有可计算的类型;在动态类型系统中,直到运行时对类型的任何了解,直到实际值该程序可以操纵。面向对象通过允许编写代码而无需在编译时知道精确的值类型,从而在静态类型的语言中提供了一定的灵活性。编写可以在不同类型上运行的代码的能力称为多态性。经典动态类型语言中的所有代码都是多态的:只有通过显式检查类型或对象在运行时无法支持操作时,才可以限制任何值的类型。
朱莉娅的类型系统是动态的,但是通过表明某些值属于特定类型,可以获得静态类型系统的某些优点。这对于生成有效的代码有很大的帮助,但更重要的是,它允许对函数参数类型的方法分派与该语言进行深度集成。在方法中详细探讨了方法分配,但它扎根于此处介绍的类型系统。
省略类型时,Julia的默认行为是允许值是任何类型。因此,无需显式使用类型就可以编写许多有用的Julia程序。但是,当需要其他表现力时,可以很容易地将显式类型注释逐步引入到以前的“无类型”代码中。这样做通常会提高这些系统的性能和健壮性,并且可能有点违反直觉,通常会大大简化它们。
用类型系统的术语描述Julia ,它是:动态的,主格的和参数化的。可以对泛型类型进行参数化,并且显式声明类型之间的层次关系,而不是由兼容结构隐含。朱莉娅类型系统的一个特别与众不同的特征是,具体类型不能互为子类型:所有具体类型都是最终类型,并且只能具有抽象类型作为其超类型。虽然这乍看起来似乎过分地限制了它,但它带来了许多有益的结果,但缺点却很少。事实证明,能够继承行为比能够继承结构更为重要,并且继承两者都会给传统的面向对象语言带来很大的困难。朱莉娅类型系统的其他高级方面应在前面提到:
isbits()
返回true 的任何类型的值(本质上是像数字和布尔值之类的东西,如C类型或没有指针指向其他对象的结构存储)的参数化,也可以由其元组参数化。当不需要引用或限制类型参数时,可以将其省略。朱莉娅的字体系统被设计为功能强大且富有表现力,但清晰,直观且不引人注目。许多Julia程序员可能永远都不会觉得需要编写显式使用类型的代码。但是,使用已声明的类型,某些类型的编程将变得更加清晰,简单,快速且健壮。
该::
操作可用于连接类型注释表达式和变量的程序。这样做有两个主要原因:
将::
运算符附加到计算值的表达式后,将其读作“是...的实例”。它可以在任何地方断言左表达式的值是右类型的实例。当右侧的类型为具体类型时,左侧的值必须具有该类型作为其实现-请记住,所有具体类型都是最终类型,因此任何实现都不是其他任何类型的子类型。当类型是抽象类型时,就可以由作为抽象类型的子类型的具体类型实现该值。如果类型断言不为真,则抛出异常,否则,返回左侧的值:
julia> (1+2)::AbstractFloat
ERROR: TypeError: typeassert: expected AbstractFloat, got Int64
julia> (1+2)::Int
3
这允许将类型断言附加到任何表达式上。
当附加到赋值左侧的变量或作为local
声明的一部分时,::
运算符的含义有些不同:它声明变量始终具有指定的类型,例如静态类型的类型声明。语言,例如C。分配给变量的每个值都将使用转换为声明的类型convert()
:
julia> function foo()
x::Int8 = 100
x
end
foo (generic function with 1 method)
julia> foo()
100
julia> typeof(ans)
Int8
此功能对于避免在变量的分配之一意外更改其类型时可能发生的性能“陷阱”很有用。
此“声明”行为仅在特定情况下发生:
local x::Int8 # in a local declaration
x::Int8 = 10 # as the left-hand side of an assignment
并适用于整个当前范围,甚至在声明之前。当前,类型声明不能在全局范围内使用,例如在REPL中,因为Julia还没有常量类型的全局变量。
声明也可以附加到函数定义中:
function sinc(x)::Float64
if x == 0
return 1
end
return sin(pi*x)/(pi*x)
end
从此函数返回的行为就像是对具有声明类型的变量的赋值一样:该值始终转换为Float64
。
抽象类型无法实例化,只能用作类型图中的节点,从而描述了相关具体类型的集合:作为其后代的具体类型。我们从抽象类型开始,尽管它们没有实例化,因为它们是类型系统的骨干:它们形成概念层次结构,这使Julia的类型系统不仅仅是对象实现的集合。
回想一下,在整数和浮点数,我们推出了多种类型的具体数值的:Int8
,UInt8
,Int16
,UInt16
,Int32
,UInt32
,Int64
,UInt64
,Int128
,UInt128
,Float16
,Float32
,和Float64
。虽然他们有不同的表示大小,Int8
,Int16
,Int32
,Int64
和Int128
所有的共同点在于它们是整数类型签名。同样UInt8
,UInt16
,UInt32
,UInt64
并且UInt128
都是无符号整数类型,同时Float16
,Float32
和Float64
与浮点类型不同,而不是整数。例如,仅当一段代码的参数是某种整数,而不真正取决于哪种特定类型的整数时,一段代码才有意义。例如,最大的公分母算法适用于所有类型的整数,但不适用于浮点数。抽象类型允许构造类型的层次结构,从而提供适合具体类型的上下文。例如,这使您可以轻松地编程为任何整数类型,而无需将算法限制为特定的整数类型。
抽象类型使用abstract type
关键字声明。声明抽象类型的常规语法为:
abstract type «name» end
abstract type «name» <: «supertype» end
该abstract type
关键字引入了一个新的抽象类型,其名字由下式给出«name»
。该名称后面可以有一个可选的名称,<:
并且该名称已经存在,表示新声明的抽象类型是此“父”类型的子类型。
如果未指定任何超类型,则默认超类型为Any
–预定义的抽象类型,该对象的所有对象均为实例,所有类型均为其子类型。在类型理论中,Any
通常将其称为“顶部”,因为它位于类型图的顶点。Julia在类型图的最低点处还有一个预定义的抽象“底部”类型,写为Union{}
。恰恰相反Any
:没有对象是的实例,Union{}
所有类型都是的超类型Union{}
。
让我们考虑一下构成Julia的数字层次结构的一些抽象类型:
abstract type Number end
abstract type Real <: Number end
abstract type AbstractFloat <: Real end
abstract type Integer <: Real end
abstract type Signed <: Integer end
abstract type Unsigned <: Integer end
该Number
类型的直接子类Any
,并且Real
是它的孩子。反过来,Real
有两个孩子(有更多孩子,但这里只显示了两个;稍后我们将介绍其他孩子):Integer
和AbstractFloat
,将世界分为整数表示和实数表示。实数的表示形式当然包括浮点类型,但也包括其他类型,例如有理数。因此,AbstractFloat
是的适当子类型Real
,仅包含实数的浮点表示。整数进一步细分为Signed
和Unsigned
。
在<:
一般的手段经营者“是的子类型”,并在声明中这样使用,声明右手型是新声明的类型的直接超类型。它也可以在表达式中用作子类型运算符,true
当其左操作数是其右操作数的子类型时返回:
julia> Integer <: Number
true
julia> Integer <: AbstractFloat
false
抽象类型的重要用途是为具体类型提供默认实现。举一个简单的例子,考虑:
function myplus(x,y)
x+y
end
首先要注意的是,上述参数声明与x::Any
和等效y::Any
。调用此函数时,如as myplus(2,5)
,调度程序将选择myplus
与给定参数匹配的最具体的方法。(有关更多调度的更多信息,请参见方法。)
假设没有找到比上述方法更具体的方法,那么Julia会根据上面给出的泛型函数在内部定义并编译一个myplus
专门针对两个Int
参数的方法,即隐式定义和编译:
function myplus(x::Int,y::Int)
x+y
end
最后,它调用此特定方法。
因此,抽象类型允许程序员编写泛型函数,这些泛型函数以后可以由许多具体类型的组合用作默认方法。由于有多个分派,程序员可以完全控制是使用默认方法还是使用更具体的方法。
需要注意的重要一点是,如果程序员依赖于其参数为抽象类型的函数,则不会降低性能,因为对于调用它的参数具体类型的每个元组,该函数都会重新编译。(但是,在函数参数是抽象类型的容器的情况下,可能会出现性能问题;请参阅性能提示。)
基本类型是一种具体类型,其数据由普通旧位组成。基本类型的经典示例是整数和浮点值。与大多数语言不同,Julia使您可以声明自己的原始类型,而不是仅提供一组固定的内置类型。实际上,标准原始类型都是在语言本身中定义的:
primitive type Float16 <: AbstractFloat 16 end
primitive type Float32 <: AbstractFloat 32 end
primitive type Float64 <: AbstractFloat 64 end
primitive type Bool <: Integer 8 end
primitive type Char 32 end
primitive type Int8 <: Signed 8 end
primitive type UInt8 <: Unsigned 8 end
primitive type Int16 <: Signed 16 end
primitive type UInt16 <: Unsigned 16 end
primitive type Int32 <: Signed 32 end
primitive type UInt32 <: Unsigned 32 end
primitive type Int64 <: Signed 64 end
primitive type UInt64 <: Unsigned 64 end
primitive type Int128 <: Signed 128 end
primitive type UInt128 <: Unsigned 128 end
声明基本类型的常规语法为:
primitive type «name» «bits» end
primitive type «name» <: «supertype» «bits» end
位数表明类型需要多少存储空间,名称为新类型提供了名称。基本类型可以选择声明为某些超类型的子类型。如果省略了父类型,则该类型默认为具有Any
其直接父类型。Bool
因此,上面的声明意味着布尔值需要八位存储,并具有Integer
其直接超类型。当前,仅支持8位倍数的大小。因此,布尔值尽管实际上只需要一个位,但是不能声明为小于8位的任何值。
类型Bool
,Int8
并且UInt8
都具有相同的表示:他们的记忆八位块。由于Julia的类型系统是主格,因此尽管结构相同,但它们是不可互换的。它们之间的根本区别是它们具有不同的超类型:Bool
直接超类型是Integer
,Int8
的is Signed
和UInt8
的is Unsigned
。之间的所有其他方面的差异Bool
,Int8
并且,UInt8
是行为的问题-函数的定义给出这些类型作为参数的对象时的行为方式。这就是为什么必须使用名词性类型系统的原因:如果结构确定了类型,而类型又决定了行为,则不可能使Bool
行为不同于Int8
或UInt8
。
组合类型在各种语言中称为记录,结构或对象。复合类型是命名字段的集合,可以将其实例视为单个值。在许多语言中,复合类型是用户定义的唯一类型,并且它们也是迄今为止Julia中最常用的用户定义类型。
在主流的面向对象的语言(例如C ++,Java,Python和Ruby)中,复合类型还具有与之关联的命名函数,这种组合称为“对象”。在诸如Ruby或Smalltalk之类的纯面向对象的语言中,所有值都是对象,无论它们是否是复合的。在不太纯的面向对象的语言(包括C ++和Java)中,某些值(例如整数和浮点值)不是对象,而用户定义的复合类型的实例是具有关联方法的真实对象。在Julia中,所有值都是对象,但函数并未与它们所操作的对象捆绑在一起。这是必需的,因为Julia会选择通过多次分派使用哪个函数方法,这意味着所有类型选择方法时,将考虑函数的自变量,而不仅仅是第一个(请参见方法,以获取有关方法和调度的更多信息)。因此,函数仅“属于”它们的第一个参数是不合适的。将方法组织到功能对象中,而不是在每个对象内部“命名”方法包,最终成为语言设计的一个非常有益的方面。
组合类型通过struct
关键字引入,后跟一个字段名称块,可以选择使用::
操作符在类型中进行注释:
julia> struct Foo
bar
baz::Int
qux::Float64
end
没有类型注释的字段默认为Any
,并且可以相应地保存任何类型的值。
Foo
通过将Foo
类型对象像函数一样应用到其字段的值来创建类型的新对象:
julia> foo = Foo("Hello, world.", 23, 1.5)
Foo("Hello, world.", 23, 1.5)
julia> typeof(foo)
Foo
当类型像函数一样应用时,它称为构造函数。系统会自动生成两个构造函数(称为默认构造函数)。一个接受任何参数并调用convert()
以将其转换为字段的类型,另一个接受与字段类型完全匹配的参数。生成这两者的原因是,这使得添加新定义变得更加容易,而不会无意间替换默认构造函数。
由于bar
字段的类型不受限制,因此任何值都可以。但是,baz
必须将的值转换为Int
:
julia> Foo((), 23.5, 1)
ERROR: InexactError()
Stacktrace:
[1] convert(::Type{Int64}, ::Float64) at ./float.jl:679
[2] Foo(::Tuple{}, ::Float64, ::Int64) at ./none:2
您可以使用该fieldnames
功能找到一个字段名称列表。
julia> fieldnames(foo)
3-element Array{Symbol,1}:
:bar
:baz
:qux
您可以使用传统的foo.bar
符号访问复合对象的字段值:
julia> foo.bar
"Hello, world."
julia> foo.baz
23
julia> foo.qux
1.5
用声明的复合对象struct
是不可变的;它们在构造后无法修改。乍一看这很奇怪,但是它有几个优点:
不可变的对象可能包含可变对象(例如数组)作为字段。这些包含的物体将保持可变。只有不可变对象本身的字段不能更改为指向不同的对象。
如有需要,可以使用关键字声明可变的复合对象mutable struct
,这将在下一节中讨论。
没有字段的复合类型是单例。只能有一个此类实例:
julia> struct NoFields
end
julia> NoFields() === NoFields()
true
该===
函数确认“的两个”构造实例NoFields
实际上是相同的。单例类型将在下面进一步详细描述。
关于如何创建复合类型的实例还有更多的话要说,但是这种讨论既取决于参数类型也取决于方法,并且非常重要,因此可以在其自己的部分中进行论述:构造函数。
如果使用mutable struct
而不是声明了复合类型struct
,则可以修改其实例:
julia> mutable struct Bar
baz
qux::Float64
end
julia> bar = Bar("Hello", 1.5);
julia> bar.qux = 2.0
2.0
julia> bar.baz = 1//2
1//2
为了支持变异,此类对象通常在堆上分配,并具有稳定的内存地址。可变对象就像一个小容器,随着时间的推移可能具有不同的值,因此只能通过其地址可靠地进行标识。相反,不可变类型的实例与特定的字段值相关联-单独的字段值可以告诉您有关对象的所有信息。在确定是否使类型可变时,请问是否具有相同字段值的两个实例将被视为相同,或者是否可能需要随时间进行独立更改。如果将它们视为相同,则类型可能应该是不变的。
回顾一下,Julia中的两个基本属性定义了不变性:
考虑一下为什么这两个属性并存的原因,对那些具有C / C ++背景的读者尤其有启发性。如果它们是分开的,即,如果可以修改通过复制传递的对象的字段,那么将难以推理某些通用代码实例。例如,假设x
是抽象类型的函数参数,并且假设函数更改了字段:x.isprocessed = true
。根据x
是通过复制传递还是通过引用传递,此语句可能会或可能不会更改调用例程中的实际参数。在这种情况下,Julia通过禁止修改通过复制传递的对象字段来避免创建功能未知的函数的可能性。
实际上,前三个部分讨论的三种类型都紧密相关。它们具有相同的关键属性:
由于具有这些共享的属性,这些类型在内部表示为具有相同概念的实例,这些概念DataType
是以下任何类型的类型:
julia> typeof(Real)
DataType
julia> typeof(Int)
DataType
A DataType
可以是抽象的或具体的。如果是具体的,则具有指定的大小,存储布局和(可选)字段名称。因此,位类型是DataType
大小非零的a ,但没有字段名。复合类型是DataType
具有字段名称或为空(零大小)的。
系统中的每个具体价值都是其中一个实例DataType
。
类型联合是一种特殊的抽象类型,它包括使用特殊Union
功能构造的其任何参数类型的所有实例作为对象:
julia> IntOrString = Union{Int,AbstractString}
Union{AbstractString, Int64}
julia> 1 :: IntOrString
1
julia> "Hello!" :: IntOrString
"Hello!"
julia> 1.0 :: IntOrString
ERROR: TypeError: typeassert: expected Union{AbstractString, Int64}, got Float64
许多语言的编译器都有一个内部的并集构造来推理类型。Julia只是将其公开给程序员。
Julia的类型系统的一个重要且强大的功能是参数化:类型可以带有参数,因此类型声明实际上引入了一整套新类型-每个可能的参数值组合一个。有许多语言支持某种版本的通用编程,其中可以指定数据结构和操作它们的算法,而无需指定所涉及的确切类型。例如,仅举几例,ML,Haskell,Ada,Eiffel,C ++,Java,C#,F#和Scala中存在某种形式的通用编程。这些语言中的某些支持真正的参数多态性(例如ML,Haskell,Scala),而其他一些则支持基于临时的,基于模板的通用编程样式(例如C ++,Java)。由于使用了多种语言的通用编程和参数类型种类繁多,我们甚至不会尝试将Julia的参数类型与其他语言进行比较,而是将重点放在自行解释Julia的系统上。但是,我们会注意到,由于Julia是一种动态类型的语言,不需要在编译时做出所有类型的决定,
DataType
可以使用所有相同的语法对所有声明的类型(变体)进行参数化。我们将按以下顺序讨论它们:首先是参数复合类型,然后是参数抽象类型,最后是参数位类型。
类型参数在类型名称之后立即引入,并用花括号括起来:
julia> struct Point{T}
x::T
y::T
end
该声明定义了一个新的参数类型,其中Point{T}
包含type的两个“坐标” T
。有人可能会问是T
什么?嗯,这正是参数类型的意义:它可以是任何类型(实际上,也可以是任何位类型的值,尽管在这里显然用作类型)。Point{Float64}
是一个具体类型等效于通过更换定义的类型T
中的定义Point
与Float64
。因此,该单个声明实际上声明的类型的数量不受限制:Point{Float64}
,Point{AbstractString}
,Point{Int64}
等。每个这些是现在可用的具体类型:
julia> Point{Float64}
Point{Float64}
julia> Point{AbstractString}
Point{AbstractString}
类型Point{Float64}
是一个点,其坐标是64位浮点值,而类型Point{AbstractString}
是一个“点”,其“坐标”是字符串对象(请参见Strings)。
Point
本身也是一个有效的类型对象,包含所有实例Point{Float64}
,Point{AbstractString}
等,如亚型:
julia> Point{Float64} <: Point
true
julia> Point{AbstractString} <: Point
true
当然,其他类型不是其子类型:
julia> Float64 <: Point
false
julia> AbstractString <: Point
false
Point
值不同的具体类型T
永远不会是彼此的子类型:
julia> Point{Float64} <: Point{Int64}
false
julia> Point{Float64} <: Point{Real}
false
警告
最后这一点是非常重要的:尽管Float64 <: Real
我们不要有Point{Float64} <: Point{Real}
。
换句话说,用类型论的话来说,Julia的类型参数是不变的,而不是协变的(甚至是协变的)。这是出于实际的原因:尽管任何实例在Point{Float64}
概念上也可能像实例Point{Real}
,但两种类型在内存中具有不同的表示形式:
Point{Float64}
可以紧凑而有效地表示为64位值的直接对。Point{Real}
必须能够容纳的任意一对实例Real
。由于作为实例的对象Real
可以具有任意大小和结构,因此在实践中,的实例Point{Real}
必须表示为指向单独分配的Real
对象的一对指针。Point{Float64}
在数组的情况下,通过存储具有立即值的对象而获得的效率大大提高:Array{Float64}
可以将an 存储为具有64位浮点值的连续存储块,而Array{Real}
必须将数组存储为单独分配的指针Real
对象-可能是装箱的 64位浮点值,但也可能是任意大而复杂的对象,这些对象被声明为Real
抽象类型的实现。
由于Point{Float64}
不是的子类型Point{Real}
,因此以下方法不能应用于类型的参数Point{Float64}
:
function norm(p::Point{Real})
sqrt(p.x^2 + p.y^2)
end
正确的方式来定义,它接受类型的所有参数的方法Point{T}
,其中T
是的子类型Real
为:
function norm(p::Point{<:Real})
sqrt(p.x^2 + p.y^2)
end
(等效地,可以定义function norm{T<:Real}(p::Point{T})
或function norm(p::Point{T} where T<:Real)
;请参见UnionAll Types。)
更多示例将在后面的“ 方法”中进行讨论。
一个人如何构造一个Point
物体?可以为复合类型定义自定义构造函数,这将在“ 构造函数”中详细讨论,但是在没有任何特殊构造函数声明的情况下,有两种创建新复合对象的默认方法,一种是显式给出类型参数另一种是在对象构造函数的参数中暗含它们。
由于类型Point{Float64}
是一个具体的类型,等效于Point
用Float64
代替声明T
,因此可以将其相应地用作构造函数:
julia> Point{Float64}(1.0, 2.0)
Point{Float64}(1.0, 2.0)
julia> typeof(ans)
Point{Float64}
对于默认构造函数,必须为每个字段提供一个参数:
julia> Point{Float64}(1.0)
ERROR: MethodError: Cannot `convert` an object of type Float64 to an object of type Point{Float64}
This may have arisen from a call to the constructor Point{Float64}(...),
since type constructors fall back to convert methods.
Stacktrace:
[1] Point{Float64}(::Float64) at ./sysimg.jl:24
julia> Point{Float64}(1.0,2.0,3.0)
ERROR: MethodError: no method matching Point{Float64}(::Float64, ::Float64, ::Float64)
由于无法覆盖参数类型,因此仅生成一个默认的构造函数。此构造函数接受任何参数并将其转换为字段类型。
在许多情况下,提供Point
一个想要构造的对象类型是多余的,因为构造函数调用的参数类型已经隐式提供了类型信息。因此,只要Point
参数类型的隐含值T
是明确的,您还可以将其自身用作构造函数:
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
,T
当且仅当两个参数Point
具有相同的类型时,类型才会明确隐含。如果不是这种情况,则构造函数将失败并显示MethodError
:
julia> Point(1,2.5)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)
Closest candidates are:
Point(::T, !Matched::T) where T at none:2
可以定义适当处理这种混合情况的构造方法,但是稍后将在Constructors中进行讨论。
参数抽象类型声明以相似的方式声明抽象类型的集合:
julia> abstract type Pointy{T} end
使用此声明,Pointy{T}
是的每个类型或整数值都有一个不同的抽象类型T
。与参数组合类型一样,每个此类实例都是的子类型Pointy
:
julia> Pointy{Int64} <: Pointy
true
julia> Pointy{1} <: Pointy
true
参数抽象类型是不变的,与参数组合类型一样:
julia> Pointy{Float64} <: Pointy{Real}
false
julia> Pointy{Real} <: Pointy{Float64}
false
符号Pointy{<:Real}
可以被用于表达的朱类似物协变型,而Pointy{>:Int}
一个的类似物逆变型,但在技术上这些代表组的类型(参见UnionAll类型)。
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
给定这样的声明,对于的每个选择T
,我们都有Point{T}
以下子类型Pointy{T}
:
julia> Point{Float64} <: Pointy{Float64}
true
julia> Point{Real} <: Pointy{Real}
true
julia> Point{AbstractString} <: Pointy{AbstractString}
true
这种关系也是不变的:
julia> Point{Float64} <: Pointy{Real}
false
julia> Point{Float64} <: Pointy{<:Real}
true
像参数这样的抽象类型有什么用途Pointy
?考虑一下,如果我们创建一个只需要一个坐标的类点实现,因为该点位于对角线上x = y:
julia> struct DiagPoint{T} <: Pointy{T}
x::T
end
现在Point{Float64}
和DiagPoint{Float64}
都是Pointy{Float64}
抽象的实现,并且类似地适用于其他所有可能的type选择T
。这样就可以编程为所有Pointy
对象共享的通用接口,Point
并为和实现DiagPoint
。但是,直到我们在下一节“ 方法”中介绍方法并进行分派之前,才能完全证明这一点。
在某些情况下,类型参数在所有可能的类型上自由范围可能没有意义。在这种情况下,可以T
像这样限制范围:
julia> abstract type Pointy{T<:Real} end
有了这样的声明,可以使用任何作为的子类型的类型来Real
代替T
,但不能使用不是以下子类型的类型Real
:
julia> Pointy{Float64}
Pointy{Float64}
julia> Pointy{Real}
Pointy{Real}
julia> Pointy{AbstractString}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Type{AbstractString}
julia> Pointy{1}
ERROR: TypeError: Pointy: in T, expected T<:Real, got Int64
可以用相同的方式来限制参数组合类型的类型参数:
struct Point{T<:Real} <: Pointy{T}
x::T
y::T
end
为了举例说明所有这些参数类型机制如何有用,下面是Julia Rational
不可变类型的实际定义(除了为简单起见,这里省略了构造函数),它表示整数的精确比例:
struct Rational{T<:Integer} <: Real
num::T
den::T
end
仅取整数值的比率是有意义的,因此将参数类型T
限制为的子类型Integer
,并且整数比率表示实数行上的值,因此任何Rational
实例都是Real
抽象的实例。
元组是函数参数的抽象,而没有函数本身。函数参数的主要方面是它们的顺序和类型。因此,元组类型类似于参数化的不可变类型,其中每个参数都是一个字段的类型。例如,2元素元组类型类似于以下不可变类型:
struct Tuple2{A,B}
a::A
b::B
end
但是,存在三个主要区别:
Tuple{Int}
是的子类型Tuple{Any}
。因此,Tuple{Any}
将其视为抽象类型,并且元组类型仅在其参数为实的情况下才是具体的。元组值用括号和逗号书写。构建元组时,会根据需要生成适当的元组类型:
julia> typeof((1,"foo",2.5))
Tuple{Int64,String,Float64}
注意协方差的含义:
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元组类型用于表示varargs方法接受的参数(请参见Varargs函数)。
类型Vararg{T,N}
完全对应于N
type的元素T
。 NTuple{N,T}
是一个方便的别名,例如Tuple{Vararg{T,N}}
,一个元组类型正好包含type的N
元素T
。
这里必须提到一种特殊的抽象参数类型:单例类型。对于每种类型,T
“单一类型” Type{T}
都是抽象类型,其唯一实例是object T
。由于定义有点难以解析,因此让我们看一些示例:
julia> isa(Float64, Type{Float64})
true
julia> isa(Real, Type{Float64})
false
julia> isa(Real, Type{Real})
true
julia> isa(Float64, Type{Real})
false
换句话说,isa(A,Type{B})
当且仅当A
和B
是相同的对象并且该对象是一种类型时,才为true 。没有参数,Type
只是一个抽象类型,它以所有类型对象作为其实例,当然包括单例类型:
julia> isa(Type{Float64}, Type)
true
julia> isa(Float64, Type)
true
julia> isa(Real, Type)
true
任何不是类型的对象都不是的实例Type
:
julia> isa(1, Type)
false
julia> isa("foo", Type)
false
在我们讨论参数化方法和转换之前,很难解释单例类型构造的实用性,但是简而言之,它允许人们将函数行为专门化为特定类型值。这对于编写其行为取决于作为显式参数给出的类型而不是其参数之一的类型所隐含类型的方法(尤其是参数化方法)很有用。
一些流行的语言具有单例类型,包括Haskell,Scala和Ruby。通常,术语“单一类型”是指其唯一实例是单个值的类型。此含义适用于Julia的单例类型,但需要注意的是,只有类型对象具有单例类型。
基本类型也可以通过参数声明。例如,指针表示为原始类型,将在Julia中声明如下类型:
# 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
我们已经说过,像这样的参数类型将Ptr
充当其所有实例(Ptr{Int64}
等等)的超类型。这是如何运作的?Ptr
它本身不能是普通的数据类型,因为在不知道引用数据的类型的情况下,该类型显然不能用于内存操作。答案是Ptr
(或其他参数类型,如Array
)是另一种类型,称为UnionAll
类型。这种类型表示某个参数的所有值的类型的迭代联合。
UnionAll
类型通常使用关键字where
。例如Ptr
,可以更准确地写为Ptr{T} where T
,表示类型为Ptr{T}
的某个值的所有值T
。在这种情况下,该参数T
通常也称为“类型变量”,因为它就像一个跨类型的变量。每个where
变量都引入一个类型变量,因此这些表达式嵌套在具有多个参数的类型中,例如Array{T,N} where N where T
。
类型应用程序语法A{B,C}
必须A
是UnionAll
类型,并且首先替换B
中最外面的类型变量A
。预期结果是另一种UnionAll
类型,C
然后将其替换。所以A{B,C}
等于A{B}{C}
。这就解释了为什么可以部分实例化一个类型,例如Array{Float64}
:第一个参数值是固定的,但是第二个参数值仍在所有可能的值范围内。使用显式where
语法,可以固定任何参数子集。例如,所有一维数组的类型都可以写成Array{T,1} where T
。
类型变量可以受子类型关系限制。Array{T} where T<:Integer
引用元素类型为的所有数组Integer
。该语法Array{<:Integer}
是的便捷简写Array{T} where T<:Integer
。类型变量可以具有上限和下限。Array{T} where Int<:T<:Number
引用Number
能够包含Int
s的所有s 数组(因为T
必须至少与一样大Int
)。该语法where T>:Int
还可以仅指定类型变量的下限,并且Array{>:Int}
等效于Array{T} where T>:Int
。
由于where
表达式嵌套,所以类型变量边界可以引用外部类型变量。例如,Tuple{T,Array{S}} where S<:AbstractArray{T} where T<:Real
引用2元组,其第一个元素为some Real
,并且其第二个元素为Array
任何类型的数组,其元素类型包含第一个元组元素的类型。
该where
关键字本身可以被嵌套在一个更复杂的声明中。例如,考虑以下声明创建的两种类型:
julia> const T1 = Array{Array{T,1} where T, 1}
Array{Array{T,1} where T,1}
julia> const T2 = Array{Array{T,1}, 1} where T
Array{Array{T,1},1} where T
类型T1
定义一维数组的一维数组;每个内部数组都由相同类型的对象组成,但是此类型可能因一个内部数组而异。另一方面,type T2
定义一维数组,其中一维数组的所有内部数组必须具有相同的类型。请注意,这T2
是抽象类型,例如Array{Array{Int,1},1} <: T2
,而是T1
具体类型。结果,T1
可以使用零参数构造函数构造a=T1()
但T2
不能构造。
有一种方便的语法来命名此类类型,类似于函数定义语法的简称:
Vector{T} = Array{T,1}
这等效于const Vector = Array{T,1} where T
。编写Vector{Float64}
与编写等效Array{Float64,1}
,并且伞形类型Vector
具有所有Array
对象的实例,其中第二个参数(数组维数)为1,无论元素类型是什么。在必须始终完整指定参数类型的语言中,这并不是特别有帮助,但是在Julia中,这允许人们只Vector
为抽象类型编写代码,包括任何元素类型的所有一维密集数组。
有时,为已经可以表达的类型引入新名称很方便。这可以通过一个简单的赋值语句来完成。例如,对于系统上的指针大小,UInt
别名为UInt32
或,别名UInt64
是:
# 32-bit system:
julia> UInt
UInt32
# 64-bit system:
julia> UInt
UInt64
这是通过以下代码完成的base/boot.jl
:
if Int === Int64
const UInt = UInt64
else
const UInt = UInt32
end
当然,这要看是什么Int
的别名-但被预定是正确的类型-无论是Int32
或Int64
。
(请注意,与不同Int
,Float
并不作为特定大小的类型别名存在AbstractFloat
。与整数寄存器不同,浮点寄存器的大小由IEEE-754标准指定。而的大小Int
反映了该计算机上本机指针的大小。)
由于Julia中的类型本身就是对象,因此普通函数可以对其进行操作。已经引入了一些对于处理或浏览类型特别有用的函数,例如<:
运算符,该运算符指示其左手操作数是否是其右手操作数的子类型。
该isa
函数测试对象是否为给定类型并返回true或false:
julia> isa(1, Int)
true
julia> isa(1, AbstractFloat)
false
该typeof()
函数已在示例的整个手册中使用,返回其参数的类型。如上所述,由于类型是对象,因此它们也具有类型,我们可以询问它们的类型是什么:
julia> typeof(Rational{Int})
DataType
julia> typeof(Union{Real,Float64,Rational})
DataType
julia> typeof(Union{Real,String})
Union
如果我们重复该过程怎么办?类型的类型是什么类型?碰巧的是,类型都是复合值,因此都具有以下类型DataType
:
julia> typeof(DataType)
DataType
julia> typeof(Union)
DataType
DataType
是它自己的类型。
适用于某些类型的另一个操作是supertype()
,它揭示了类型的超类型。只有声明的类型(DataType
)具有明确的超类型:
julia> supertype(Float64)
AbstractFloat
julia> supertype(Number)
Any
julia> supertype(AbstractString)
Any
julia> supertype(Any)
Any
如果将其应用于supertype()
其他类型对象(或非类型对象),MethodError
则会引发a:
julia> supertype(Union{Float64,Int64})
ERROR: MethodError: no method matching supertype(::Type{Union{Float64, Int64}})
Closest candidates are:
supertype(!Matched::DataType) at operators.jl:41
supertype(!Matched::UnionAll) at operators.jl:46
通常,人们想要自定义类型实例的显示方式。这是通过重载show()
功能来实现的。例如,假设我们定义了一种类型,以极性形式表示复数:
julia> struct Polar{T<:Real} <: Number
r::T
Θ::T
end
julia> Polar(r::Real,Θ::Real) = Polar(promote(r,Θ)...)
Polar
在这里,我们添加了一个自定义的构造函数,以便它可以接受不同Real
类型的参数并将其提升为通用类型(请参阅构造函数以及Conversion和Promotion)。(当然,我们必须定义很多其他的方法,也使它像一个Number
,例如+
,*
,one
,zero
,促销规则等等。)默认情况下,而只是这种类型的显示器的情况下,与有关信息类型名称和字段值,例如Polar{Float64}(3.0,4.0)
。
如果我们希望将其显示为3.0 * exp(4.0im)
,则可以定义以下方法将对象打印到给定的输出对象io
(代表文件,终端,缓冲区等;请参见网络和流):
julia> Base.show(io::IO, z::Polar) = print(io, z.r, " * exp(", z.Θ, "im)")
Polar
可以对对象的显示进行更细粒度的控制。特别地,有时人们既想要用于在REPL和其他交互环境中显示单个对象的冗长的多行打印格式,又想要一种更紧凑的用于print()
或作为另一对象的一部分显示对象的单行格式。(例如,在数组中)。尽管默认情况下show(io, z)
会在两种情况下都调用该函数,但是您可以通过重载以MIME类型作为其第二个参数的三参数形式来定义用于显示对象的不同多行格式(请参见Multimedia I / O),例:showtext/plain
julia> Base.show{T}(io::IO, ::MIME"text/plain", z::Polar{T}) =
print(io, "Polar{$T} complex number:\n ", z)
(请注意,print(..., z)
这里将调用2参数show(io, z)
方法。)这将导致:
julia> Polar(3, 4.0)
Polar{Float64} complex number:
3.0 * exp(4.0im)
julia> [Polar(3, 4.0), Polar(4.0,5.3)]
2-element Array{Polar{Float64},1}:
3.0 * exp(4.0im)
4.0 * exp(5.3im)
单行show(io, z)
形式仍然用于Polar
值数组。技术上,REPL来电display(z)
显示执行一条线,其默认的结果show(STDOUT, MIME("text/plain"), z)
,而这又默认show(STDOUT, z)
,但你应该不定义新的display()
,除非你要定义一个新的多媒体显示处理器(参见方法多媒体I / O)。
此外,您还可show
以为其他MIME类型定义方法,以在支持此功能的环境(例如IJulia)中更丰富地显示对象(HTML,图像等)。例如,我们可以通过以下方式定义Polar
对象的带格式的HTML显示:上标和斜体
julia> Base.show{T}(io::IO, ::MIME"text/html", z::Polar{T}) =
println(io, "<code>Polar{$T}</code> complex number: ",
z.r, " <i>e</i><sup>", z.Θ, " <i>i</i></sup>")
然后,Polar
对象将在支持HTML显示的环境中使用HTML自动显示,但是如果需要,您可以show
手动调用以获取HTML输出:
julia> show(STDOUT, "text/html", Polar(3.0,4.0))
<code>Polar{Float64}</code> complex number: 3.0 <i>e</i><sup>4.0 <i>i</i></sup>
HTML渲染器将显示为:Polar{Float64}
复数:3.0 e 4.0 i
在Julia中,您无法分派诸如或的值。但是,您可以分派参数类型,Julia允许您将“普通位”值(类型,符号,整数,浮点数,元组等)包括为类型参数。一个常见的示例是中的Dimensionity参数,其中是类型(例如),但仅仅是一个。truefalseArray{T,N}TFloat64NInt
您可以创建自己的以值作为参数的自定义类型,并使用它们来控制自定义类型的调度。为了说明这一点,让我们介绍一个参数类型,Val{T}
它是在不需要更详尽的层次结构的情况下利用此技术的一种常用方法。
Val
定义为:
julia> struct Val{T}
end
除了实现之外,没有更多的实现了Val
。Julia的标准库中的某些函数接受Val
类型作为参数,您也可以使用它编写自己的函数。例如:
julia> firstlast(::Type{Val{true}}) = "First"
firstlast (generic function with 1 method)
julia> firstlast(::Type{Val{false}}) = "Last"
firstlast (generic function with 2 methods)
julia> firstlast(Val{true})
"First"
julia> firstlast(Val{false})
"Last"
为了确保Julia的一致性,呼叫站点应始终传递Val
类型而不是创建实例,即使用foo(Val{:bar})
而不是foo(Val{:bar}())
。
值得注意的是,滥用参数“值”类型非常容易,包括Val
; 在不利的情况下,您很容易最终使代码的性能变差。特别是,您永远都不想编写如上所述的实际代码。有关正确(和不正确)使用的更多信息Val
,请阅读性能提示中的更广泛的讨论。
在许多设置中,您需要与T
可能存在或可能不存在的类型的值进行交互。为了处理这些设置,Julia提供了称为的参数类型Nullable{T}
,可以将其视为可以包含零或一个值的特殊容器类型。Nullable{T}
提供了一个最小的界面,旨在确保与缺失值的交互是安全的。目前,该界面包含几种可能的交互方式:
Nullable
对象。Nullable
对象是否缺少值。Nullable
对象的值并保证NullException
如果缺少该对象的值将抛出a 。Nullable
对象的值,并保证T
如果缺少该对象的值,将返回默认类型的值。Nullable
,得到Nullable
结果。如果缺少原始值,则结果将丢失。Nullable
如果Nullable
本身丢失或测试失败,则得到缺少的结果。Nullable
对象执行常规操作,以传播丢失的数据。Nullable
对象要构造表示缺少类型的值的对象T
,请使用以下Nullable{T}()
函数:
julia> x1 = Nullable{Int64}()
Nullable{Int64}()
julia> x2 = Nullable{Float64}()
Nullable{Float64}()
julia> x3 = Nullable{Vector{Int64}}()
Nullable{Array{Int64,1}}()
要构造一个表示type的非缺失值的对象T
,请使用以下Nullable(x::T)
函数:
julia> x1 = Nullable(1)
Nullable{Int64}(1)
julia> x2 = Nullable(1.0)
Nullable{Float64}(1.0)
julia> x3 = Nullable([1, 2, 3])
Nullable{Array{Int64,1}}([1, 2, 3])
请注意,这两种构造Nullable
对象的方式之间的核心区别是:在一种样式中,您提供了类型T
,作为函数参数;在另一种样式中,您提供一个type值T
作为参数。
Nullable
对象是否具有值您可以使用以下命令检查Nullable
对象是否具有任何值isnull()
:
julia> isnull(Nullable{Float64}())
true
julia> isnull(Nullable(0.0))
false
Nullable
对象的值您可以使用以下命令安全地访问Nullable
对象的值get()
:
julia> get(Nullable{Float64}())
ERROR: NullException()
Stacktrace:
[1] get(::Nullable{Float64}) at ./nullable.jl:92
julia> get(Nullable(1.0))
1.0
如果不存在该值Nullable{Float64}
,NullException
则将引发错误。该get()
函数的错误抛出性质可确保任何访问缺失值的尝试均立即失败。
如果某个合理的默认值存在,而当某个Nullable
对象的值丢失时可以使用该默认值,则可以将该默认值作为第二个参数提供给get()
:
julia> get(Nullable{Float64}(), 0.0)
0.0
julia> get(Nullable(1.0), 0.0)
1.0
小费
确保传递给默认值的类型get()
与Nullable
对象的类型匹配,以避免类型不稳定,这可能会损害性能。convert()
如果需要,请手动使用。
Nullable
对象执行操作Nullable
对象表示可能丢失的值,可以通过使用进行测试,首先检查值是否丢失isnull()
,然后执行适当的操作,从而使用这些对象编写所有代码。但是,在一些常见的用例中,通过使用高阶函数可以使代码更简明。
该map
函数将一个函数f
和一个Nullable
值作为参数x
。它产生一个Nullable
:
x
是缺失值,则产生缺失值;x
具有值,则产生一个Nullable
包含f(get(x))
值。如果期望的行为是简单地向前传播缺失的值,则对于在可能缺失的值上执行简单操作很有用。
该filter
函数将谓词函数p
(即返回布尔值的函数)和Nullable
value 作为参数x
。它产生一个Nullable
值:
x
是缺失值,则产生缺失值;p(get(x))
为true,则产生原始值x
。p(get(x))
为false,则产生一个缺失值。这样,filter
可以认为仅选择允许的值,然后将不允许的值转换为缺失值。
虽然map
和filter
在特定情况下很有用,但到目前为止,最有用的高阶函数是broadcast
,它可以处理各种情况,包括使现有操作正常工作和传播Nullable
。一个例子将激发对的需求broadcast
。假设我们有一个函数,可以使用二次公式来计算二次方程的两个实根中的较大者:
julia> root(a::Real, b::Real, c::Real) = (-b + √(b^2 - 4a*c)) / 2a
root (generic function with 1 method)
正如我们期望的那样,我们可以验证的结果root(1, -9, 20)
是5.0
,因为它5.0
是二次方程的两个实根中的较大者。
现在假设我们想找到一个二次方程的最大实根,其中系数可能缺少值。数据集中缺少值是现实数据中的常见现象,因此能够处理它们很重要。但是,如果我们不知道所有系数,就无法找到方程式的根。最好的解决方案将取决于特定的用例。也许我们应该抛出一个错误。但是,对于本示例,我们将假定最佳解决方案是将丢失的值向前传播。也就是说,如果缺少任何输入,我们只会产生一个缺少的输出。
该broadcast()
功能使此任务变得容易。我们可以简单地传递root
我们写给的函数broadcast
:
julia> broadcast(root, Nullable(1), Nullable(-9), Nullable(20))
Nullable{Float64}(5.0)
julia> broadcast(root, Nullable(1), Nullable{Int}(), Nullable{Int}())
Nullable{Float64}()
julia> broadcast(root, Nullable{Int}(), Nullable(-9), Nullable(20))
Nullable{Float64}()
如果缺少一个或多个输入,则输出broadcast()
将丢失。
对于broadcast()
使用点表示法的功能,存在特殊的语法糖:
julia> root.(Nullable(1), Nullable(-9), Nullable(20))
Nullable{Float64}(5.0)
特别是,可以broadcast()
使用.
-prefixed 运算符方便地使用常规算术运算符:
julia> Nullable(2) ./ Nullable(3) .+ Nullable(1.0)
Nullable{Float64}(1.66667)