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.