Julia 结构体与类型系统
结构体(struct)是把多个相关数据组合成一个整体的自定义类型,类似 C 语言的 struct 或 Python 的类(但不包含方法)。
在 Julia 中,数据(结构体)和方法(函数)是分离的:函数不属于任何类型,而是通过多重派发根据参数类型选择实现,这一点会在方法与多重派发章节详细讲解。
定义结构体
使用 struct 关键字定义结构体,字段可以带类型标注,不带则默认为 Any:
实例
x::Int # 带类型标注的字段
y::Int
label # 不带标注,类型为 Any
end
把类型名当函数调用即可创建实例(类型名本身就是构造函数):
实例
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 定义的结构体是不可变的,创建后字段不能修改:
实例
ERROR: setproperty!: immutable struct of type Point cannot be changed
不可变有诸多好处:
- 更高效:编译器可以把小对象完全放在寄存器或栈上,甚至完全省掉内存分配。
- 更安全:对象的值一旦创建就不会被意外改动。
- 字段相同的两个实例完全等价(=== 为 true)。
实例
x::Int
y::Int
end
julia> ImmutablePoint(1, 2) === ImmutablePoint(1, 2) # 值相同即同一个
true
最佳实践:在 Julia 中应优先使用不可变结构体,只有在确实需要修改对象状态时才用 mutable struct。
可变结构体(mutable struct)
用 mutable struct 定义的类型,实例的字段可以被修改:
实例
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 === b # 不同的对象
false
julia> a == b # 默认按字段比较?不,默认 == 等同于 ===
false
如果希望 == 按字段比较,可以自己定义 == 方法:
实例
julia> a == b
true
const 字段
Julia 1.8 起,可变结构体的个别字段可以用 const 标注为不可修改:
实例
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() 列出直接子类型:
实例
true
julia> Int <: AbstractFloat
false
julia> supertype(Int64)
Signed
julia> Any <: Any # Any 是所有类型的根
true
参数化类型(泛型)
参数化类型相当于其他语言中的泛型,用花括号声明类型参数:
实例
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)
类型参数可以加约束,限制它必须是某个抽象类型的子类型:
实例
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}。
实例
false
julia> Vector{Int} <: Vector{<:Real} # 用 <:Real 表达"Real 的子类型"
true
自定义构造函数
可以定义额外的构造函数方法,实现参数校验或提供便捷的创建方式。
外构造函数定义在 struct 外部,最终要调用默认构造函数:
实例
bar
baz
end
# 单参数构造:两个字段取相同值
julia> Foo(x) = Foo(x, x)
Foo
julia> Foo(1)
Foo(1, 1)
内构造函数定义在 struct 内部,可以使用 new 关键字创建实例,适合做参数校验:
实例
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 宏可以快速定义枚举类型:
实例
julia> @enum Fruit apple=1 banana orange
实例
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 | 表示"没有值"的类型 / 唯一实例 |
实例
(:x, :y)
julia> fieldtypes(Point)
(Int64, Int64)
julia> isa(1, Int)
true
julia> isa(1, Any)
true
julia> typeof(nothing)
Nothing
