You would use an “inner constructor” to ensure c always has the right value. And an “outer constructor” to create a new instance where c is given the correct value. See the docs here.
julia> struct MyNumbers
a::Int64
b::Int64
c::Int64
MyNumbers(a, b, c) = begin
@assert c == a + b
new(a, b, c)
end
end;
julia> MyNumbers(a, b) = MyNumbers(a, b, a + b);
julia> MyNumbers(1, 2, 5)
ERROR: AssertionError: c == a + b
Stacktrace:
[1] MyNumbers(a::Int64, b::Int64, c::Int64)
@ Main ./REPL[1]:6
[2] top-level scope
@ REPL[3]:1
julia> MyNumbers(1, 2, 3)
MyNumbers(1, 2, 3)
julia> MyNumbers(1, 2)
MyNumbers(1, 2, 3)
If your extra field requires some expensive calculation, then you can precompute it using a constructor, as people have suggested.
However, for simple calculations, the getproperty approach suggested above is nicer because it doesn’t actually use the extra memory and won’t become wrong if you change a or b (although in your example, MyNumbers is immutable so this won’t happen).
getproperty can sometimes be a little tedious to define, though. So my preferred approach (and the most flexible) is to access objects through accessor functions. Personally, I almost never use direct field access outside of defining an accessor. For example: