Working with Struct – one of the underappreciated core classes
The Struct
class is one of the underappreciated Ruby core classes. It allows you to create classes with one or more fields, with accessors automatically created for each field. So, say you have the following:
class Artist attr_accessor :name, :albums def initialize(name, albums) @name = name @albums = albums end end
Instead of that, you can write a small amount of Ruby code, and have the initializer and accessor automatically created:
Artist = Struct.new(:name, :albums)
In general, a Struct
class is a little lighter on memory than a regular class, but has slower accessor methods. Struct
used to be faster in terms of both initialization and reader methods in older versions of Ruby, but regular classes and attr_accessor
methods have gotten faster at a greater rate than Struct
has. Therefore, for maximum performance, you may want to consider using regular classes and attr_accessor
methods instead of Struct
classes.
One of the more interesting aspects of Struct
is how it works internally. For example, unlike the new
method for most other classes, Struct.new
does not return a Struct
instance; it returns a Struct
subclass:
Struct.new(:a, :b).class # => Class
However, the new
method on the subclass creates instances of the subclass; it doesn't create future subclasses. Additionally, if you provide a string and not a symbol as the first argument, Struct
will automatically create the class using that name nested under its own namespace:
Struct.new('A', :a, :b).new(1, 2).class # => Struct::A
A simplified version of the default Struct.new
method is similar to the following. This example is a bit larger, so we'll break it into sections. If a string is given as the first argument, it is used to set the class in the namespace of the receiver; otherwise, it is added to the list of fields:
def Struct.new(name, *fields) unless name.is_a?(String) fields.unshift(name) name = nil end
Next, a subclass is created. If a class name was given, it is set as a constant in the current namespace:
subclass = Class.new(self) if name const_set(name, subclass) end
Then, some internal code is run to set up the storage for the members of the subclass. Then, the new
, allocate
, []
, members
, and inspect
singleton methods are defined on the subclass. Finally, some internal code is run to set up accessor instance methods for each member of the subclass:
# Internal magic to setup fields/storage for subclass def subclass.new(*values) obj = allocate obj.initialize(*values) obj end # Similar for allocate, [], members, inspect # Internal magic to setup accessor instance methods subclass end
Interestingly, you can still create Struct
subclasses the normal way:
class SubStruct < Struct end
Struct
subclasses created via the normal way operate like Struct
itself, not like Struct
subclasses created via Struct.new
. You can then call new
on the Struct
subclass to create a subclass of that subclass, but the setup is similar to a Struct
subclass created via Struct.new
:
SubStruct.new('A', :a, :b).new(1, 2).class # => SubStruct::A
In general, Struct
is good for creating simple classes that are designed for storing data. One issue with Struct
is that the design encourages the use of mutable data and discourages a functional approach, by defaulting to creating setter methods for every member. However, it is possible to easily force the use of immutable structs by freezing the object in initialize
:
A = Struct.new(:a, :b) do def initialize(...) super freeze end end
There have been feature requests submitted on the Ruby issue tracker to create immutable Struct
subclasses using a keyword argument to Struct.new
or via the addition of a separate Struct::Value
class. However, as of Ruby 3, neither feature request has been accepted. It is possible that a future version of Ruby will include them, but in the meantime, freezing the receiver in initialize
is the best approach.