ruby picture

RCR 190: Disallow @classInstanceVar in class body

Submitted by coreywangler (Wed Jan 14 02:01:44 UTC 2004)

Abstract

Make @classInstanceVariables consistent with @instanceVariables by restricting them to use within defined class methods.

Prevent @instanceVariable initialization bug

Problem

1) Having @classInstanceVariables accessible in the class body is not consistent with the rule that "an @instanceVariable can only be used within instance methods" (in this case the instance is a class, and the methods are class methods). Note that the class body is not a callable class method -- it only runs once, and there is no way to execute that code again.

2) Allowing initialization of @classInstanceVariables in the class body can confuse the programmer, especially where the nubie mistakenly puts an initialization of an @instanceVariable outside of an instance method (making it a @classInstanceVariable instead of what they assumed would be a normal @instanceMethod).


    
  1. probable initialization bug gets introduced in this code
class Foo
  @a = 123   # (1) class instance var initialized
  def foo
    p @a     # (2) 'normal' instance var accessed ...nil not 123
  end
end

Proposal

Disallow @classInstanceVariables in class body.

If wanting to initialize @classInstanceVars from the class body, a class method can be called there (as a class body statement), with the advantage of having the initializing method callable using "super" in a derived class:


    
  class Foo
    def Foo.init(valA)
      @classInstanceVarA = valA
    end
    Foo.init(400)
    def Foo.test
      puts "in Foo.test, @classInstanceVarA is " + 
                         @classInstanceVarA.to_s
    end
    Foo.test
  end
  #==> in Foo.test, @classInstanceVarA is 400
  class Bar < Foo
    def Bar.init(valB)
      super(800)
      @classInstanceVarB = valB
    end
    Bar.init(864)
    def Bar.test
      puts "in Bar.test, @classInstanceVarA is " + 
                         @classInstanceVarA.to_s
      puts "in Bar.test, @classInstanceVarB is " + 
                         @classInstanceVarB.to_s
    end
    Bar.test
  end
  #==> in Bar.test, @classInstanceVarA is 800
  #==> in Bar.test, @classInstanceVarB is 864

    

Using class methods to initialize @classInstanceVars is also useful when creating a derived class using "Class.new(ASuperClass)" as the following example shows:


    
  AnotherBar = Class.new(Bar)
  AnotherBar.test
  #==> in Bar.test, @classInstanceVarA is
  #==> in Bar.test, @classInstanceVarB is
  AnotherBar.init(888)
  AnotherBar.test
  #==> in Bar.test, @classInstanceVarA is 800
  #==> in Bar.test, @classInstanceVarB is 888

    

Note that immediately after "Class.new(ASuperClass)" is used to create a class instance, all @classInstanceVars are nil. This is the case regardless of where initialization of @classInstanceVars is done, because there is no automatic calling of any initialization code for generated class instances (the body of ASuperClass is not executed again). Here it is *necessary* to call a class method to give appropriate values to the @classInstanceVars.

The following shows behaviour that may surprise some (@classInstanceVarA is initialized in the body of class Foo2, but becomes nil in the derived class Bar2 -- because Bar2 is a different class instance)...


    
  class Foo2
    @classInstanceVarA = 400
    def Foo2.test
      puts "in Foo2.test, @classInstanceVarA is " + 
                          @classInstanceVarA.to_s
    end
    Foo2.test
  end
  #==> in Foo2.test, @classInstanceVarA is 400
  class Bar2 < Foo2
    @classInstanceVarB = 864
    def Bar2.test
      puts "in Bar2.test, @classInstanceVarA is " + 
                          @classInstanceVarA.to_s
      puts "in Bar2.test, @classInstanceVarB is " + 
                          @classInstanceVarB.to_s
    end
    Bar2.test
  end
  #==> in Bar2.test, @classInstanceVarA is 
  #==> in Bar2.test, @classInstanceVarB is 864

    

Disallowing @classInstanceVariables in the class body forces all initialization of @classInstanceVars to be done in a consistent manner, thus avoiding the "surprise" that @classInstanceVars are nil for derived classes.

Analysis

###--- extract from RCR 185 ---

Maybe warnings to the instance variable assignments in the class body might solve the problem very nicely without modifying language syntax.

- matz.

###----------------------------

...it seems Matz always has the best solutions! :)

Class methods must be defined to access and use @classInstanceVariables, so mandating that they need to be initialized within such defined class methods is not a burden to the programmer.

In my opinion, forcing @classInstanceVariable initialization into class methods makes the code clearer and easier to understand. It also produces code that is more readily extended (code in the class body cannot be re-used).

It makes it clear that: "@instanceVariables are used inside instance methods"

...less surprise ;P

***Advantages***

Makes the use of @classInstanceVar and @instanceVar consistent and clear (even to beginners).

A class method needs to be called to initialize @classInstanceVariables -- this is an advantage because there is no way to execute the statements in the class body again. Having the initialization done in a class method allows that initialization routine to be executed in an inherited class by calling super in the corresponding class method, or by directly calling an inherited class method.

Prevents the @instanceVariable initialization bug by disallowing @instanceVariables from the class body -- a place where their meaning is not so clear to beginners (assumed to be a @instanceVariable, but is actually a @classInstanceVariable).

***Disadvantages***

Programmer might have to write more code, as they must put @classInstanceVariable initialization into class method(s).

Implementation

In first stage of implementation, give a warning if @instanceVariable is used in the class body.

In second stage, prohibit @classInstanceVariable from class body.


    
  class Foo
    @variable = 0    #produce an error message
  end
  #==> ERROR: instance variables are not allowed in class body

    

ruby picture
Comments Current voting


David Black:

The problem here is that there's no problem: that is, there's no rule saying that instance variables are only visible in instance methods. An instance variable belongs to whatever object is 'self' at the time the instance variable is used. This may or may not be happening inside one of that object's instance methods. It can also happen in an instance_eval. And since 'self' is set to the Class object at the top level of a class definition, instance variables used at that level belong to that object.

I know that people sometimes misunderstand and think they're using a Thing object's instance variable when they're actually using one of Thing's instance variables. But the way it actually works is consistent, and the language mustn't be made inconsistent just to save people having to learn how it works. As a non-beginner, I would not want to have to work around an anomaly that made it otherwise -- nor would I want to have to explain to a beginner why there was this exception to the rule about instance variables :-)

(I definitely wouldn't want a warning either, unless it was only turned on by a specific switch that I could avoid using.)

As with so many things, this boils down to the "a class is just an object" principle. That's a basic design principle of Ruby, and I feel strongly that people should be encouraged to embrace it and accept it completely. This includes the fact that a subclass is not the same object as the class it subclasses, so there's no reason to expect them to share instance variables. (Indeed, no two objects share instance variables, which is why they're called instance variables :-)


Giving warning might help finding problems. The point is after understanding the principles of instance variable in Ruby, whether one can make such mistakes so often, so that language should help and encourage something about it.

I doubt it. And even if it proved to be true, I will not prohibit the so-called "class instance variables" in the class body, warning is good enough.

-matz.


David Black:

But there's nothing wrong with referring to a class's instance variables in the class body; it's actually a good way to maintain state per-class. A warning would definitely label it as a bad practice. I'm afraid it would further discourage people from understanding the underlying design.


Withdrawing RCR

I think keeping @classInstanceVars out of the class body is "good practice" -- avoids pitfalls/confusion, and produces code that is more readily extended.

But I have to agree with all of David's insights into this area, so @classInstanceVars in the class body is not "bad practice", and a warning is thus not appropriate.

Cheers, Corey.


Strongly opposed 2
Opposed 1
Neutral 1
In favor 0
Strongly advocate 1
ruby picture
If you have registered at RCRchive, you may now sign in below. If you have not registered, you may sign up for a username and password. Registering enables you to submit new RCRs, and vote and leave comments on existing RCRs.
Your username:
Your password:

ruby picture

Powered by .