Instead of defining a type with
class you can do so with
struct Point property x, y def initialize(@x : Int32, @y : Int32) end end
Structs inherit from Value so they are allocated on the stack and passed by value: when passed to methods, returned from methods or assigned to variables, a copy of the value is actually passed (while classes inherit from Reference, are allocated on the heap and passed by reference).
Therefore structs are mostly useful for immutable data types and/or stateless wrappers of other types, usually for performance reasons to avoid lots of small memory allocations when passing small copies might be more efficient (for more details, see the performance guide).
Mutable structs are still allowed, but you should be careful when writing code involving mutability if you want to avoid surprises that are described below.
Passing by value¶
A struct is always passed by value, even when you return
self from the method of that struct:
struct Counter def initialize(@count : Int32) end def plus @count += 1 self end end counter = Counter.new(0) counter.plus.plus # => Counter(@count=2) puts counter # => Counter(@count=1)
Notice that the chained calls of
plus return the expected result, but only the first call to it modifies the variable
counter, as the second call operates on the copy of the struct passed to it from the first call, and this copy is discarded after the expression is executed.
You should also be careful when working on mutable types inside of the struct:
class Klass property array = ["str"] end struct Strukt property array = ["str"] end def modify(object) object.array << "foo" object.array = ["new"] object.array << "bar" end klass = Klass.new puts modify(klass) # => ["new", "bar"] puts klass.array # => ["new", "bar"] strukt = Strukt.new puts modify(strukt) # => ["new", "bar"] puts strukt.array # => ["str", "foo"]
What happens with the
Arrayis passed by reference, so the reference to
["str"]is stored in the property of
struktis passed to
modify, a copy of the
struktis passed with the reference to array inside it
- the array referenced by
arrayis modified (element inside it is added) by
object.array << "foo"
- this is also reflected in the original
struktas it holds reference to the same array
object.array = ["new"]replaces the reference in the copy of
struktwith the reference to the new array
object.array << "bar"appends to this newly created array
modifyreturns the reference to this new array and its content is printed
- the reference to this new array was held only in the copy of
strukt, but not in the original, so that's why the original
struktonly retained the result of the first statement, but not of the other two statements
Klass is a class, so it is passed by reference to
object.array = ["new"] saves the reference to the newly created array in the original
klass object, not in the copy as it was with the
- A struct implicitly inherits from Struct, which inherits from Value. A class implicitly inherits from Reference.
- A struct cannot inherit from a non-abstract struct.
The second point has a reason to it: a struct has a very well defined memory layout. For example, the above
Point struct occupies 8 bytes. If you have an array of points the points are embedded inside the array's buffer:
# The array's buffer will have 8 bytes dedicated to each Point ary =  of Point
Point is inherited, an array of such type should also account for the fact that other types can be inside it, so the size of each element should grow to accommodate that. That is certainly unexpected. So, non-abstract structs can't be inherited from. Abstract structs, on the other hand, will have descendants, so it is expected that an array of them will account for the possibility of having multiple types inside it.
A struct can also include modules and can be generic, just like a class.
The Crystal Standard Library provides the
record macro. It simplifies the definition of basic struct types with an initializer and some helper methods.
record Point, x : Int32, y : Int32 Point.new 1, 2 # => #<Point(@x=1, @y=2)>
record macro expands to the following struct definition:
struct Point getter x : Int32 getter y : Int32 def initialize(@x : Int32, @y : Int32) end def copy_with(x _x = @x, y _y = @y) self.class.new(_x, _y) end def clone self.class.new(@x.clone, @y.clone) end end