Engee 文档

转型及推广

Julia有一个将数学运算符的参数推进到一般类型的系统,这在其他部分中已经提到,包括 整数和浮点数, 数学运算和初等函数, 类型方法。 本节解释了这个推广系统的工作原理,如何将其扩展到新类型,并将其不仅应用于数学运算符,还应用于其他函数。 在促进算术论证方面,编程语言传统上分为两类。

  • *自动提升内置算术类型和运算符。*在大多数语言中,当使用内置数字类型作为中缀语法算术运算符的操作数时,例如``, `-`, `*` 和'/`,它们被自动提升为一般类型以获得预期的结果。 在C,Java,Perl,Python和许多其他语言中,添加`1+1.5`的结果将是`2.5`的浮点值,尽管``的操作数之一是整数。 这些系统很方便,并且经过深思熟虑,因此对于程序员来说,它们通常几乎是看不见的:很少有人在编写这样的表达式时考虑促销。 但是,编译器和解释器需要在添加之前执行转换,因为整数和浮点值不能自己添加。 因此,自动类型转换的复杂规则不可避免地成为此类语言规范和实现的一部分。

  • *缺乏自动推广。*静态类型非常严格的Ada和ML语言属于这一类。 在这些语言中,任何转换都必须由程序员明确定义。 因此,在编译示例表达式`1+1.5’时,Ada和ML都会出现错误。 相反,使用表达式’real(1)+1.5`,在加法之前显式地将整数值`1`转换为浮点值。 然而,不断执行显式转换是如此不方便,以至于即使在某种程度上实现了自动转换:整数字面量自动提升为预期的整数类型,浮点字面量提升为相应的浮

从某种意义上说,Julia属于没有自动提升的语言类别:数学运算符只是具有特殊语法的函数,函数参数永远不会自动转换。 但是,可以注意到,将数学运算应用于各种类型的参数组合是多态多重调度的极端情况,而这正是Julia的类型和调度系统非常适合的。 数学操作数的自动提升原来只是一个应用的特例:Julia预先定义了数学运算的通用调度规则,这些规则在没有特定的操作数类型组合的具体实现时调用。 根据这些通用规则,所有操作数首先被提升为通用类型,同时考虑到用户定义的提升规则,然后对接收到的值调用运算符的专门实现,这些值现在是相 通过定义将自定义类型转换为其他类型(反之亦然)的方法,以及通过设置确定与其他类型结合使用时应将其提升为哪些类型的提升规则,可以轻松地将自定义类型集成到此提升系统中。

转型

获取某种类型`T`的值的标准方法是调用这种类型的构造函数`T(x)`。 但是,在某些情况下,在没有程序员明确请求的情况下,将值从一种类型转换为另一种更方便。 一个例子是为数组元素赋值:如果’A’是一个对象'向量{Float64}',表达式’A[1]=2`应该自动将值`2`从类型`Int`转换为`Float64’并将结果保存在数组中。 这是由函数完成的 '转换'

'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>转换(数组{Float64},a)
2×3矩阵{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中,应该使用特殊函数来执行此操作。 'parse',这使得操作更加明确。

什么时候调用"转换"?

'Convert’函数由以下语言结构调用。

  • 为数组赋值时,它会转换为数组元素的类型。

  • 为对象字段赋值时,它将转换为声明的字段类型。

  • 创建对象时使用 'new'正在转换为声明的对象字段类型。

  • 当为具有声明类型的变量赋值时(例如,local x::T)正在转换为此类型。

  • 如果为函数声明了返回类型,则返回值将转换为该类型。

  • 当将值传递给 `ccall'它被转换为适当的参数类型。

转型与创造

看起来调用’convert(T,x)`的工作原理与`T(x)`几乎相同。 事实上,通常情况就是这样。 但是,请注意一个重要的语义差异。:由于’convert’函数可以隐式调用,因此其方法仅限于被认为是安全或预期的情况。 "convert"仅在表示相同性质实体的类型之间执行转换(例如,不同编码中的数字或字符串的不同表示)。 此外,转换通常是无损的:如果将值转换为不同的类型,然后返回,则应获得完全相同的值。

构造函数与`convert`函数有四种主要情况。

与参数无关的类型的构造函数

对于某些构造函数,转换的概念并不适用。 例如,调用’Timer(2)'会创建一个两秒的计时器,但这不能被称为从整数转换为timer对象。

可变集合

如果`x`已经是`T`类型,则对`convert(T,x)`的调用应返回`x’的原始值。 相反,如果’T’是可变集合的类型,则调用’T(x)`应始终创建一个新集合(从`x’复制元素)。

外壳类型

对于作为其他值包装器的某些类型,构造函数可以将其参数包含在新对象中,即使该参数已具有请求的类型。 例如,'Some(x)'作为`x`的包装器,指示值存在(在结果可能是`Some`或`nothing’的上下文中)。 但是’x’也可以是`Some(y)的对象。 在这种情况下,结果将是’Some(Some(y)),即两级shell。 反过来,表达式’convert(Some,x)简单地返回`x,因为这个变量已经是’Some’类型。

不返回其类型实例的构造函数

在极少数情况下,构造函数`T(x)返回不是`T’类型的对象是有意义的。 如果包装器类型与自身相反(例如,`Flip(Flip(x))===x),或者如果需要重新设计库以支持向后兼容的旧语法,则可能会出现这种情况。 但是,对`convert(T,x)`的调用必须始终返回类型为`T’的值。

定义新的转换

定义新类型时,最初必须将其创建的所有方法定义为构造函数。 如果事实证明隐式转换可能有用,并且某些构造函数符合上述安全标准,则可以添加"convert"方法。 通常,这些方法非常简单,因为它们只调用适当的构造函数。 这样的定义可能是这样的。

import Base: convert
convert(::Type{MyType}, x) = MyType(x)

此方法的第一个参数具有类型 '类型{MyType}',其中唯一的实例是’MyType'。 因此,只有当第一个参数是`MyType’类型的值时,才调用此方法。 注意用于第一个参数的语法:省略字符`::`之前的参数名称,只指定类型。 此语法在Julia中用于指定类型的函数参数,但其值不需要通过名称引用。

默认情况下,某些抽象类型的所有实例都被认为足够相似,因此基本Julia模块提供了`convert`函数的通用定义。 例如,根据以下定义,任何数字类型(Number)都可以使用`convert’转换为任何其他数字类型,方法是使用一个参数调用构造函数。

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)

浮点值被提升为浮点参数类型中最大的值。 整数值被提升为最大的整数参数类型。 如果类型具有相同的大小但符号不同,则选择无符号类型。 整数值和浮点值的组合被提升为足够大的浮点类型,以存储每个值。 整数和有理值的组合被提升为有理值。 有理值和浮点值的组合被提升为浮点值。 复数值和实数值的组合被提升为适当类型的复数值。

这结束了使用促销的问题。 剩下的就是胜任地应用它们,并且,作为一项规则,胜任的应用包括定义数值运算的通用方法,例如算术运算符。 +, -, * 和'’。 以下是通用方法的一些定义https://github.com/JuliaLang/julia/blob/master/base/promotion.jl ['推广。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)...)

根据这些方法的定义,如果没有更具体的数字值对的加、减、乘和除规则,则将值提升为一般类型并重复尝试。 就是这样:在执行算术运算时,再也没有人需要担心升级到通用数字类型-它会自动发生。 在https://github.com/JuliaLang/julia/blob/master/base/promotion.jl ['推广。jl']有许多其他算术和数学函数的通用提升方法的定义,但除此之外,基本Julia模块中的其他`promote`调用实际上不需要。 大多数情况下,`promote’函数用于外部构造函数方法,这些方法是为了方便而提供的。 多亏了它,具有不同类型参数的构造函数调用被传递给一个内部方法,该方法的字段被提升到相应的常规类型。 例如,回想一下,在https://github.com/JuliaLang/julia/blob/master/base/rational.jl ['理性。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_rule',可以为其提供方法。 'Promote_rule’函数接受一对类型对象并返回另一个类型对象,以便参数类型的实例被提升为返回的类型。 因此,使用以下规则:

import Base: promote_rule
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64

它声明在推进64位和32位浮点值时,它们必须前进到64位浮点值。 生成的提升类型不一定必须是参数类型之一。 例如,Base Julia模块具有以下提升规则。

promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt

在后一种情况下,得到的类型是 'BigInt',因为`BigInt`是唯一一个足够大的类型来存储在任意精度算术运算中使用的整数。 此外,请注意,还应定义规则`promote_rule(::Type)。{A},::类型{B})`,和规则'promote_rule(::Type{B},::类型{A})'不必要—​在提升过程中,对称应用`promote_rule’函数。

函数’promote_rule’用作定义另一个函数的组件 — 'promote_type',它接受任意数量的类型对象,并返回作为参数传递给`promote_type`函数的相应值将被提升到的常规类型。 因此,使用’promote_type’函数,当值本身还不知道时,您可以找出将某些类型的一组值提升到哪种类型。

julia> promote_type(Int8, Int64)
Int64

请注意,'promote_type’函数*不会*直接重载:相反,'promote_rule’被重载。 'Promote_type’函数使用’promote_rule’并添加对称规则。 如果直接重载它,可能会由于模糊性而发生错误。 为了确定应该如何提升类型,我们重载`promote_rule`,并找出它是如何发生的,我们使用`promote_type'。

在内部,"promote"在内部使用函数"promote_type"来确定参数值应转换为提升的类型。 好奇的读者可以阅读模块中的代码。 https://github.com/JuliaLang/julia/blob/master/base/promotion.jl ['推广。jl'],其在大约35行中完全定义了促进机制。

例子:推广有理数

总之,我们将继续研究Julia中有理数类型的例子,它具有相对复杂的实现提升机制的规则。

import Base: promote_rule
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)

根据第一规则,有理数与任何整数类型的组合被提升为有理类型,其分子和分母的类型是分子和分母的原始类型和原始整数类型的提升的结果。 第二条规则将相同的逻辑应用于不同类型的两个有理数:产生的有理数类型是推进分子和分母的原始类型的结果。 第三个也是最后一个规则规定将有理数和浮点数的组合提升到分子和分母类型以及浮点类型的类型。

这一小组提升规则,以及默认的类型构造函数和数字的"转换"方法,足以让有理数与Julia中的任何其他数字类型-整数,浮点和复杂一起非常自然地使用。 通过以类似的方式为任何自定义数字类型提供转换方法和提升规则,您可以确保它适用于预定义的Julia数字类型。