OPB
OPB

Reputation: 11

Instance Variable changed unintentionally by a different class

I'm currently learning the basics of Ruby and OOP in general. From what I've read so far, I can use attr_reader to grab the value of an instance variable but not give it access to be overwritten. However, given the code block below, the end result is not what I intended and the instance variable was completely changed from outside of the class. What would be the best way where I can simply read the value and return the intended changes into another variable instead of overwriting the instance variable itself?

class X
  def initialize
    @y = [1,2,3,4]
  end
  attr_reader :y
end

class Z
  def initialize
  end
  def self.change(arr)
    arr[1] = 0
    arr[2] = 0
    return arr
  end
end

x = X.new
z = Z.change(x.y)
p z
p x.y

Upvotes: 1

Views: 412

Answers (3)

Stefan
Stefan

Reputation: 114218

From what I've read so far, I can use attr_reader to grab the value of an instance variable but not give it access to be overwritten.

Yes and no.

attr_reader :y creates a so-called getter which is equivalent to:

def y
  @y
end

attr_writer :y creates the corresponding setter:

def y=(value)
  @y = value
end

And attr_accessor creates both.

The getter allows you to conveniently access @y from the outside. The setter allows you to re-assign @y.

But even with just a getter, you can still send messages to the object. And if the object is mutable, like your array, you can modify it that way:

x = X.new
x.y #=> [1, 2, 3, 4]
x.y.push(5)
x.y #=> [1, 2, 3, 4, 5]

In the above example, @y is not re-assigned, it still refers the same object. But the message push caused the object to change itself.

What would be the best way where I can simply read the value [...]

There are several options. If you want to prevent modification form the outside, you could return a copy of the original array:

class X
  def initialize
    @y = [1, 2, 3, 4]
  end

  def y
    @y.dup
  end
end

dup creates a shallow copy of the array and returns it. "shallow" means that a new array is created containing the same elements. Any modification from the outside to the array will only affect the copy.

But you could still modify its elements (via messages) and that change would be reflected by both, original and copy.

Fortunately, your array contains integers which are immutable.

Upvotes: 2

Jörg W Mittag
Jörg W Mittag

Reputation: 369536

However, given the code block below, the end result is not what I intended and the instance variable was completely changed from outside of the class.

No, it wasn't. The object that the instance variable references was changed. That is to be expected: arrays can be changed, and you handed the caller an array, so the caller can obviously change the array.

But the instance variable was not changed: it still points to the exact same object as before.

My boss and my mother call me by different names. But if I shave my beard, both of them will see my clean-shaven face.

Understanding the difference between a thing and the name of the thing is fundamental in programming.

What would be the best way where I can simply read the value and return the intended changes into another variable instead of overwriting the instance variable itself?

The best way is to not expose internal representation in the first place. It's not quite clear from your question how a client is expected to use X (the naming is pretty terrible). So, for example, if clients are expected to iterate over the contents of @y, then you offer them a way to do exactly that and only that.

class X
  def initialize
    self.y = [1, 2, 3, 4]
  end

  def each_y(...)
    return enum_for(__callee__) unless block_given?
    y.each(...)
    self
  end

  private attr_accessor :y
end

See Overriding the << method for instance variables for another example of the same problem and how to solve it.

Upvotes: 1

darkash
darkash

Reputation: 161

by using clone: https://ruby-doc.org/core-2.7.2/Object.html#method-i-clone

Produces a shallow copy of obj—the instance variables of obj are copied, but not the objects they reference.

class X
  def initialize
    @y = [1,2,3,4]
  end
  attr_reader :y
end

class Z
  def self.change(arr)
    arr2 = arr.clone
    arr2[1] = 0
    arr2[2] = 0
    return arr2
  end
end

x = X.new
z = Z.change(x.y)
p z
p x.y

Upvotes: 0

Related Questions