Mutable member variables -- surprising behavior

A

Adam Bender

I noticed some surprising behavior in one of my Ruby programs, and am
wondering what the rationale is. Basically,
given read-only access to a member variable (using attr_reader) of
class Array, I can modify that Array by modifying (what I think should
be) a local copy of it. Shouldn't the accessor return a copy of the
variable instead of a reference to it? Or is it standard practice to
hand-write an accessor for arrays that returns a clone of the array?

#!/usr/bin/ruby

class Example
attr_reader :elems

def initialize
@elems = []
3.times { @elems << Object.new }
end

def to_s
@elems.inspect
end
end

e = Example.new
puts e
# output is:
# -604282308
# [#<Object:0xb7f6c450>, #<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

list = e.elems
puts list.object_id
# output is: -604282308
list.delete_at(0)

puts e
# output is
# -604282308
# [#<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]
 
J

Jason Roelofs

I noticed some surprising behavior in one of my Ruby programs, and am
wondering what the rationale is. Basically,
given read-only access to a member variable (using attr_reader) of
class Array, I can modify that Array by modifying (what I think should
be) a local copy of it. Shouldn't the accessor return a copy of the
variable instead of a reference to it? Or is it standard practice to
hand-write an accessor for arrays that returns a clone of the array?

#!/usr/bin/ruby

class Example
attr_reader :elems

def initialize
@elems = []
3.times { @elems << Object.new }
end

def to_s
@elems.inspect
end
end

e = Example.new
puts e
# output is:
# -604282308
# [#<Object:0xb7f6c450>, #<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

list = e.elems
puts list.object_id
# output is: -604282308
list.delete_at(0)

puts e
# output is
# -604282308
# [#<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

You misunderstand what "attr_reader" does. The "attr_" methods simply
generate setter / getter methods on your class for instance variables.

attr_reader :variable

def variable; @variable; end

attr_writer :variable

def variable=(val); @variable = val; end

attr_accessor :variable creates both.

If you want something truely immutable, you have to #freeze it, but
then it's not changeable inside the class either.

If you're really worried about this, you can manually do:

class Example

def initialize
@elems = []
3.times { @elems << Object.new }
end

def to_s
@elems.inspect
end

# Always give a copy of the array, that way the internal @elems
# array is never changed.
def elems
@elems.clone
end
end

Hope that helps.

Jason R.
 
C

Chris Shea

I noticed some surprising behavior in one of my Ruby programs, and am
wondering what the rationale is. Basically,
given read-only access to a member variable (using attr_reader) of
class Array, I can modify that Array by modifying (what I think should
be) a local copy of it. Shouldn't the accessor return a copy of the
variable instead of a reference to it? Or is it standard practice to
hand-write an accessor for arrays that returns a clone of the array?

#!/usr/bin/ruby

class Example
attr_reader :elems

def initialize
@elems = []
3.times { @elems << Object.new }
end

def to_s
@elems.inspect
end
end

e = Example.new
puts e
# output is:
# -604282308
# [#<Object:0xb7f6c450>, #<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

list = e.elems
puts list.object_id
# output is: -604282308
list.delete_at(0)

puts e
# output is
# -604282308
# [#<Object:0xb7f6c43c>, #<Object:0xb7f6c428>]

This was recently discussed on this list. See:
http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/18058b9f36bdd1a7/

But the short answer is: Yes, you'll have to write your own accessor
if you don't want this behavior. All attr_reader does is define the
accessor, which just returns the object. You don't get a setter
method (elems=), but that doesn't make the object immutable.

HTH,
Chris
 
A

ara.t.howard

# Always give a copy of the array, that way the internal @elems
# array is never changed.
def elems
@elems.clone
end
end

does not work, neither does dup. both are rather shallow in ruby. if
you *really* want a copy you need

Marshal.load(Marshal.dump(@elems))

otherwise you'll end up with subtle bugs when the array is cloned, but
elements inside of it, or deeper, are not.

fyi.

a @ http://codeforpeople.com/
 
J

Jason Roelofs

does not work, neither does dup. both are rather shallow in ruby. if you
*really* want a copy you need

Marshal.load(Marshal.dump(@elems))

otherwise you'll end up with subtle bugs when the array is cloned, but
elements inside of it, or deeper, are not.

fyi.

a @ http://codeforpeople.com/


Eep, yeah, I forgot about that. #dup and #clone won't work, sorry for
that misinformation.

Jason
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,770
Messages
2,569,583
Members
45,074
Latest member
StanleyFra

Latest Threads

Top