Using Instance Variables in Class Methods - Ruby
The reason instance variables work on classes in Ruby is that Ruby classes are instances themselves (instances of class Class). Try it for yourself by inspecting DummyClass.class
. There are no "static methods" in the C# sense in Ruby because every method is defined on (or inherited into) some instance and invoked on some instance. Accordingly, they can access whatever instance variables happen to be available on the callee.
Since DummyClass
is an instance, it can have its own instance variables just fine. You can even access those instance variables so long as you have a reference to the class (which should be always because class names are constants). At any point, you would be able to call ::DummyClass.instance_variable_get(:@arr)
and get the current value of that instance variable.
As for whether it's a good thing to do, it depends on the methods.
If @arr
is logically the "state" of the instance/class DummyClass
, then store it in instance variable. If @arr
is only being used in dummy_method2
as an operational shortcut, then pass it as an argument. To give an example where the instance variable approach is used, consider ActiveRecord in Rails. It allows you to do this:
u = User.new
u.name = "foobar"
u.save
Here, the name that has been assigned to the user is data that is legitimately on the user. If, before the #save
call, one were to ask "what is the name of the user at this point", you would answer "foobar". If you dig far enough into the internals (you'll dig very far and into a lot of metaprogramming, you'll find that they use instance variables for exactly this).
The example I've used contains two separate public invocations. To see a case where instance variables are still used despite only one call being made, look at the ActiveRecord implementation of #update_attributes
. The method body is simply load(attributes, false) && save
. Why does #save
not get passed any arguments (like the new name
) even though it is going to be in the body of save where something like UPDATE users SET name='foobar' WHERE id=1;
? It's because stuff like the name is information that belongs on the instance.
Conversely, we can look at a case where instance variables would make no sense to use. Look at the implementation of #link_to_if
, a method that accepts a boolean-ish argument (usually an expression in the source code) alongside arguments that are ordinarily accepted by #link_to
such as the URL to link to. When the boolean condition is truthy, it needs to pass the rest of the arguments to #link_to
and invoke it. It wouldn't make much sense to assign instance variables here because you would not say that the invoking context here (the renderer) contains that information in the instance. The renderer itself does not have a "URL to link to", and consequently, it should not be buried in an instance variable.
Ruby: How should I access instance variables inside a class?
I would use the second option:
class Point
def initialize(x, y)
@x = x
@y = y
end
def distance
Math.sqrt(x ** 2 + y ** 2)
end
private
attr_reader :x, :y
end
For two reasons:
attr_reader
might be faster (as Filip Bartuzi already pointed out)- Using
attr_reader
might make it easier to refactor that class later on by replacing theattr_reader
with a custom getter method.
In Ruby, in the context of a class method, what are instance and class variables?
People seem to be ignoring that the method is a class method.
@blih will be instance variable of the instance of class Class for the constant Bleh. Hence:
irb(main):001:0> class Bleh
irb(main):002:1> def self.bleh
irb(main):003:2> @blih = "Hello"
irb(main):004:2> @@blah = "World"
irb(main):005:2> end
irb(main):006:1> end
=> nil
irb(main):007:0> Bleh.instance_variables
=> []
irb(main):008:0> Bleh.bleh
=> "World"
irb(main):009:0> Bleh.instance_variables
=> ["@blih"]
irb(main):010:0> Bleh.instance_variable_get :@blih
=> "Hello"
@@blah will be available as a class variable of Bleh:
irb(main):017:0> Bleh.class_variables
=> ["@@blah"]
irb(main):018:0> Bleh.send :class_variable_get, :@@blah
=> "World"
Ruby class instance variable vs. class variable
Instance variable on a class:
class Parent
@things = []
def self.things
@things
end
def things
self.class.things
end
end
class Child < Parent
@things = []
end
Parent.things << :car
Child.things << :doll
mom = Parent.new
dad = Parent.new
p Parent.things #=> [:car]
p Child.things #=> [:doll]
p mom.things #=> [:car]
p dad.things #=> [:car]
Class variable:
class Parent
@@things = []
def self.things
@@things
end
def things
@@things
end
end
class Child < Parent
end
Parent.things << :car
Child.things << :doll
p Parent.things #=> [:car,:doll]
p Child.things #=> [:car,:doll]
mom = Parent.new
dad = Parent.new
son1 = Child.new
son2 = Child.new
daughter = Child.new
[ mom, dad, son1, son2, daughter ].each{ |person| p person.things }
#=> [:car, :doll]
#=> [:car, :doll]
#=> [:car, :doll]
#=> [:car, :doll]
#=> [:car, :doll]
With an instance variable on a class (not on an instance of that class) you can store something common to that class without having sub-classes automatically also get them (and vice-versa). With class variables, you have the convenience of not having to write self.class
from an instance object, and (when desirable) you also get automatic sharing throughout the class hierarchy.
Merging these together into a single example that also covers instance variables on instances:
class Parent
@@family_things = [] # Shared between class and subclasses
@shared_things = [] # Specific to this class
def self.family_things
@@family_things
end
def self.shared_things
@shared_things
end
attr_accessor :my_things
def initialize
@my_things = [] # Just for me
end
def family_things
self.class.family_things
end
def shared_things
self.class.shared_things
end
end
class Child < Parent
@shared_things = []
end
And then in action:
mama = Parent.new
papa = Parent.new
joey = Child.new
suzy = Child.new
Parent.family_things << :house
papa.family_things << :vacuum
mama.shared_things << :car
papa.shared_things << :blender
papa.my_things << :quadcopter
joey.my_things << :bike
suzy.my_things << :doll
joey.shared_things << :puzzle
suzy.shared_things << :blocks
p Parent.family_things #=> [:house, :vacuum]
p Child.family_things #=> [:house, :vacuum]
p papa.family_things #=> [:house, :vacuum]
p mama.family_things #=> [:house, :vacuum]
p joey.family_things #=> [:house, :vacuum]
p suzy.family_things #=> [:house, :vacuum]
p Parent.shared_things #=> [:car, :blender]
p papa.shared_things #=> [:car, :blender]
p mama.shared_things #=> [:car, :blender]
p Child.shared_things #=> [:puzzle, :blocks]
p joey.shared_things #=> [:puzzle, :blocks]
p suzy.shared_things #=> [:puzzle, :blocks]
p papa.my_things #=> [:quadcopter]
p mama.my_things #=> []
p joey.my_things #=> [:bike]
p suzy.my_things #=> [:doll]
Variable available to class methods (within Concerns)
For a quick solution, to make Product.all
and Product.visible
work with the least amount of modification to your existing code, you can define a parameters
method inside module ClassMethods
. For example:
def parameters
@parameters ||= [:visible, :desc, :value]
end
This method solution can also serve as a long-term solution if you plan to use the parameters outside of the concern, or if a subclass might want to define its own parameters.
However, if the parameters are only meant to be used inside this concern, and this data will never change, at least not through any application logic, then a constant would be the best solution because it conveys the proper meaning to the reader. I would also freeze it to prevent modification:
PARAMETERS = [:visible, :desc, :value].freeze
Another option, as Rich mentioned, is to define a class variable. Note that the constant will work whether you define it inside the List
module, or inside the ClassMethods
module. However, a class variable will only work inside the ClassMethods
module if you want Product
to be able to call it as parameters
.
Also, note that self
is implied in any method within ClassMethods
, so you don't need to specify it. If you defined a parameters
method, it would be considered a Product
class method, and if you used parameters
within the all
method, it would refer to the class method, not an instance method as suggested by Rich.
Class variables are generally discouraged in Ruby because their side effects are often misunderstood. The Ruby Style Guide recommends avoiding them: https://github.com/bbatsov/ruby-style-guide#no-class-vars
As for speed, I compared the method and constant solutions, and it looks like the constant is faster:
require "benchmark/ips"
PARAMETERS = [:visible, :desc, :value].freeze
def parameters
@parameters ||= [:visible, :desc, :value]
end
def uses_constant
puts PARAMETERS
end
def uses_method
puts parameters
end
Benchmark.ips do |x|
x.report("constant") { uses_constant }
x.report("method") { uses_method }
x.compare!
end
The result:
Comparison:
constant: 45256.8 i/s
method: 44799.6 i/s - 1.01x slower
Related Topics
Finding Out Current Index in Each Loop (Ruby)
Using a String as a Variable at Run Time
Add Http(S) to Url If It's Not There
Rails - Whenever Gem - Dynamic Values
Validate Words Against an English Dictionary in Rails
Block Syntax Difference Causes "Localjumperror: No Block Given (Yield)"
How to Sort an Array of Hashes by a Value in the Hash
How to Add Information to an Exception Message in Ruby
Difference Between Each.With_Index and Each_With_Index in Ruby
Csv.Read Illegal Quoting in Line X
Get the Value of an Instance Variable Given Its Name
How to Use Bundler with Offline .Gem File
Project Euler 1:Find the Sum of All the Multiples of 3 or 5 Below 1000
How to Pass Parameter on 'Vagrant Up' and Have It in the Scope of Vagrantfile