16

I am using class_eval to write code to be executed under the context of current class. In the following code, I want to add a counter for changes of attribute values.

class Class
  def attr_count(attr_name)
    attr_name = attr_name.to_s
    attr_reader attr_name # create the attribute's getter
    class_eval %Q{
      @count = 0
      def #{attr_name}= (attr_name)
        @attr_name = attr_name
        @count += 1
      end

      def #{attr_name}
        @attr_name
      end
    }
    end
  end
class Foo
  attr_count :bar
end

f = Foo.new
f.bar = 1

My understanding of class_eval is that it evaluates the block in the context of the runtime class - in my case, under class Foo. I expect the above code runs similar as:

class Foo
  attr_count :bar
  @count = 0
  def bar= (attr_name)
    @attr_name = attr_name
    @count += 1
  end

  def bar
    @attr_name
  end
end

However the above code resulted in error saying, the error is caused by @count += 1. I cannot figure out why @count has nil:NilClass as its super?

(eval):5:in `bar=': undefined method `+' for nil:NilClass (NoMethodError)

On the other hand, @selman has given a solution to put @count assignment within the instance method and it works.

class Class
  def attr_count(attr_name)
    #...
    class_eval %Q{
      def #{attr_name}= (attr_name)
        @attr_name = attr_name
        if @count
          @count += 1
        else
          @count = 1
        end
      end
      #...
    }
  end
end

Why changes the variable scope works? How does class_eval execute its following string?

steveyang
  • 9,178
  • 8
  • 54
  • 80

1 Answers1

12

it is not about class_eval it is about @count. if you define this variable at class level it will be a class instance variable not an instance variable.

class Class
  def attr_count(attr_name)
    attr_name = attr_name.to_s
    attr_reader attr_name # create the attribute's getter
    class_eval %Q{
      def #{attr_name}= (attr_name)
        @attr_name = attr_name
        if @count
          @count += 1
        else
          @count = 1
        end
      end

      def #{attr_name}
        @attr_name
      end
    }
  end
end

class Foo
  attr_count :bar
end

f = Foo.new
f.bar = 1
Selman Ulug
  • 867
  • 5
  • 17
  • It works. I am still confused by the difference between `@count` and `@@count` in the `class_eval` method. My understanding is 1.The string passed to `class_eval` will be evaluated at runtime - when the `class Foo` is called. 2. `@count` should be an instance variable for all instances of `Foo`. Why use class variable `@@count` makes it behaves like **instance variable**? I know the topic is complicated, do you have some reference which I could read? – steveyang Feb 27 '12 at 15:31
  • Hi, @selmen. I found `@@count` doesn't really behave as a instance variable. It behaves as class variable. That's not what I expected – steveyang Feb 27 '12 at 16:35
  • It works. Thx. Compare with your and mine, the problem seems to lie at the scope of `@count`. Why the `@count` assigned directly in `class_eval` (in my code) couldn't be accessed by methods defined in `class_eval`? – steveyang Feb 28 '12 at 04:42