Structs, mutable state and constructors
Define composite types, choose between immutable and mutable, add validating constructors, and use type parameters for fast, specialised code.
Defining structs
struct Point
x::Float64
y::Float64
end
# immutable: no method error, construction is one error
p = Point(1.0, 2.0)
# p.x = 3.0 # ERROR: setfield!: immutable struct of type Point cannot be changed
mutable struct Counter
value::Int
end
c = Counter(0)
c.value += 1 # fine, the field is mutable
# parametric struct: the element type is part of the type
struct Stack{T}
items::Vector{T}
end
Stack{Int}([]) # concrete, fast
Stack([1, 2, 3]) # the type parameter is inferred| Choice | Consequence |
|---|---|
struct | Immutable; safe to share, often stack allocated |
mutable struct | Fields assignable; heap allocated, values can change under you |
const field | In a mutable struct, fixes the field type for specialisation |
struct{Foo} | Parametric; one concrete type per parameter value |
Base.@kwdef | Generates a keyword constructor with defaults |
Constructors and validation
struct Interval
lo::Float64
hi::Float64
function Interval(lo::Float64, hi::Float64)
lo <= hi || throw(ArgumentError("lower bound must not exceed upper bound"))
new(lo, hi)
end
end
# an outer constructor with defaults, delegating to the inner one
Interval(hi::Float64) = Interval(0.0, hi)
Base.@kwdef struct Config
host::String = "127.0.0.1"
port::Int = 8000
retries::Int = 3
end
Config(port = 9000) # everything else takes its defaultAn inner constructor runs on every construction path, so it is the right place for invariants. An outer constructor cannot bypass the inner one, and that is the point: no code can create an object that violates your rules.
Field types and allocation
# BAD: an abstract field type means the compiler cannot know what is inside
struct BadBox
value::Any
end
# GOOD: parametric, so the concrete type travels with the struct
struct Box{T}
value::T
end
struct Pair2
a::Float64
b::Float64
end
isbitstype(Pair2) # true: no pointers, can live on the stack
isbitstype(Box{Int}) # true
isbitstype(Box{String}) # false: a String holds a pointer
@allocated Box(1.0)
@allocated BadBox(1.0)💡
A field declared as an abstract type such as
Any, Number or AbstractString makes every access on it a dynamic dispatch. Use a type parameter instead: struct Box{T} keeps the concrete type in the type of the struct, and the compiler specialises on it.FAQ
Why can I not change a field of my struct?
You declared it with
struct, which is immutable. Either declare it mutable struct, or return a new instance with Base.setproperty style reconstruction, which is usually the cleaner design.What does @kwdef actually generate?
A keyword-argument constructor plus a default for each field that has one. It is a macro convenience: the struct itself is unchanged, and the generated constructor still runs alongside the positional one.
Related
Syntax and the type system Arrays and broadcasting in depth
Last refreshed 2026-09-18.