现在位置: 首页 > Julia 教程 > 正文

Julia 结构体与类型系统

结构体(struct)是把多个相关数据组合成一个整体的自定义类型,类似 C 语言的 struct 或 Python 的类(但不包含方法)。

在 Julia 中,数据(结构体)和方法(函数)是分离的:函数不属于任何类型,而是通过多重派发根据参数类型选择实现,这一点会在方法与多重派发章节详细讲解。


定义结构体

使用 struct 关键字定义结构体,字段可以带类型标注,不带则默认为 Any:

实例

julia> struct Point
           x::Int      # 带类型标注的字段
           y::Int
           label       # 不带标注,类型为 Any
       end

把类型名当函数调用即可创建实例(类型名本身就是构造函数):

实例

julia> p = Point(3, 4, "A")
Point(3, 4, "A")

julia> typeof(p)
Point

# 访问字段用点语法
julia> p.x
3

# 声明了类型的字段会自动做类型转换
julia> Point(1.0, 2.0, "B")     # 1.0 被转换为 Int
Point(1, 2, "B")

# 无法转换时抛出 InexactError
julia> Point(1.5, 2, "B")
ERROR: InexactError: Int64(1.5)

注意:struct 字段的类型必须全都是同一种,无法给不同实例存不同类型。要让字段类型灵活可变,需要使用参数化类型,见下文。


不可变结构体

用 struct 定义的结构体是不可变的,创建后字段不能修改:

实例

julia> p.x = 10
ERROR: setproperty!: immutable struct of type Point cannot be changed

不可变有诸多好处:

  • 更高效:编译器可以把小对象完全放在寄存器或栈上,甚至完全省掉内存分配。
  • 更安全:对象的值一旦创建就不会被意外改动。
  • 字段相同的两个实例完全等价(=== 为 true)。

实例

julia> struct ImmutablePoint
           x::Int
           y::Int
       end

julia> ImmutablePoint(1, 2) === ImmutablePoint(1, 2)    # 值相同即同一个
true

最佳实践:在 Julia 中应优先使用不可变结构体,只有在确实需要修改对象状态时才用 mutable struct。


可变结构体(mutable struct)

mutable struct 定义的类型,实例的字段可以被修改:

实例

julia> mutable struct Stack
           items::Vector{Any}
           name::String
       end

julia> s = Stack([], "RUNOOB 栈")
Stack([], "RUNOOB 栈")

julia> push!(s.items, 1)     # 修改字段
1-element Vector{Any}:
 1

julia> s.name = "改名了"
"改名了"

可变对象分配在堆上,通过地址来标识身份,所以内容相同的两个实例也是不同的对象:

实例

julia> a = Stack([], "x"); b = Stack([], "x");

julia> a === b      # 不同的对象
false

julia> a == b       # 默认按字段比较?不,默认 == 等同于 ===
false

如果希望 == 按字段比较,可以自己定义 == 方法:

实例

julia> Base.:(==)(a::Stack, b::Stack) = a.name == b.name && a.items == b.items;

julia> a == b
true

const 字段

Julia 1.8 起,可变结构体的个别字段可以用 const 标注为不可修改:

实例

julia> mutable struct Baz
           a::Int
           const b::Float64     # b 创建后不可修改
       end

julia> baz = Baz(1, 1.5);

julia> baz.a = 2
2

julia> baz.b = 2.0
ERROR: setfield!: const field .b of type Baz cannot be changed

抽象类型

抽象类型(abstract type)不能实例化,它只是类型层级中的节点,用来描述一组相关的具体类型,类似其他语言中的接口概念:

实例

# 声明抽象类型,<: 表示继承自某个父类型
julia> abstract type Shape end

julia> abstract type Polygon <: Shape end     # Polygon 是 Shape 的子类型

Julia 的数值类型就是一个庞大的抽象类型体系:

Number
├─ Complex
└─ Real
   ├─ AbstractFloat(Float16/32/64、BigFloat)
   ├─ Integer
   │  ├─ Bool
   │  ├─ Signed(Int8...Int128、BigInt)
   │  └─ Unsigned(UInt8...UInt128)
   ├─ Rational
   └─ Irrational

<: 运算符可以判断子类型关系,supertype() 查询父类型,subtypes() 列出直接子类型:

实例

julia> Integer <: Number
true

julia> Int <: AbstractFloat
false

julia> supertype(Int64)
Signed

julia> Any <: Any       # Any 是所有类型的根
true

参数化类型(泛型)

参数化类型相当于其他语言中的泛型,用花括号声明类型参数:

实例

# Point 的两个字段类型相同,都由参数 T 决定
julia> struct Point{T}
           x::T
           y::T
       end

julia> Point(1, 2)          # T 自动推断为 Int64
Point{Int64}(1, 2)

julia> Point(1.0, 2.0)      # T 自动推断为 Float64
Point{Float64}(1.0, 2.0)

# 两个参数类型不一致时无法推断
julia> Point(1, 2.0)
ERROR: MethodError: no method matching Point(::Int64, ::Float64)

类型参数可以加约束,限制它必须是某个抽象类型的子类型:

实例

# T 必须是 Real 的子类型
julia> struct RPoint{T<:Real}
           x::T
           y::T
       end

julia> RPoint(1, 2)
RPoint{Int64}(1, 2)

julia> RPoint(1, "a")
ERROR: TypeError: in RPoint, in T, expected T<:Real, got a value of type String

参数化类型也常用于限定字段为某种容器:

实例

# 包装一个向量
julia> struct Wrapper{T}
           data::Vector{T}
       end

julia> Wrapper([1, 2, 3])
Wrapper{Int64}([1, 2, 3])

# Union 参数可以表达"可能缺失"的语义,类似其他语言的 Maybe
julia> struct MaybeValue
           value::Union{Int, Nothing}
       end

julia> MaybeValue(10)
MaybeValue(10)

julia> MaybeValue(nothing)
MaybeValue(nothing)

重要概念——类型不变性:Vector{Int} 不是 Vector{Real} 的子类型!即使 Int <: Real 成立。这是因为 Vector{Real} 的内存里放的是指针,而 Vector{Int} 放的是紧凑的整数,两者内存布局完全不同。接受"任意元素类型的向量"应该写成 Vector{<:Real}

实例

julia> Vector{Int} <: Vector{Real}
false

julia> Vector{Int} <: Vector{<:Real}     # 用 <:Real 表达"Real 的子类型"
true

自定义构造函数

可以定义额外的构造函数方法,实现参数校验或提供便捷的创建方式。

外构造函数定义在 struct 外部,最终要调用默认构造函数:

实例

julia> struct Foo
           bar
           baz
       end

# 单参数构造:两个字段取相同值
julia> Foo(x) = Foo(x, x)
Foo

julia> Foo(1)
Foo(1, 1)

内构造函数定义在 struct 内部,可以使用 new 关键字创建实例,适合做参数校验:

实例

julia> struct OrderedPair
           x::Real
           y::Real
           # 内构造函数:校验 x <= y
           OrderedPair(x, y) = x > y ? error("out of order") : new(x, y)
       end

julia> OrderedPair(1, 2)
OrderedPair(1, 2)

julia> OrderedPair(2, 1)
ERROR: out of order

一旦定义了内构造函数,默认构造函数就不再生成,所有实例都必须经过校验逻辑创建,这保证了类型的不变式。


枚举类型

@enum 宏可以快速定义枚举类型:

实例

# 定义枚举,默认值从 0 开始,也可以显式指定
julia> @enum Fruit apple=1 banana orange

实例

julia> apple
apple::Fruit = 1

julia> orange
orange::Fruit = 3

julia> Fruit(2)      # 按值取枚举成员
banana::Fruit = 2

julia> Int(apple)    # 取枚举成员的值
1

类型操作速查

函数 / 运算符描述
typeof(x)查询值 x 的具体类型
isa(x, T)判断 x 是否为 T 类型的实例
T1 <: T2判断 T1 是否为 T2 的子类型
supertype(T)查询 T 的直接父类型
subtypes(T)列出 T 的直接子类型
fieldnames(T)查询结构体的字段名元组
fieldtypes(T)查询结构体的字段类型元组
Union{T1, T2}类型联合:值可以是其中任意一种类型
Union{}空类型(底类型),没有任何实例
Nothing / nothing表示"没有值"的类型 / 唯一实例

实例

julia> fieldnames(Point)
(:x, :y)

julia> fieldtypes(Point)
(Int64, Int64)

julia> isa(1, Int)
true

julia> isa(1, Any)
true

julia> typeof(nothing)
Nothing