I have seen codebases using Structs to wrap around attributes and behavior inside a class. What is the difference between a Ruby Class and a Struct? And when should one be used
To add to the other answers, there are some things you can not do with a Struct, and some than you can.
For example, you can not create a Struct with no arguments:
Bar = Struct.new
=> ArgumentError: wrong number of arguments (given 0, expected 1+)
Bar = Struct.new(:bar)
bar = Bar.new(nil)
bar.class
=> Bar
However, a class will let you do that:
class Foo; end
foo = Foo.new
foo.class
=> Foo
You can not set a default value for Struct arguments:
Bar = Struct.new(bar: 'default')
=> ArgumentError: unknown keyword: bar
Bar = Struct.new(bar = 'default')
=> NameError: identifier default needs to be constant
But you can do it with a class, either passing a hash, were the arguments can be in any order or even missing:
class Bar
attr_reader :bar, :rab
def initialize(bar: 'default', rab:)
@bar = bar
@rab = rab
end
end
bar = Bar.new(rab: 'mandatory')
bar.rab
=> 'mandatory'
bar.bar
=> 'default'
bar = Bar.new(rab: 'mandatory', bar: 'custom_value')
bar.rab
=> 'mandatory'
bar.bar
=> 'custom_value'
or passing the values directly, were the arguments should be given in the same order, with the defaulted ones always at the end:
class Bar
attr_reader :rab, :bar
def initialize(rab, bar = 'default')
@rab = rab
@bar = bar
end
end
bar = Bar.new('mandatory')
bar.rab
=> 'mandatory'
bar.bar
=> 'default'
bar = Bar.new('mandatory', 'custom_value')
bar.rab
=> 'mandatory'
bar.bar
=> 'custom_value'
You can not do any of that with Structs, unless you set default values for your arguments in this super verbose way:
A = Struct.new(:a, :b, :c) do
def initialize(a:, b: 2, c: 3)
super(a, b, c)
end
end
(example taken from this answer)
You can define methods in a Struct:
Foo = Struct.new(:foo) do
def method(argument)
# do something with argument
end
end
end
Structs can be useful to create data objects, like the point example mentioned in one of the answers.
I sometimes use them to create fakes and mocks in tests in a simple way. Sometimes RSpec allow(foo).to receive(:blah)
etc. can get a bit too verbose and using a Struct is much simple.
Struct is a Ruby shorthand for creating Classes. Using Struct where applicable simplifies your code. There is a good discussion of this at https://www.rubytapas.com/2012/11/07/episode-020-struct/
From the Struct docs:
A Struct is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class.
The Struct class generates new subclasses that hold a set of members and their values. For each member a reader and writer method is created similar to Module#attr_accessor.
So, if I want a Person
class that I can access a name attribute (read and write), I either do it by declaring a class:
class Person
attr_accessor :name
def initalize(name)
@name = name
end
end
or using Struct:
Person = Struct.new(:name)
In both cases I can run the following code:
person = Person.new
person.name = "Name"
#or Person.new("Name")
puts person.name
When use it?
As the description states we use Structs when we need a group of accessible attributes without having to write an explicit class.
For example I want a point variable to hold X and Y values:
point = Struct.new(:x, :y).new(20,30)
point.x #=> 20
Some more examples:
I'd like to to @sam_forgot suggested benchmark. The comparison is not very fair. Both class and struct, these days, support keyword arguments. Using keyword arguments on each has opposite effects, as you can see from my example struct's with keyword arguments performance is not that dramatically different from the class.
require 'benchmark'
REP=1000000
SUser = Struct.new(:name, :age)
SUserK = Struct.new(:name, :age, keyword_init: true)
DATA = { name: "Harry", age: 75 }
DATA2 = DATA.values
class CUser
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
end
class CUserK
attr_accessor :name, :age
def initialize(name:, age:)
@name = name
@age = age
end
end
Benchmark.bmbm do |x|
x.report 'Struct create and access, without keyword arguments' do
REP.times do
user = SUser.new(DATA)
"#{user.name} - #{user.age}"
end
end
x.report 'Struct create and access, with keyword arguments' do
REP.times do
user = SUserK.new(**DATA)
"#{user.name} - #{user.age}"
end
end
x.report 'Class create and access, without keyword arguments' do
REP.times do
user = CUser.new(*DATA2)
"#{user.name} - #{user.age}"
end
end
x.report 'Class create and access, with keyword arguments' do
REP.times do
user = CUserK.new(DATA)
"#{user.name} - #{user.age}"
end
end
end
Rehearsal ---------------------------------------------------------------------------------------
Struct create and access, without keyword arguments 3.484609 0.011974 3.496583 ( 3.564523)
Struct create and access, with keyword arguments 0.965959 0.005543 0.971502 ( 1.007738)
Class create and access, without keyword arguments 0.624603 0.003999 0.628602 ( 0.660931)
Class create and access, with keyword arguments 0.901494 0.004926 0.906420 ( 0.952149)
------------------------------------------------------------------------------ total: 6.003107sec
user system total real
Struct create and access, without keyword arguments 3.300488 0.010372 3.310860 ( 3.339511)
Struct create and access, with keyword arguments 0.876742 0.004354 0.881096 ( 0.903551)
Class create and access, without keyword arguments 0.553393 0.003962 0.557355 ( 0.568985)
Class create and access, with keyword arguments 0.831672 0.004811 0.836483 ( 0.850224)
There is quite a big practical performance difference, example behavior in ruby 2.6.3p62:
user system total real
Struct create and access 3.052825 0.005204 3.058029 (3.066316)
Class create and access 0.738605 0.001467 0.740072 (0.743738)
Example code:
require 'benchmark'
REP=1000000
SUser = Struct.new(:name, :age)
DATA = { name: "Harry", age: 75 }
class User
attr_accessor :name, :age
def initialize(name:, age:)
@name = name
@age = age
end
end
Benchmark.bm 20 do |x|
x.report 'Struct create and access' do
REP.times do
user = SUser.new(DATA)
"#{user.name} - #{user.age}"
end
end
x.report 'Class create and access' do
REP.times do
user = User.new(DATA)
"#{user.name} - #{user.age}"
end
end
end