2.1 Julia类型系统


2.1 类型系统

Julia 类型系统的一句话模型:一棵抽象类型树 + 挂在叶子上的具体类型,值属于具体类型,变量不锁定类型。类型标注不是给人看的,是给编译器用来生成机器码的。

Julia 类型层次示意图

Julia 类型层次示意图

动手验证这棵树,REPL 里敲:

isa(3, Real) # true:Int 是 Real 的后代 supertype(Int64) # Signed supertype(Signed) # Integer Int64 <: Real # true,<: 读作"是…的子类型" subtypes(AbstractFloat) # 4 个叶子

自己建类型:struct 与参数化

抽象类型划分类别,具体类型装数据。给二维点建模:

abstract type Shape end struct Point x::Float64 y::Float64 end p = Point(1.0, 2.0) p.x # 1.0

struct 默认不可变——这对性能是好事(可以放栈上、编译器可自由优化)。需要修改字段时用 mutable struct。更进一步,参数化类型让容器和函数做到"类型泛化但性能不损失":

struct Point{T} x::T y::T end Point(1, 2) # Point{Int64},整数版 Point(1.0, 2.0) # Point{Float64},浮点版

Vector{Int64} 就是 Array{Int64,1} 的别名——Julia 的数组本身就是参数化类型,每个数组的元素类型在创建时就定死,这就是它能生成紧凑机器码的原因。

💡 关键直觉:Julia 没有类、没有继承方法。想复用行为,用"组合 + 多重派发"(下一节),别找 class 关键字,找不到的。

类型稳定性:快的分水岭

一个函数是"类型稳定"的,指任意输入其返回类型都能被推断。反例:

function unstable(x) if x > 0 return 1 else return 1.0 # 有时 Int 有时 Float64 end end

编译器只能按"抽象类型"处理返回值,落到慢路径。诊断工具:

@code_warntype unstable(1) # 输出里黄色/红色标注即不稳定点

修法很多:统一返回类型、拆成两个函数、或显式 convert

⚠️ 常见坑:全局变量天然类型不稳定。脚本顶层的 n = 10^7 在函数里引用时,每次访问都要动态查类型。要么 const n = 10^7,要么把逻辑包进函数——这也是 Julia 惯例"一切写在函数里"的真实原因。

类型转换与提升:一段容易写错的代码

convertpromote 是类型系统的日常工具,也最容易和别的语言习惯打架。看一段对照实验:

convert(Float64, 3) # 3.0,无损转换没问题 convert(Int64, 3.7) # 报 InexactError,拒绝静默截断 round(Int64, 3.7) # 4,先取整再转换才是正路 promote(1, 2.5) # (1.0, 2.5),把一组数提到共同类型 1 + 2.5 # 3.5,运算符内部自动走 promote

Julia 在"可能丢信息"的转换上一律报错,这和 C 语言隐式截断的传统完全相反。好处是 bug 在发生点就炸出来,而不是藏在第几千行输出的一个小数位里。混合精度计算出问题时,先想 promote_type(Int64, Float64) 会给什么,多数疑惑都能自己解开。另外一个细节:Float64 <: Real 成立,但 Vector{Float64} <: Vector{Real} 不成立——参数化类型默认不变,想要这种"元素是 Real 即可"的语义得写成 Vector{<:Real}。这一条在读函数签名时天天遇到,值得单独记牢。

union 与 nothing:可选值的类型安全写法

表示"可能没有值",Julia 用 nothing 而不是 null。查字典的经典写法:

function lookup_or(d, key, default) v = get(d, key, nothing) v === nothing ? default : v end d = Dict("a" => 1) lookup_or(d, "a", 0) # 1 lookup_or(d, "b", 0) # 0

判断是否为 nothing 用三等号 ===,它比较"是不是同一个对象",比 == 更快也更严格,是配 nothing 的标准姿势。当一个函数的返回类型是 Union{Nothing, Int} 这种小规模联合时,编译器还能生成无需堆分配的分支代码,所以这种写法在生态里随处可见,读标准库源码时不会被它绊住。反过来,如果发现某个容器被推断成 Vector{Any},多半是往里 push 过 nothing 或混了类型,这时候该停下来重新设计数据流,而不是放任它拖慢整段程序。

案例:用类型建模一个实验记录,走完"背景到变式"

类型系统的价值在真实数据建模里才看得清。背景:实验室每天记录若干次测量,希望既能装整数计数又能装浮点读数,还要带上仪器名。第一步先写朴素版本:

struct Reading{T} device::String value::T end today = [Reading("天平", 102), Reading("温度计", 36.6)] # 数组元素类型被提升为 Vector{Reading} 的联合,遍历时会有派发开销

操作层面,给这批数据求均值需要先理解一个坑:Reading 装进同一数组后,Int 版和 Float64 版是两个不同具体类型,数组退化成联合类型。结果就是遍历变慢、编译器不敢放开优化。解读之后得到改法——在录入时就统一成 Float64

uniform = Reading{Float64}[] # 显式声明元素类型 push!(uniform, Reading("天平", 102.0)) push!(uniform, Reading("温度计", 36.6)) sum(r.value for r in uniform) / length(uniform) # 69.3,全程类型稳定

变式:如果测量值还可能是缺失(仪器离线),字段类型改成 Union{T, Missing},创建时 Reading("电压表", missing),统计前用 skipmissing 过滤。这个小案例把本节三块知识——参数化、类型稳定、union——串成了一条真实工作流,遇到"数组越用越慢"时按这条链路排查即可。

概念辨析:convert、构造与 promote 规则

初学者常把三件事混成一团,分开记。convert(T, x) 是"语义等价下的换容器",整数变浮点可以,浮点变整数拒绝;T(x) 是类型构造,走的是该类型的构造函数,Int64(3.7) 同样报错但 Float64(3) 成功;promote 则负责把一组值拉到"共同语言",它查的是类型之间的提升规则表。日常写码中,函数签名里收 Real 然后内部 float(x) 归一精度,是比到处 convert 更省心的模式。理解这三者分工后,读标准库中 + 的实现(内部一行 promote(x, y) 再调内核函数)会突然变得透明。

收束

  • 类型树:Any 为根,抽象类型只组织结构,具体类型才存数据;
  • struct 不可变优先,mutable struct 按需;参数化 {T} 一份代码多类型专用编译;
  • @code_warntype 是你的听诊器,黄色就是病灶;
  • 性能口诀:类型稳定 + 避免全局变量,其余优化都是锦上添花。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U