HalloDu
HalloDu

Reputation: 907

Initializing class instance variables in Ruby

I am working on a small rails app and have a problem with ruby's OOP model. I have the following simplified class structure.

class Foo
  protected
    @bar = []
    def self.add_bar(val)
      @bar += val
    end
    def self.get_bar
      @bar
    end
end

class Baz < Foo
  add_bar ["a", "b", "c"]
end

My problem is now, that when I call add_bar in the class definition of Baz, @bar is apparently not initialized and I get an error that the + Operator is not available for nil. Calling add_bar on Foo directly does not yield this problem. Why is that and how can I initialize @bar correctly?

To make clear what I want, I will point out the behavior I would expect from these classes.

Foo.add_bar ["a", "b"]
Baz.add_bar ["1", "2"]
Foo.get_bar # => ["a", "b"]
Baz.get_bar # => ["a", "b", "1", "2"]

How could I achieve this?

Upvotes: 19

Views: 22681

Answers (4)

Nicolas Goy
Nicolas Goy

Reputation: 1430

I do it like so:

class Base

    class << self

        attr_accessor :some_var


        def set_some_var(value)
            self.some_var = value
        end

    end

end


class SubClass1 < Base
  set_some_var :foo
end

class SubClass2 < Base
  set_some_var :bar
end

Then it should do what you want.

[8] pry(main)> puts SubClass1.some_var
foo
[9] pry(main)> puts SubClass2.some_var
bar

Note that the set_some_var method is optional, you can do SubClass1.some_var = ... if you prefer.

If you want some default value, add something like that under class << self

def some_var
    @some_var || 'default value'
end

Upvotes: 2

r00k
r00k

Reputation: 661

Short answer: instance variables don't get inherited by subclasses

Longer answer: the problem is that you wrote @bar = [] in the body of the class (outside any method). When you set an instance variable, it is stored on whatever is currently self. When you're in a class body, self is the class object Foo. So, in your example, @foo gets defined on the class object Foo.

Later, when you try to look up an instance variable, Ruby looks in whatever is currently self. When you call add_bar from Baz, self is Baz. Also self is STILL Baz in the body of add_bar (even though that method is in Foo). So, Ruby looks for @bar in Baz and can't find it (because you defined it in Foo).

Here's an example that might make this clearer

class Foo
  @bar = "I'm defined on the class object Foo. self is #{self}"

 def self.get_bar
    puts "In the class method. self is #{self}"    
    @bar
  end

  def get_bar
    puts "In the instance method. self is #{self} (can't see @bar!)"
    @bar
  end
end

>> Foo.get_bar
In the class method. self is Foo
=> "I'm defined on the class object Foo. self is Foo"

>> Foo.new.get_bar
In the instance method. self is #<Foo:0x1056eaea0> (can't see @bar!)
=> nil

This is admittedly a bit confusing, and a common stumbling point for people new to Ruby, so don't feel bad. This concept finally clicked for me when I read the 'Metaprogramming' chapter in Programming Ruby (aka "The Pickaxe").

How I'd solve your problem: Look at Rails' class_attribute method. It allows for the sort of thing you're trying to do (defining an attribute on a parent class that can get inherited (and overidden) in its subclasses).

Upvotes: 43

khelll
khelll

Reputation: 23990

Well, since @bar is defined as class instance variable then it's limited to the class Foo. Check this:

class Foo
    @bar = []
end

class Baz < Foo
end

Foo.instance_variables #=> [:@bar]
Baz.instance_variables #=> []

Anyway, for this simple example you can do this:

class Foo
  protected
    def self.add_bar(val)
      @bar ||=[]
      @bar += val
    end
    def self.get_bar
      @bar
    end
end

class Baz < Foo
  add_bar ["a", "b", "c"]
end

Read more about this here.

Upvotes: 3

Guilherme Bernal
Guilherme Bernal

Reputation: 8293

This appears to work well:

class Foo
  protected
  @@bar = {}
  def self.add_bar(val)
    @@bar[self] ||= []
    @@bar[self] += val
  end
  def self.get_bar
    (self == Foo ? [] : @@bar[Foo] || []) + (@@bar[self] || [])
  end
end
class Baz < Foo
end

Foo.add_bar ["a", "b"]
Baz.add_bar ["1", "2"]
Foo.get_bar     # => ["a", "b"]
Baz.get_bar     # => ["a", "b", "1", "2"]

Upvotes: 0

Related Questions