Search code examples
rubylocal-variablesambiguity

Unexecuted code overrides local variable


Given code:

class Foo
  attr_reader :bar

  def initialize
    @bar = "abc"
    if false
      bar = "123"
    end
    p bar
  end
end

Foo.new

The result is

nil

Why does p bar in initialize print nil instead of abc?


Solution

  • Try this:

    class Foo
      attr_reader :bar
      def initialize
        p "instance methods defined in Foo: #{self.methods(false)}"  
        @bar = "abc"
        p "defined? @bar: #{defined? @bar}"
        p "bar: #{bar}"
        p "defined? bar: #{defined? bar}"
        if false
          bar = "123"
        end
        p "defined? bar, 2nd time: #{defined? bar}"
        p "bar.nil? = #{bar.nil?}"
        p "self.bar = #{self.bar}"
        p "instance methods defined in Foo: #{self.class.instance_methods(false)}"  
      end
    end
    
    Foo.new
    "instance methods defined in Foo: [:bar]"
    "defined? @bar: instance-variable"
    "bar: abc"
    "defined? bar: method"
    "defined? bar, 2nd time: local-variable"
    "bar.nil? = true"
    "self.bar = abc"
    "instance methods defined in Foo: [:bar]"
    

    The lines:

    "defined? @bar: instance-variable"
    "defined? bar: method"
    

    show that @bar is an instance variable and bar is an instance method, namely the getter method for @bar created by attr_reader :bar. Before

    if false
      bar = "123"
    end
    

    is evaluated, Ruby peers into the if clause. There she sees bar = "123". If invoked, this would assign the value "123"to an uninitialized local variable bar.

    bar= cannot be an instance method (e.g., a setter for @bar) because any method whose name ends with an equals sign must be invoked on an explicit receiver. (It works that way to allow coders to use local variables that have the same names as instance variables, minus the leading @.)

    What is an "explicit" receiver? If Foo had a public instance method buz, you could write:

    foo = Foo.new
    foo.buz
    

    foo is an explicit receiver for the method buz. To invoke buz from within one of Foo's instance methods, you could use an explicit receiver:

    self.buz
    

    or just write:

    buz
    

    in which case self is the implicit receiver.

    As bar= can only be written with an explicit receiver, we would have write:

    attr_writer :bar
    ...
    self.bar = "123"
    

    to invoke @bar's setter.

    Where were we? Ah, we just concluded that:

    if false
      bar = "123"
    end
    

    would assign a value to the local variable bar if the if clause were executed, regardless of whether there exists a method Foo#bar=.

    Because false is, well, false, the contents of the if clause are not executed, so the value of bar is not changed from nil.

    The important thing is that the local variable bar and the instance variable @bar are just as different from each other as are night and @day. We can easily show that as follows:

    a  = 'cat'
    @a = 'dog'
    a  #=> "cat" 
    a  = 'pig'
    @a #=> "dog"