cattr_accessor
cattr_accessor(*syms, &blk)Defines both class and instance accessors for class attributes.
class Person cattr_accessor :hair_colors end Person.hair_colors = [:brown, :black, :blonde, :red] Person.hair_colors # => [:brown, :black, :blonde, :red] Person.new.hair_colors # => [:brown, :black, :blonde, :red]
If a subclass changes the value then that would also change the value for parent class. Similarly if parent class changes the value then that would change the value of subclasses too.
class Male < Person end Male.hair_colors << :blue Person.hair_colors # => [:brown, :black, :blonde, :red, :blue]
To opt out of the instance writer method, pass :instance_writer => false. To opt out of the instance reader method, pass :instance_reader => false.
class Person cattr_accessor :hair_colors, :instance_writer => false, :instance_reader => false end Person.new.hair_colors = [:brown] # => NoMethodError Person.new.hair_colors # => NoMethodError
Or pass :instance_accessor => false, to opt out both instance methods.
class Person cattr_accessor :hair_colors, :instance_accessor => false end Person.new.hair_colors = [:brown] # => NoMethodError Person.new.hair_colors # => NoMethodError
Also you can pass a block to set up the attribute with a default value.
class Person cattr_accessor :hair_colors do [:brown, :black, :blonde, :red] end end Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red]
3Notes
Important note
It has been said that “it can be compared to, but isn’t the same thing as”:
class Bar
class << self
attr_accessor :greeting
end
end
Which is true. However, they are “inherited” isn't exactly the case. Rather, cattr_accessor uses class variables.
The problem with class variables in Ruby, is that a class variable is the same object across all subclasses of a class. Consider the following example of what happens with cattr_accessor:
class A
@@foo = 'foo'
def self.foo
@@foo
end
end
p A.foo # => "foo"
class B < A
end
p B.foo # => "foo"
class B
@@foo = 'bar'
end
p B.foo # => "bar"
So far so good you might think. However, something you might not have expected is that the variable has now also changed in class A:
p A.foo # => "bar"
This is in my opinion almost never what you'd want. More probable is that you'd want the individual class instance to have an accessor. (Remember classes are objects in Ruby). I do the following in regular Ruby:
class A
class << self
attr_accessor :foo
end
self.foo = 'foo'
end
p A.foo # => "foo"
class B < A
end
p B.foo # => nil
class B
self.foo = 'bar'
end
p B.foo # => "bar"
p A.foo # => "foo"
As you can see, this returns +nil+ when a value hasn't explicitly been set yet on the new class instance. If you'd like to have inheritance without messing with the superclasses variables, have a look at ActiveSupport’s class_inheritable_accessor, which does the same as I just explained, but creates a clone of the object and assigns it to the subclass whenever a class is inherited.
What I’d normally do in Ruby to fix the issue of it returning +nil+ is to create the accessor manually and have it set the instance variable to the default if it’s +nil+:
class A
class << self
def foo
@foo ||= 'foo'
end
end
end
class B < A
end
p B.foo # => nil
So to recap:
- cattr_accessor uses class variables (@@foo), in which case the object is shared across all subclasses of a class. Use it mainly for static data, in which case you'd probably best use a constant.
- class_inheritable_accessor (or what I showed) uses instance variables (@foo) at the Class instance level. These variables are not shared across all subclasses.
Usage
This defines attr_accessors at a class level instead of instance level. class Foo cattr_accessor :greeting end
Foo.greeting = "Hello"
This could be compared to, but is not the same as doing this: class Bar class << self attr_accessor :greeting end end
Bar.greeting = "Hello"
The difference might not be apparent at first, but cattr_accessor will make the accessor inherited to the instances: Foo.new.greeting #=> "Hello" Bar.new.greeting # NoMethodError: undefined method `greeting' for #Bar:0x18e4d78
This inheritance is also not copy-on-write in case you assumed that: Foo.greeting #=> "Hello" foo1, foo2 = Foo.new, Foo.new
foo1.greeting = "Hi!"
Foo.greeting #=> "Hi!"
foo2.greeting #=> "Hi!"
This makes it possible to share common state (queues, semaphores, etc.), configuration (max value, etc.) or temporary values through this.
cattr_accessor_with_default
Class attribute assessors are neat if you want to set up modifiable constant-like varibles. This is how you'd normally set it up:
module MyPlugin
class Conf
@@awesome_level = 'huge'
cattr_accessor :awesome_level
end
end
Then you can call and modify it like this:
>> MyPlugin::Conf.awesome_level
=> 'huge'
>> MyPlugin::Conf.awesome_level = 'massive'
>> MyPlugin::Conf.awesome_level
=> 'massive'
If you have a pile of those accessors I'd do something like this (there might be a better way, but it works):
module MyPlugin
class Conf
def self.cattr_accessor_with_default(name, value = nil)
cattr_accessor name
self.send("#{name}=", value) if value
end
cattr_accessor_with_default :awesome_level, 'huge'
cattr_accessor_with_default :speed_level, 'insane'
cattr_accessor_with_default :indifferent_level
cattr_accessor_with_default :craziness_level, 'nuts'
end
end
This way you declare accessor and it's optional default value on the same line