Two Ruby methods seem to cause more confusion than any others, particularly due
to the way they are explained. These two are class_eval and
instance_eval. The names are very similar, and their behavior is
counterintuitive. The bottom line is this:
ClassName.instance_eval to define a
class method (one associated with the class object but not
visible to instances). ClassName.class_eval to define an instance
method (one that applies to all of the instances of
ClassName). To understand why this is true, let's go through some examples, starting with the following code:
class MyClass
def initialize(num)
@num = num
end
end
a = MyClass.new(1)
b = MyClass.new(2)
Before we get going, remember that in Ruby everything is an object.
That means classes are objects too. When you define MyClass,
Ruby will create a global variable with the name MyClass, which
is the class object for MyClass. When you write
MyClass.new you are getting the class object MyClass
and then calling the new method on that object, which
gives you a new instance of MyClass. Instances are the actual
objects of the class that you would normally use. There is one class with
many instances.
So we have two objects that are both of the same class. Of course, this
class isn't very useful because it doesn't do anything.
There is no way to access @num becasuse we did not define
any getter or setter methods.
irb> a.num NoMethodError: undefined method `num' for #<MyClass:0x007fba5c02c858 @num="1">
Let's look briefly at instance_eval. What can we do with it?
We can run code as if we were inside a method of the specific object we
call it on.
That means we can access instance variables and private methods. Let's evaluate
an expression in the instances of MyClass so that we get the
values out.
irb> a.instance_eval { @num }
=> 1
irb> b.instance_eval { @num }
=> 2
That's great, but it would be a real pain to do that a lot. Let's define a method to do it for us.
irb> a.instance_eval do irb> def num irb> @num irb> end irb> end => nil irb> a.num => 1 irb> b.num NoMethodError: undefined method `num' for #<MyClass:0x007fba5c08e5f8 @num="2">
Whoops! We used instance_eval, which only evaluates in
the context of one object. We defined a method, but only on the particular
object a. How do we make a method that is shared by all
objects of that class? Perhaps we should define a method on the class
object?
irb> MyClass.instance_eval do irb> def num irb> @num irb> end irb> end => nil irb> b.num NoMethodError: undefined method `num' for #<MyClass:0x007fba5c08e5f8 @num="2">
Oops, that didn't work either. What happened? Well, we did the same thing
as above, but on a class object! That means we defined a method on the
class object; this is not the same thing as a method that gets inherited
by the objects of that class. It's just the same as if we defined a method
in the class with def self.num, which is similar to a class
static method in Java. This method would have to be invoked
as MyClass.num, not a.num, and it won't work
anyway:
irb> MyClass.num => nil
We get nil here because there is no variable @num
in the MyClass object. Undefined variables have a default
value of nil.
Alright, so what's the right way to do it? The answer is
class_eval:
irb> MyClass.class_eval do irb> def num irb> @num irb> end irb> end => nil irb> b.num => 2
Horray! That worked. We defined a method for the class, not on the class object, and that method is then available for all objects of that class.
Note that we called class_eval on MyClass,
not on one of the
instances. Invoking class_eval on an instance wouldn't work
because class_eval isn't
a method of arbitrary objects, only of class objects like
MyClass. But you can get an object's class dynamically
with the class method:
irb> a.class_eval
NoMethodError: undefined method `class_eval' for #<MyClass:0x007fba5c02c858 @num="1">
irb> a.class.class_eval {}
=> nil
Another way to think about this is that class_eval is equivalent
to typing the code inside a class statement:
MyClass.class_eval do
def num
@num
end
end
behaves exactly the same as the following code:
class MyClass
def num
@num
end
end