Search code examples
rubyreferenceencapsulationinstance-variables

Why is a Ruby class allowing access to an instance variable through a return from a method?


I have this Ruby code from a colleague:

class Leaky

   def initialize
      @internalOnly = [[1, 2, 3], [3, 4, 5]]
   end

   def foo
      if 5 > 4
         temp = @internalOnly
      end
   end

   def printInternal
      puts " here is my @internalOnly: #{@internalOnly}"
   end

end

a = Leaky.new
a.printInternal

b = a.foo  # 1) Gets Leaky:@internal!
b[1] = 666 # 2) Modifies it!

a.printInternal

It produces this output:

 here is my @internalOnly: [[1, 2, 3], [3, 4, 5]]
 here is my @internalOnly: [[1, 2, 3], 666]

In statement # 1), Ruby is apparently returning a reference to the Leaky instance variable @internalOnly, which it then uses to modify @internalOnly in statement #2).

When Leaky:printInternal is called, the (externally) modified value is displayed.

I understand that

  1. Variables are references in Ruby, so temp = @internalOnly in the Leaky:foo method assigns a reference to @internalOnly
  2. Ruby returns the value of this expression -- the Array assigned to @internalOnly

After that, I don't understand how b = a.foo ends up with a reference to @internalOnly; this would seem to violate Ruby's object encapsulation in a big way. What is going on here?

I couldn't find anything in the Ruby Language FAQ that would enlighten me. (Maybe this is an inFAQ...)


Solution

  • 1) foo() is just an accessor method with a strange name, no different than:

    def internalOnly
      @internalOnly
    end
    

    2) Any method in a class can access the private variables of an instance, i.e. its @variables.

    3) Methods return the 'value' of the last statement executed.

    4) The value of an assignment statement is the right hand side.

    After that, I don't understand how b = a.foo ends up with a reference to @internalOnly

    Variables are references in Ruby

    Because a.foo returns a reference to the array, which gets assigned to b. As a result, the two variables @internalOnly and b reference the same array.

    this would seem to violate Ruby's object encapsulation in a big way. What is going on here?

    Well, the programmer allowed that to happen. If that wasn't intended, then the code can be rewritten like this:

       def foo
          if 5 > 4
             @internalOnly.dup
          end
       end
    

    But, you should know that any programmer with enough knowledge of ruby can always access an instance's private variables:

    class Dog
      attr_reader :friends
    
      def initialize(friends)
        @friends = friends
      end
    end
    
    d = Dog.new(['cat', 'bird', 'mouse'])
    
    x = d.instance_variable_get(:@friends)
    x[0] = 'velociraptor'
    
    p d.friends
    
    --output:--
    ["velociraptor", "bird", "mouse"]