-1

I have a base controller

class Base
  @@var = 'base'

  def self.result
    @@var
  end
  def self.result=(var)
    @@var = var
  end

  def do_sth
    // do something here
  end
end

and 2 subclasses

class A < Base
  Base.result = 'a'
end

class B < Base
  Base.result = 'b'
end

when visit url_a goes to class A, and then I visit url_b goes to class B, both of them work fine.

But when I switch back to url_a, the Base.result still returns b, why?


edit
I've changed @@var to @var, I got the same result.

worldask
  • 1,837
  • 3
  • 22
  • 37
  • This is a `class` variable and you're setting it when Ruby reads in the definition of (and defines) the `class`. If it's not re-reading in the source for the `class` each time you reference it, it's not going to change. – lurker Sep 18 '14 at 13:54
  • @vee that's different. I've already changed it to instance variable. The result is the same. And I didn't want to `new` the subclass. – worldask Sep 18 '14 at 14:13
  • @lurker If I don't want to `new` the subclass, how can I achieve my goal? – worldask Sep 18 '14 at 14:14
  • When you change URL's can you call a class method that selects the class and sets the variable? BTW, changing `@@var` to `@var` won't help due to the same issue I pointed out: you are setting the variable during *class definition*. – lurker Sep 18 '14 at 14:22

1 Answers1

1

This is an extension of what @lurker has mentioned in the comments to the question. It does not matter whether you use a class variable or a class level instance variable, you will see the same behaviour.

However what happens behind the scenes is different when you use the two types of variables.

Case 1 : Class variable (i.e. @@var)

The value of the class variable is being set during class declaration, i.e. when the Ruby source code is read and that happens only once.

There are two things to keep in mind here :

  • Rails follows lazy loading, i.e. it loads up the definition of a class when it requires it for the first time.
    • So when you hit url_a for the first time class A is loaded, i.e. its source is parsed
    • class B is not yet loaded. It gets loaded later when you hit url_b.
  • When a Ruby source file is parsed, all the code that is outside any function is executed right away. So the Base.result = method calls are executed when the two classes are loaded.

So the sequence of steps would be :

  • In the call to url_a, class A is parsed and Base.result = a sets @@var to a
  • Then in the call to url_b, class b is parsed and Base.result = b sets @@var to b and it remains so for all subsequent calls.

The following code snippet might help understand the second point :

irb(main):033:0> class ParseTest
irb(main):034:1> @@time_now = Time.now
irb(main):035:1> def speak_time
irb(main):036:2> puts @@time_now.to_s
irb(main):037:2> end
irb(main):038:1> end
=> nil
irb(main):039:0> pt = ParseTest.new
=> #<ParseTest:0x007f80758514c8>
irb(main):040:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):041:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):042:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):043:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):044:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):045:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):046:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):047:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):048:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):049:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):050:0> pt.speak_time
2014-09-18 23:15:15 +0530
=> nil
irb(main):051:0> class ParseTest2 < ParseTest
irb(main):052:1> @@time_now = Time.now
irb(main):053:1> end
=> "2014-09-18T23:16:41.911+05:30"
irb(main):054:0> pt.speak_time
2014-09-18 23:16:41 +0530
=> nil
irb(main):055:0> 

As you can see, after the ParseTest class definition was parsed once, the value of @@time_now did not change in any of the subsequent puts. The value of time was what it was when the source code was parsed.

However when I defined the ParseTest2 sub-class and its code was parsed, the same class variable was given a new value of time. This new value is reflected when I print it using the same old object of the base class.

This is what is happening in your code too.

Case 2 : Class level instance variable (i.e. @var in class definition outside any instance function) Now, if instead of a class variable you use an instance variable in the class definition (i.e. outside any function), that is a very different. This case might appear a little confusing, so read and re-read the following code snippet if it appears confusing to you in the first go.

irb(main):089:0> class Base
irb(main):090:1>   @time_now = Time.now
irb(main):091:1> 
irb(main):092:1*   def self.time_now=(time)
irb(main):093:2>     @time_now = time
irb(main):094:2>   end
irb(main):095:1> 
irb(main):096:1*   def self.time_now
irb(main):097:2>     puts @time_now.to_s
irb(main):098:2>   end
irb(main):099:1> end
=> nil
irb(main):100:0> class A < Base
irb(main):101:1>   Base.time_now = Time.now
irb(main):102:1> end
=> "2014-09-18T23:33:26.514+05:30"
irb(main):103:0> Base.time_now
2014-09-18 23:33:26 +0530
=> nil
irb(main):104:0> A.time_now

=> nil
irb(main):105:0> A.time_now = Time.now
=> "2014-09-18T23:34:27.093+05:30"
irb(main):106:0> A.time_now
2014-09-18 23:34:27 +0530
=> nil
irb(main):107:0> Base.time_now
2014-09-18 23:33:26 +0530
=> nil
irb(main):108:0> 

The class level instance variable is private to that class. It does not get passed on / shared during inheritance. So every class in the inheritance hierarchy has its own set of instance variables. However the methods do get passed on and those methods act on the instance variable of the class on which they were called. So depending on which class you call the time_now= setter method, the corresponding instance variable is set.

In your case you are always referring to the instance variable of the Base class. So the same set of steps happen as described in the previous case

  • In the call to url_a, class A is parsed and Base.result = a sets @var of Base to a
  • Then in the call to url_b, class b is parsed and Base.result = b sets @var of Base to b and it remains so for all subsequent calls.
brahmana
  • 1,286
  • 12
  • 24
  • thanks for you long long explanation. I think I understand what happened behind @@ & @ now. – worldask Sep 19 '14 at 02:00
  • But my work still cannot go on. I want a method defined in a base class, return a variable changed in different subclasses. Is there another way to do it? – worldask Sep 19 '14 at 02:04
  • Use proper instance methods and instance variables and set the values inside those methods. However it would be a lot more clearer if you tell us what you are trying to achieve. There might be better ways of achieving the same. – brahmana Sep 19 '14 at 03:11
  • I may have tens of controllers they have similar methods in my rails app. I want to move those similar methods into a `BaseController`, then I can keep less code in those controllers. Now I am blocked by an issue as this question mentioned, each page related to a controller will get a `page title` from the controller, the titles are different, but all return by a method in the `BaseController`. You can take a look [here](https://github.com/worldask/ror-ng-bootstrap-admin/tree/master/app/controllers/admin), there are three controllers, I think you'll know what I want. – worldask Sep 19 '14 at 07:22