转换与提升
Julia 具有一个用于将数学运算符的参数提升到通用类型的系统,这在其他部分中有所提及,包括 整数和浮点数、数学运算和基本函数、类型 和 方法。在本节中,我们将解释此提升系统的工作原理,以及如何将其扩展到新类型并将其应用于除内置数学运算符之外的函数。传统上,编程语言在算术参数的提升方面分属两个阵营
- 用于内置算术类型和运算符的自动提升。 在大多数语言中,内置数值类型在用作具有中缀语法的算术运算符的操作数时,例如
+
、-
、*
和/
,会自动提升到通用类型以产生预期的结果。举几个例子,C、Java、Perl 和 Python 都能正确地计算出1 + 1.5
的和为浮点值2.5
,即使+
的其中一个操作数是整数。这些系统很方便,而且设计得足够谨慎,因此对于程序员来说几乎是不可见的:几乎没有人会在编写这样的表达式时有意识地想到这种提升正在发生,但编译器和解释器必须在加法之前执行转换,因为整数和浮点值无法按原样相加。因此,对于此类语言的规范和实现来说,用于此类自动转换的复杂规则不可避免地是其中的一部分。 - 没有自动提升。 这一阵营包括 Ada 和 ML——非常“严格”的静态类型语言。在这些语言中,必须由程序员显式地指定每次转换。因此,在 Ada 和 ML 中,示例表达式
1 + 1.5
将是一个编译错误。相反,必须编写real(1) + 1.5
,在执行加法之前显式地将整数1
转换为浮点值。然而,在任何地方都进行显式转换非常不便,即使 Ada 也具有一定的自动转换功能:整数文字会自动提升到预期的整数类型,浮点文字也会类似地提升到相应的浮点类型。
从某种意义上说,Julia 属于“没有自动提升”类别:数学运算符只是具有特殊语法的函数,函数的参数永远不会自动转换。但是,可以观察到,将数学运算应用于各种混合参数类型只是多态多重分派的极端情况——Julia 的分派和类型系统特别适合处理这种情况。“自动”提升数学操作数只是作为一个特殊应用出现:Julia 附带预定义的用于数学运算符的万能分派规则,当不存在用于某些操作数类型组合的特定实现时,这些规则会被调用。这些万能规则首先使用用户可定义的提升规则将所有操作数提升到一个通用类型,然后调用运算符的特定实现来处理现在具有相同类型的结果值。用户定义的类型可以通过定义转换为其他类型的方法以及提供一些提升规则来轻松地参与此提升系统,这些提升规则定义了在与其他类型混合时应该提升到哪些类型。
转换
获取特定类型 T
的值的标准方法是调用类型的构造函数 T(x)
。但是,在某些情况下,在不显式要求程序员的情况下将值从一种类型转换为另一种类型会很方便。一个例子是将值分配到数组中:如果 A
是一个 Vector{Float64}
,表达式 A[1] = 2
应该通过自动将 2
从 Int
转换为 Float64
并将结果存储到数组中来工作。这是通过 convert
函数完成的。
convert
函数通常接受两个参数:第一个是类型对象,第二个是要转换为该类型的数值。返回值是转换为给定类型实例的数值。了解此函数最简单的方法是查看其在实际中的应用
julia> x = 12
12
julia> typeof(x)
Int64
julia> xu = convert(UInt8, x)
0x0c
julia> typeof(xu)
UInt8
julia> xf = convert(AbstractFloat, x)
12.0
julia> typeof(xf)
Float64
julia> a = Any[1 2 3; 4 5 6]
2×3 Matrix{Any}:
1 2 3
4 5 6
julia> convert(Array{Float64}, a)
2×3 Matrix{Float64}:
1.0 2.0 3.0
4.0 5.0 6.0
转换并不总是可能的,在这种情况下,会抛出 MethodError
,指示 convert
不知道如何执行所请求的转换
julia> convert(AbstractFloat, "foo")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
[...]
某些语言认为将字符串解析为数字或将数字格式化为字符串是转换(许多动态语言甚至会自动为您执行转换)。在 Julia 中并非如此。尽管某些字符串可以解析为数字,但大多数字符串不是数字的有效表示形式,而且只有非常有限的字符串子集是有效的表示形式。因此,在 Julia 中,必须使用专门的 parse
函数来执行此操作,使其更加明确。
何时调用 convert
?
以下语言结构会调用 convert
- 分配到数组会转换为数组的元素类型。
- 分配到对象的字段会转换为该字段的声明类型。
- 使用
new
构造对象会转换为对象的声明字段类型。 - 分配到具有声明类型的变量(例如
local x::T
)会转换为该类型。 - 具有声明返回值类型的函数会将其返回值转换为该类型。
- 将值传递给
ccall
会将其转换为相应的参数类型。
转换与构造
请注意,convert(T, x)
的行为似乎与 T(x)
几乎相同。实际上,通常是这样的。但是,存在一个关键的语义差异:由于 convert
可以被隐式调用,因此其方法仅限于被认为“安全”或“不令人惊讶”的情况。convert
仅会在表示同一基本类型的类型之间进行转换(例如,数字的不同表示形式或不同的字符串编码)。它通常也是无损的;将值转换为不同的类型然后再转换回来应该会产生完全相同的值。
在以下四种一般情况下,构造函数与 convert
不同
用于与参数无关类型的构造函数
某些构造函数没有实现“转换”的概念。例如,Timer(2)
创建一个 2 秒的计时器,这实际上不是将整数转换为计时器的“转换”。
可变集合
如果 x
已经是 T
类型,则 convert(T, x)
预计会返回原始 x
。相反,如果 T
是可变集合类型,则 T(x)
应该始终创建一个新的集合(复制 x
中的元素)。
包装类型
对于某些“包装”其他值的类型,构造函数可能会将它的参数包装在一个新的对象中,即使它已经是请求的类型。例如 Some(x)
将 x
包装起来以指示一个值存在(在一个结果可能是 Some
或 nothing
的上下文中)。然而,x
本身可能是对象 Some(y)
,在这种情况下,结果是 Some(Some(y))
,有两层包装。另一方面,convert(Some, x)
仅返回 x
,因为它已经是 Some
。
不返回自身类型实例的构造函数
在非常罕见的情况下,构造函数 T(x)
可能返回一个不是 T
类型的对象。如果包装类型是它自己的逆(例如 Flip(Flip(x)) === x
),或者为了支持库重构时为了向后兼容的旧调用语法,这可能会发生。但是 convert(T, x)
应该始终返回一个 T
类型的值。
定义新的转换
在定义一个新类型时,最初所有创建它的方法都应该定义为构造函数。如果很明显隐式转换会很有用,并且一些构造函数符合上述“安全”标准,那么可以添加 convert
方法。这些方法通常非常简单,因为它们只需要调用相应的构造函数。这样的定义可能看起来像这样
convert(::Type{MyType}, x) = MyType(x)
此方法第一个参数的类型是 Type{MyType}
,它的唯一实例是 MyType
。因此,只有当第一个参数是类型值 MyType
时,才会调用此方法。注意第一个参数使用的语法:参数名称在 ::
符号之前被省略,只给出类型。这是 Julia 中指定类型但其值不需要按名称引用的函数参数的语法。
默认情况下,所有抽象类型的实例都被认为“足够相似”,因此 Julia Base 提供了一个通用的 convert
定义。例如,此定义指出通过调用一个 1 参数构造函数将任何 Number
类型转换为任何其他类型是有效的
convert(::Type{T}, x::Number) where {T<:Number} = T(x)::T
这意味着新的 Number
类型只需要定义构造函数,因为此定义将为它们处理 convert
。还提供了一个标识转换来处理参数已经是请求类型的情况
convert(::Type{T}, x::T) where {T<:Number} = x
类似的定义适用于 AbstractString
、AbstractArray
和 AbstractDict
。
提升
提升是指将混合类型的值转换为单个通用类型。虽然它不是严格必要的,但通常隐含的是,将值转换到的通用类型可以忠实地表示所有原始值。从这个意义上说,术语“提升”是合适的,因为值被转换为“更高”的类型——也就是说,能够在一个通用类型中表示所有输入值的类型。然而,重要的是不要将它与面向对象(结构化)超类型或 Julia 的抽象超类型概念混淆:提升与类型层次结构无关,而与在替代表示之间转换有关。例如,虽然每个 Int32
值也可以表示为一个 Float64
值,但 Int32
不是 Float64
的子类型。
Julia 通过 promote
函数执行提升到通用“更高”类型,该函数接受任意数量的参数,并返回相同数量的值的元组,这些值被转换为通用类型,或者如果提升不可行,则抛出异常。提升最常见的用例是将数值参数转换为通用类型
julia> promote(1, 2.5)
(1.0, 2.5)
julia> promote(1, 2.5, 3)
(1.0, 2.5, 3.0)
julia> promote(2, 3//4)
(2//1, 3//4)
julia> promote(1, 2.5, 3, 3//4)
(1.0, 2.5, 3.0, 0.75)
julia> promote(1.5, im)
(1.5 + 0.0im, 0.0 + 1.0im)
julia> promote(1 + 2im, 3//4)
(1//1 + 2//1*im, 3//4 + 0//1*im)
浮点值被提升到最大的浮点参数类型。整数值被提升到最大的整数值参数类型。如果类型大小相同但符号不同,则选择无符号类型。整数和浮点值的混合被提升为足够大的浮点类型以容纳所有值。整数与有理数混合被提升为有理数。有理数与浮点数混合被提升为浮点数。复数与实数混合被提升为相应类型的复数。
这就是使用提升的全部内容。其余的只是一个巧妙应用的问题,最典型的“巧妙”应用是定义用于数值运算(如算术运算符 +
、-
、*
和 /
)的通用方法。以下是 promotion.jl
中给出的一些通用方法定义
+(x::Number, y::Number) = +(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)
这些方法定义说,在没有更具体的规则来添加、减去、乘法和除法数值对的情况下,将这些值提升为通用类型,然后再次尝试。这就是全部内容:在其他地方,人们永远不需要担心对算术运算的通用数值类型进行提升——它会自动发生。在 promotion.jl
中,为许多其他算术和数学函数定义了通用提升方法,但除此之外,Julia Base 中几乎不需要调用 promote
。promote
最常见的用法出现在外部构造函数方法中,为了方便起见,它们被提供出来,以允许使用混合类型的构造函数调用委托给内部类型,其字段被提升为适当的通用类型。例如,回想一下 rational.jl
提供了以下外部构造函数方法
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)
这使得以下调用能够正常工作
julia> x = Rational(Int8(15),Int32(-5))
-3//1
julia> typeof(x)
Rational{Int32}
对于大多数用户定义的类型,最好让程序员显式地向构造函数提供预期类型,但有时,特别是对于数值问题,自动进行提升可能很方便。
定义提升规则
虽然原则上可以为 promote
函数直接定义方法,但这将需要为所有可能的参数类型排列组合进行许多冗余定义。相反,promote
的行为是在名为 promote_rule
的辅助函数的基础上定义的,可以为其提供方法。promote_rule
函数接受一对类型对象,并返回另一个类型对象,这样,参数类型的实例将被提升为返回的类型。因此,通过定义规则
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64
声明当 64 位和 32 位浮点值一起提升时,它们应该被提升为 64 位浮点值。提升类型不需要是参数类型之一。例如,以下提升规则都出现在 Julia Base 中
promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt
在后一种情况下,结果类型是 BigInt
,因为 BigInt
是唯一能够保存任意精度整数算术的整数的类型。还要注意,不需要定义 promote_rule(::Type{A}, ::Type{B})
和 promote_rule(::Type{B}, ::Type{A})
——对称性是由 promote_rule
在提升过程中使用的方式隐含的。
promote_rule
函数用作构建块来定义第二个名为 promote_type
的函数,该函数给定任意数量的类型对象,返回这些值(作为 promote
的参数)应该被提升到的通用类型。因此,如果想要知道,在没有实际值的情况下,某些类型的值的集合将被提升为哪种类型,可以使用 promote_type
julia> promote_type(Int8, Int64)
Int64
注意,我们不直接重载 promote_type
:我们重载 promote_rule
。promote_type
使用 promote_rule
,并添加了对称性。直接重载它会导致歧义错误。我们重载 promote_rule
来定义如何进行提升,并使用 promote_type
来查询它。
在内部,promote_type
在 promote
中使用,以确定参数值应被提升到的类型。好奇的读者可以阅读 promotion.jl
中的代码,它在约 35 行中定义了完整的提升机制。
案例研究:有理数提升
最后,我们结束对 Julia 有理数类型的持续案例研究,该类型使用以下提升规则相对复杂地利用了提升机制
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{Rational{S}}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:AbstractFloat} = promote_type(T,S)
第一条规则说,用任何其他整数类型提升有理数会提升为一个有理数类型,其分子/分母类型是其分子/分母类型与其他整数类型提升的结果。第二条规则将相同的逻辑应用于两种不同类型有理数,导致一个有理数,它是其各自分子/分母类型提升的结果。第三条也是最后一条规则规定,用浮点数提升有理数的结果与用浮点数提升分子/分母类型相同。
这少量的提升规则,再加上类型的构造函数和数字的默认 convert
方法,足以让有理数与 Julia 的所有其他数值类型(整数、浮点数和复数)自然地交互。通过以相同的方式提供适当的转换方法和提升规则,任何用户定义的数值类型都可以像 Julia 的预定义数值一样自然地交互。