Problem in pushing a value to an element of struct in Julia

Viewed 197

Say I have a struct:

mutable struct DataHolder
    data1::Vector{Float64}
    data2::Vector{Float64}
    function DataHolder()
        emp = Float64[]
        new(emp, emp)
    end
end
d = DataHolder()

When I try to push a value to only one element of struct d by doing:

push!(d.data1, 1.0)

the value is pushed not only d.data1 but also d.data2. Indeed, the REPL says

julia> d
DataHolder([1.0], [1.0])

How can I push a value to only one element of the struct??

2 Answers

Your problem is not in push!, but rather in the inner constructor of DataHolder. Specifically:

emp = Float64[]
new(emp, emp)

This code pattern means that the fields of the new DataHolder both point toward the same array (in memory). So if you mutate one of them (say, via push!), you also mutate the other.

You could instead replace those two lines with:

new(Float64[], Float64[])

to get the behaviour you want.

More generally, although it is not forbidden, your use of the inner constructor is a bit odd. Typically, inner constructors should have a method signature corresponding exactly to the fields of your struct, and the inner constructor itself is typically only used to provide a set of universal tests that any new DataHolder should undergo.

Personally I would re-write your code as follows:

mutable struct DataHolder
    data1::Vector{Float64}
    data2::Vector{Float64}
    function DataHolder(data1::Vector{Float64}, data2::Vector{Float64})
        #Any tests on data1 and data2 go here
        new(data1, data2)
    end
end
DataHolder() = DataHolder(Float64[], Float64[])

If you don't need to do any universal tests on DataHolder, then delete the inner constructor entirely.

Final food for thought: Does DataHolder really need to be mutable? If you only want to be able to mutate the arrays in data1 and data2, then DataHolder itself does not need to be mutable, because those arrays are already mutable. You only need DataHolder to be mutable if you plan to completely re-allocate the values in those fields, e.g. an operation of the form dh.data1 = [2.0].

UPDATE AFTER COMMENT: Personally I don't see anything wrong with DataHolder() = DataHolder(Float64[], ..., Float64[]). It's just one line of code and you never have to think about it again. Alternatively, you could do:

DataHolder() = DataHolder([ Float64[] for n = 1:10 ]...)

which just splats an a vector of empty vectors into the constructor arguments.

@ColinTBowers has answered your question. Here is a very simple and a more general implementation:

struct DataHolder  # add mutable if you really need it
    data1::Vector{Float64}
    data2::Vector{Float64}
end
DataHolder() = DataHolder([], [])

Perhaps you'd like to allow other types than Float64 (because why wouldn't you?!):

struct DataHolder{T}
    data1::Vector{T}
    data2::Vector{T}
end
DataHolder{T}() where {T} = DataHolder{T}([], [])
DataHolder() = DataHolder{Float64}()  # now `Float64` is the default type.

Now you can do this:

julia> DataHolder{Rational}()
DataHold{Rational}(Rational[], Rational[])

julia> DataHolder()
DataHold{Float64}(Float64[], Float64[])
Related