Julia 的 struct 里有没有默认的继承操作?

接着上一个帖子的操作: 运算符[]能否被重载?
定义了某波函数类 WaveFunc,实际存数据的是里面的成员变量 psi::Array{Complex, 1}
只是为了让 WaveFunc 对象在外界用起来像数组一样舒服,结果要把几乎所有的数组操作都重载一遍……
Julia 肯定是不让 WaveFunc 继承 Array,但能不能指定里面的某变量,比如 psi,让 WaveFunc 默认行为都和 psi 绑定,比如给 psi 声明成 super 或 parent 啥的,这样调用 ψ[i] 就等同调用 ψ.psi[i],达到变相继承的目的?


using Printf

struct WaveFunc
    psi::Array{ComplexF64, 1}
    function WaveFunc(n)
        new(Array{ComplexF64}(undef, n))
    end    
end

# 下面这一堆的重载,实际都是 psi 这个数组的默认行为……
Base.getindex(ψ::WaveFunc, i::Int64) = ψ.psi[i]
Base.getindex(ψ::WaveFunc, r::UnitRange{Int64}) = ψ.psi[r]
Base.setindex!(ψ::WaveFunc, v::ComplexF64, i::Int64) = (ψ.psi[i] = v)
Base.setindex!(ψ::WaveFunc, v::Array{ComplexF64,1}, r::UnitRange{Int64}) = (ψ.psi[r] = v)
Base.size(ψ::WaveFunc) = size(ψ.psi)
Base.length(ψ::WaveFunc) = length(ψ.psi)
Base.copyto!(ψ::WaveFunc, rhs) = (copyto!(ψ.psi, rhs); ψ)
Base.iterate(ψ::WaveFunc, args...) = iterate(ψ.psi, args...)
Base.axes(ψ::WaveFunc) = axes(ψ.psi)

# 多重派发非常好,就是上面重复的重载比较蛋疼
function dump_to_file(ψ::WaveFunc, filename::String)
    open(filename, "w") do file
        for psi in ψ
            @printf file "%le %le\n" real(psi) imag(psi)
        end
    end
end

为啥不让?

我看这里本质上还是 Array 那就取个别名不行么?

const WaveFunc = Vector{ComplexF64}

取个别名的话,那本质上就是一个东西,多重派发认不出差别

const WaveFunc = Vector{ComplexF64}

function write(psi::WaveFunc)
    println("write WaveFunc")
end

function write(psi::Vector{ComplexF64})
    println("write vector")
end

function main()
    x = WaveFunc()
    write(x)
end

main()

你觉得这个会出什么结果?

这样写后面的定义会覆盖前面的。

对就是一样的,你实际上的结构就一样。和 Matrix 一个道理


julia 只通过多重派发继承方法。

所以还可以

多维用 AbstractArray{T}

julia> struct WaveFunc{T} <: AbstractVector{T} end

julia> WaveFunc(n) = Vector{ComplexF64}(undef, n)
WaveFunc

julia> WaveFunc(1)
1-element Array{Complex{Float64},1}:
 1.611605853e-315 + 7.579948e-316im

julia> WaveFunc
WaveFunc

julia> WaveFunc |> typeof
UnionAll

julia> WaveFunc{ComplexF64}
WaveFunc{Complex{Float64}}

julia> WaveFunc{ComplexF64} == Vector{ComplexF64}
false

julia> f(w::WaveFunc) = println("write WaveFunc")
f (generic function with 1 method)

julia> f(w::Vector{ComplexF64}) = println("write vector")
f (generic function with 2 methods)

使用

julia> wf = WaveFunc(10)
10-element Array{Complex{Float64},1}:
 1.788715027e-315 + 1.788719296e-315im
 1.788719454e-315 + 1.78883044e-315im
 1.788725304e-315 + 1.78872546e-315im
  1.78883123e-315 + 1.788768307e-315im
 1.788831547e-315 + 1.79979443e-315im
  1.79981451e-315 + 0.0im
              0.0 + 0.0im
              0.0 + 0.0im
              0.0 + 0.0im
              0.0 + 0.0im

julia> size(wf)
(10,)

julia> length(wf)
10

julia> wf[1]
1.788715027e-315 + 1.788719296e-315im

julia> wf[1] = 10im
0 + 10im

julia> wf[1]
0.0 + 10.0im

julia> axes(wf)
(Base.OneTo(10),)

julia> [c.re for c = wf]
10-element Array{Float64,1}:
 0.0
 1.788719454e-315
 1.788725304e-315
 1.78883123e-315
 1.788831547e-315
 1.79981451e-315
 0.0
 0.0
 0.0
 0.0

这种写法,WaveFunc 对象还是 Array,而不是自己定义的新类型

struct WaveFunc{T} <: AbstractVector{T}
end

WaveFunc(n::Int64) = Vector{ComplexF64}(undef, n)

# 自定义针对 WaveFunc 类型的 write 函数
function write(psi::WaveFunc)
    println("write WaveFunc")
end

function main()
    psi::WaveFunc = WaveFunc(10)
    # 以下会报错,认为 psi 是 Array{ComplexF64,1} (父类型),而非自定义的 WaveFunc 类型
    write(psi) # 不会调用上面定义的 write(psi::WaveFunc)
end

main()

翻了下文档。

https://docs.juliacn.com/latest/manual/interfaces/#man-interface-array-1

size, getindex, setindex! 还是得手工实现,继承一下 <: AbstractArray 的好处在于在实现了这些函数的基础上无需实现其他函数就能使用。

所以并没有什么好的办法。

你说的文档上的情况,是确实想改变默认方法中的行为。
而我们这里,其实就是想直接用默认行为,对数组的映射方式都没变。所以这样写就真的很麻烦很冗余。

当然,从用户角度上确实没办法,连 DifferentialEquation 下面的底层库好像都是这么处理的

抽象类型继承给我的感觉是,
不是真的想让用户去衍生出一个可以自定义的类型,
而是在已有类型的架构上,改变原有方法的行为方式
但没办法在原架构上额外添加方法

其实不用继承的概念挺好
我也更喜欢组合的思路
例如

struct WaveFunc
    psi::Array{ComplexF64,1} 
    var1::Int # some other members
    var2::Int #
    WaveFunc(n::Int) = (new(Array{ComplexF64,1}(undef, n), 1, 2))
end

wf = WaveFunc(10)
println(wf.psi[4])

WaveFunc 认为是一个 Array{ComplexF64,1} 与两个 Int 的组合,简单又清楚

但它的一个问题是,当用到 WaveFunc 的核心数据 psi 时,不得不写成 wf.psi
这还只是一层组合。如果把 WaveFunc 类型作为一个成员,再合成别的新类型时,用的时候会变成 some_new_obj.wf.psi 之类

如果开发者能考虑一个语法糖,将定义的类型绑定到一个核心成员(比如用 super、key 之类的关键字),当对类型作用时,如果找不到显式定义的变量或方法,则会找对核心成员的默认操作,比如这里的 psi 对应的 Array 类型。
在使用的时候,遇上 wf[4],如果用户没有设置 getindex,那就默认等价于 wf.psi[4],这样话会方便很多,实际上这也相当于用组合实现了继承,与 Julia 默认思路不违背。