This month I explore what Structs and OpenStructs are and how they compare with classes and hashes.
Why do I care about Struct /OpenStruct
Structs/OpenStructs are data containers you can use to bundle a set of values together in an object. Once you create a Struct/OpenStruct, you can access the attributes as a method.
1
2
3
4
5
6
7
Person = Struct.new(:name, :age)
john = Person.new "john", 30
john.name
jane = OpenStruct.new({name: "Jane", age:"30"})
jane.name
Struct/OpenStructs share some similarities with hashes and classes but have their own benefits.
What is a Struct
Structs are a part of the core Ruby programming language and are a shortcut for creating a class.
1
2
Person = Struct.new(:name, :age)
john = Person.new "john", 30
When you call Struct.new
, you create an anonymous class, which has attributes corresponding to the symbols passed to the instructor. The Struct class above is similar to doing the following in a class.
1
2
3
4
5
6
7
8
9
class Person
attr_accessor :name
attr_accessor :age
def initialize(name, age)
@name = name
@age = age
end
end
Essentially, you create an anonymous class that has a set of variables already defined.
You may be wondering why you can call Person.new
in the example. When you assign an anonymous class to a variable, then the name of the class is set to the variable name. In the example, the name of the anonymous class is set to Person
. After that, you can initialize new instances of this anonymous class by calling Person.new
.
Why not use a class?
Creating a class using a struct will give you some different functionality out of the box compared to creating a plain ruby class.
For one, a struct inherits from Enumerable. This means a struct will respond to methods such as any?
, find
, filter
, map
which iterate over the attributes of the struct. If you wanted similar functionality with a class, you would have to implement it yourself.
Another difference between a struct and a class is that equality is based on the value of the attributes, not the object id. Normally, if you create two objects of the same class, you look at the object id to determine if two objects are equal. However, with structs, by default equality will depend on the attributes.
1
2
3
john = Person.new "john", 30
john2 = Person.new "john", 30
john == john2 // true
However, equality depends on the Structs being instances of the same anonymous class.
1
2
3
4
P1 = Struct.new(:name)
P2 = Struct.new(:name)
P1.new("John") == P2.new("John") // false
Even though P1
and P2
have the same attributes, they are different classes under the hood. This is why P1
and P2
are not equal.
If having equality based on attributes or being able to iterate over attributes is useful, then this is a sign to use Struct over a class.
What is an OpenStruct
Unlike, Struct, OpenStruct is not a part of the core Ruby language. To use OpenStruct, you have to call require 'ostruct'
An OpenStruct is essentially a fancy wrapper around a hash. You create an OpenStruct by passing it a hash and then you can access the attributes of the hash as methods.
1
2
jane = OpenStruct.new({name: "Jane", age:"30"})
jane.age // 30
Unlike a Struct, where attributes are fixed at initialization, you can add new attributes to the OpenStruct after initialization.
1
jane.last_name = "Doe"
💡 This actually creates a new
last_name
method on the open struct instance by usingdefine_singleton_method
.define_singleton_method
allows you to create a new method for an object without adding this method to other instances of the same class. In this case, thelast_name
method is added to thejane
object without adding it to all instances ofOpenStruct
Furthermore, if you try to access an attribute that doesn’t exist on a Struct, you will get a method not defined error. However, for OpenStruct, this will not cause an error but rather return nill
.
1
jane.address // nil
💡 Aside: OpenStruct uses
method_missing
to catch all method calls for attributes that do not exist on the object. This allows it to returnnil
instead of throwing an error.
Finally, two OpenStructs are considered equal if the underlying hashes are the same.
Why not use a hash?
Hashes also allow you to define new key/value pairs after initialization and access keys that do not exist on the hash. If an OpenStruct uses hashes under the hood and behaves similarly to a hash, why not use a hash?
Well, even though an OpenStruct is implemented using a hash, it is not a hash! You don’t have access to methods such as length
, compact
etc that you have for a hash. Use OpenStruct when you need a simple object that responds to a set of methods. For example, if you need to pass it as a param to a method that expects an object and not a hash.
Furthermore, OpenStructs are very slow. They are almost 3 times slower than Structs. For this reason, if performance is a huge concern, you’ll want to use a struct or a hash.
Conclusion
The purpose of learning about Structs and OpenStructs was to learn more about how to handle data in ruby. While you may not be using Structs all the time in my day-to-day coding, it’s good to know what options are available to you so that you know you are picking the best tool for the job.