Apr 15, 2015
Table of contents:
Last week we looked at using Hashes in Ruby. A Hash is a dictionary of keys and values. This can be useful for passing around structured data that is unknown or could potentially have many key / value pairs.
However it’s sometimes the case that you need to be a little more explicit with structured data.
A Struct is a way to create a very simple class object without the overhead of actually creating a new class.
In today’s tutorial we’re going to be looking at Structs in Ruby, what are they and when you should use them.
A Struct is essentially a very simple class that allows you to encapsulate attributes and accessor methods without having to explicitly define a class.
Often when you are passing around data, a simple Array or Hash will be adequate for your needs.
However, Arrays and Hashes lack the explicit attributes and accessor methods of a class.
You can create a new Struct like this:
Computer = Struct.new(:name, :os)
The new
method on the Struct class accepts a list of symbols that will act as the Struct’s attributes. By calling Struct.new
you will be returned a new Class
that you can save as a constant.
You can then use the Struct as a regular object:
laptop = Computer.new('MacBook', 'OS X')
# => #<struct Computer name="MacBook", os="OS X">
The Struct will have accessor methods for the symbols you passed in on instantiation:
laptop.name
# => "MacBook"
You can also use the square bracket notation to access the attributes:
laptop[:os]
# => "OS X"
You can change the values of the attributes by using the accessor methods or by using square bracket notation:
laptop.name = 'Dell'
laptop[:os] = 'Winblows'
However, if you try to set an attribute that is not defined on the object, you will get an error:
laptop.age = '2 years'
# NoMethodError: undefined method 'age=' for #<struct Computer name="Dell", os="Winblows">
You can return the values of the object as an Array by calling the values
method or the to_a
method:
laptop.values
# => ["Dell", "Winblows"]
laptop.to_a
# => ["Dell", "Winblows"]
You can also return the object as a Hash by calling the to_h
method:
laptop.to_h
# => {:name=>"Dell", :os=>"Winblows"}
Two objects will be the same if they are the same struct subclass and have equal attributes:
other = Computer.new('Dell', 'Winblows')
# => #<struct Computer name="Dell", os="Winblows">
laptop == other
# => true
You can also define methods when you create a new Struct by passing a block:
require 'date'
Person =
Struct.new(:name, :dob) do
def age
Date.today.year - dob
end
end
Person.new('Jess', 1987).age
# => 28
In this example we’ve created a new Person
Struct that has a method age
that can calculate the person’s age from the given date of birth.
So a Struct is like a very simple class definition that can encapsulate attributes and methods.
So when would you use a Struct?
The big benefit of a Struct over a Hash or an Array is the fact that it gives meaning to the data.
For example, imagine you had the following list of coordinates as an Array:
locations = [[40.748817, -73.985428], [40.702565, 73.992537]]
This Array isn’t very explicit and so it’s a bit ambiguous that the values represent longitude and latitudes of locations.
Instead we could use a simple Struct to give meaning to these values:
Location = Struct.new(:longitude, :latitude)
You can now access the attributes of the object using accessor methods, rather than the index of an Array:
location = Location.new(40.748817, -73.985428)
location.longitude
# => 40.748817
You should use a Struct when the data you are working with is closely related. For example, the attributes of a location are closely related and should be encapsulated as an object.
Another benefit of using a Struct over a Hash is that a Struct will require specific attributes, whereas a Hash will accept anything.
For example, imagine we are working with structured data about books in a library. If were were using a Hash we might do something like this:
book = {}
# => {}
book[:tile] = 'Zero to One'
# => "Zero to One"
The Hash doesn’t care that we spelled the title
attribute incorrectly.
If we were using a Struct, we would of got an error:
Book = Struct.new(:title)
book = Book.new
# => #<struct Book title=nil>
book[:tile] = 'Zero to One'
# NameError: no member 'tile' in struct
This is important when you are modelling known things in the domain and you expect an error if the attribute is incorrect.
So we’ve looked at when you would use a Struct, but when would you not use a Struct?
When you define a new Struct, you need to know all of the attributes that you are defining. Once the Struct has been defined, these attributes can’t be changed.
This is perfect for modelling known things in the Domain of your application, however, it’s not very useful if those attributes are likely to change.
On the other hand, if you have data that is only loosely related such as config items or data options, it’s probably best that you don’t try to artificially encapsulate that data as a Struct. In this case it would be better to use a regular Hash.
A Hash is useful for passing options to an method, storing configuration items or working with loosely related data that will need to be mutated.
So you basically have a Hash on one side, a Class on the other side. A Struct sits in the middle of this spectrum.
Ruby also has another similar data structure to Hash and Struct known as an OpenStruct.
An OpenStruct sits in-between the Hash and Struct on the Hash / Struct / Class spectrum:
require 'ostruct'
person = OpenStruct.new
person.name = 'Philip'
person.location = 'UK'
The first thing to notice is that we need to require ostruct
in order to use OpenStructs.
Secondly, when you create a new OpenStruct, you don’t have to define it’s attributes. As you can see in the example above, I can create a new OpenStruct and arbitrarily set it’s attributes.
The benefit of using an OpenStruct over a Hash is if you want to work with something that looks and feels more like an object. In other words, it has accessor methods. However, OpenStructs are not good for performance, and so you will more than likely want to replace them with Structs or Classes once you have your design in place.
The benefit of using a Struct over an OpenStruct is that you can define the attributes of the thing you are modelling and you can also define methods as we saw earlier. This is great when you want to model a known thing of your domain, but either you don’t need the overhead of a class or you haven’t settled on your implementation just yet.
Structs are a very useful aspect of the Ruby language. You will often see them dealing with little niggly bits of code that needs encapsulation but do not warrant the overhead of a Class.
When learning a new programming language it’s important to understand idioms like Structs. Structs are usually used for a specific purpose and so being able to understand and recognise their usage will put you in a better position to understand someone else’s code.
Syntax is usually not the most important thing to learn and remember now that every answer is just a Google search away.