Julia复合类型

Julia复合类型,一般而言,元类型表达的是单一、纯粹的数值。但在实际的开发过程中,我们往往需要对各类事物进行建模,用于描述更为复杂多样的数据类型,例如,一个三维的地理坐标,不同制造商、颜色、发动机型号的汽车,等等。

为此,需要有一种结构能够将多个概念组合封装,用于表达多属性的事物模型。复合类型是Julia中可具有成员字段的复合结构,为开发者提供了自定义类型的机制,与元类型等基本类型有着相同的独立地位,也是Julia面向对象编程的基础,在Julia中有着广泛的应用。

基本定义

Julia中,复合类型均通过struct关键进行声明,基本语法为:

        [mutable] struct类型名
          成员字段1::类型1
          成员字段2::类型2
          # 其他成员字段
        end

其中,语法中的操作符::会限定成员字段为指定类型,不过是可以省略的。当省略时,成员类型默认为Any类型,意味着该字段变量能够绑定到任何已有的类型,包括元类型、抽象类型甚至是复合类型本身。关键字mutable是可选的标识符,不用时意味着该struct一旦实例化,成员取值将不能再进行任何变更;使用时则说明该struct可变,即创建后对象的成员可以反复被修改更新。

提示
如果了解其他面向对象语言会发现,struct内部并没有提供成员函数的定义语法,这是Julia有别于其他语言的特色之一:类型仅仅是类型。关键字struct仅需纯粹地声明复合类型,相关的各种操作方法可在结构外部进行定义,这也是Julia设计者建议的规范。

下面我们分别定义不可变和可变两个复合类型,如下:

struct FooA                 # 不可变类型
  a
  b::Float64
end

mutable struct FooB        # 可变类型
  a
  b::Float64
end

两者有着相同的字段,a无类型限制,b限定为Float64类型。

当然,成员也可以是复合类型,例如:

struct Bar
  m::FooB
  n::Int64
end

对于声明过的类型,我们可以通过内置的dump()函数查看其内部结构,如下:

FooA <: Any
  a::Any
  b::Float64

可见,a被默认为Any型,同时声明的复合类型的父类型也是Any型,因为复合类型也是Julia中的一种类型。另外一个复合类型FooB因类同不再列出。

不过,Bar的内部有些特别,如下:

Bar <: Any
  m::FooB
  n::Int64

其中,成员m是声明过的复合类型FooB。

实际上,如果查看声明的复合类型FooA本身的类型,会发现:

julia> typeof(FooA)
DataType

也是DataType的一种。事实上,以struct声明的复合类型均是DataType的实例对象。

对于声明的任意复合类型,除了使用dump()函数外,我们也可以通过一个专门函数fieldnames()获知其有哪些字段,例如:

julia> fieldnames(FooB)
(:a, :b)

该函数会在元组中以Symbol类型列出所有成员的名称。

默认构造函数

在声明复合类型后,对其实例化的最简单做法是采用如下的形式:

类型名(成员值1, 成员值2, …)                   # 其中三点省略符,指余下还有其他的成员实参值

这与函数的常规调用方式极为相似,只不过函数名正好是类型名。调用时,只需在参数列表中按字段定义的顺序逐一给出每个字段的值即可。例如:
Julia复合类型

便可得到FooA的复合类型对象x变量,其中a取1而b取2.5。

事实上,在复合类型声明的同时,Julia内部会自动为其提供默认的构造方法(Constructors),而且一般会有两种形式。以FooA为例,默认构造方法为:

FooA(a, b::Float64)
FooA(a, b)

可见构造方法是与类型同名的函数,而且默认构造方法的参数个数与字段数一致。

这两个默认构造方法的差异为:

  • 前者参数表依据字段定义,给出了严格的类型限定,调用者必须提供满足条件的实参值。
  • 后者参数表均是Any类型,接收参数时会自动将其转换到各字段要求的类型,如果转换失败则报错,实例化也会失败。

构造对象时,Julia会依照多态分发原则在多个构造方法中自动选择。如果构造时提供的参数不满足所有方法的要求,便会报MethodError异常,例如:
Julia复合类型

因为该例在对FooB实例化时,参数个数不符合任一构造方法的原型。

提示
出现异常时,Julia会在错误信息中的“Closest candidates are”之后给出可选的方法,开发者可以依此调整代码,或者按需自定义构造方法。

成员访问及不可变性

构造出复合类型的实例对象后,便可通过成员访问符(英文句号)访问其内部成员,形式为:

        对象名.字段名

注意
“对象名”与“成员访问符”之间不能有空格。

对于上文中FooA对象的x变量,有:

julia> x.a
1
julia> x.b
2.5

但是如果试图修改其成员的内容,会报错:
Julia复合类型

这是因为FooA在声明时,没有使用mutable进行标识。不过,前文中的FooB类型在声明时,是有mutable标识的。我们创建一个FooB对象,如下:

julia> y = FooB(2, 3.5)
FooB(2, 3.5)

julia> y.a
2

julia> y.b
3.5

再尝试修改其成员值:

julia> y.a = 2.9                     # 类型由Int64变为Float64
2.9

julia> y.b = 2//3
2//3

julia> y
FooB(2.9, 0.6666666666666666)      # 有理数被提升为浮点数

可见,成员被成功修改了。

此外,由于y中的字段b限定了类型,所以即使修改时提供了别的类型,新的值也会保持类型不变。但这种操作需要保证提供的新值能被提升为限定的类型,否则会报错,例如:

julia> y.a = 3.2        # 字段a是Any类型,所以正常执行
3.2

julia> y.b = 3+2im      # 字段b限定为浮点型,但给定的复数型是比浮点型“更大”的类型,故错误
ERROR: InexactError: Float64(Float64, 3 + 2im)

但是,可以将成员的类型限定为抽象类型,以提高其相容性,例如:

julia> mutable struct FooC
          a::Int32
          b::Real        # 限定为抽象类型,可以为实数的任意子类型
        end

julia> w = FooC(1, 2);

julia> typeof(w.b)
Int64                     # 实际类型是Int64

julia> w.b = 3.2;

julia> typeof(w.b)
Float64                  # 类型被改变

其中,定义的FooC成员b限定为Real类型,所以赋值为Int64或Float64类型均可。显然,成员b仍不能赋值为复数型,即:

julia> w.b = 3+2im      # Complex{Int64}
ERROR: InexactError: Real(Real, 3 + 2im)

会报错,因为复数不是Real类型的子类型。

如果成员有复合类型的Bar,将上例中的FooB对象y作为参数对其实例化,例如:

julia> z = Bar(y, 20)
Bar(FooB(2.9, 0.6666666666666666), 20)

详细查看其成员内容,如下:

julia> z.m
FooB(2.9, 0.6666666666666666)

julia> z.n
20

同样,Bar对象z也是不可变的:

julia> z.m = FooB(2.3, 3.8)
ERROR: type Bar is immutable

julia> z.n = 30
ERROR: type Bar is immutable

但其内部的FooB却是可变的,即:

julia> z.m.a = 2.5
2.5

julia> z.m.b = 5.0
5.0

可见,struct结构的标识符mutable只控制着本层的不可变性,不会波及内层或外层,即不可变性不会在上下层之间传播。

不可变的复合类型是Julia对结构内权限进行控制的一种方式。相对于可变复合类型,编译器在处理不可变类型时能够高效地处理内部的存储结构,而且能够推导出代码中使用了哪些不可变对象,从而提高了编译效率。

另外,正因为不可变对象内部成员的值是恒定的,所以值的组合便可用于区分不同的对象。相对地,可变对象的内容随时都会改变,不变的仅是其在堆中的内存地址,所以地址是区分它们的唯一可信的依据。更需要注意的一点是,不可变对象在赋值或函数传参时均采用“值拷贝”的方式,而可变对象则会采用引用的方式。

在开发过程中,使用可变对象还是不可变对象,可以参考以下两点:

  • 当两个对象具备相同的成员值集合时,是否需要识别为不同的事物。
  • 对象之间是否会随时独立地进行值的变换。

如果以上的答案都是“否”,则建议使用不可变类型。

单例复合类型

没有任何成员字段的不可变复合类型会成为单例(Singleton),即以其为基础创建的任意对象实际都是一样的,没有区别。换言之,单例类型有且仅有一个实例对象。例如:

julia> struct NoFields1        # 声明空类型NoFields1
        end
julia> struct NoFields2        # 声明空类型NoFields2
        end

对创建NoFields1的两个对象进行对比:

julia> NoFields1() === NoFields1()
true

可见两者是“完全相同”的。显然,对于NoFields2类型也同样如此。

但是,不同名的struct单例是不同的对象,即:

julia> NoFields1() === NoFields2()      # 不满足完全不同的要求
false

julia> NoFields1() == NoFields2()       # 内容虽然都是空的,但却不同
false

其实这点不难理解,类型名不同所以对象不同;而类型名相同时,既然两个对象都是零字段,自然在对比时内容也是相同的。

但需注意的是,被mutable关键字修饰的空字段的可变复合类型是不会成为单例类型的,例如:

julia> mutable struct NoMember1 end

julia> NoMember1() === NoMember1()
false

julia> NoMember1() == NoMember1()
false

虽然NoMember1的两个对象类型一致而且都是零字段,但却不是相同的对象,甚至都不相等,这点需要特别注意。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!