SalmonKiller
SalmonKiller

Reputation: 2203

Ruby - passing by value

Is there a way to pass objects by value and not by reference in Ruby? For Example,

class Person
  attr_accessor :name
end
def get_name(obj)
  obj.name = "Bob"
  puts obj.name
end
jack = Person.new
jack.name = "Jack"
puts jack.name
get_name(jack)
puts jack.name

the output should be

Jack
Bob
Jack

instead of

Jack
Bob
Bob

Any help would be appreciated.

Upvotes: 3

Views: 1219

Answers (3)

Michael Stalker
Michael Stalker

Reputation: 1367

No. Ruby passes by reference, not value.

If you need to simulate passing by value, you can use Ruby's Object#clone method. In this case, you'd do something like this:

def get_name(obj)
  new_object = obj.clone
  new_object.name = "Bob"
  puts new_object.name
end

This makes a shallow copy of an object. In other words, an object's instance variables are copied, but the objects the variables reference aren't copied. If you need to do a deep copy, you can read this Stack Overflow post. Ruby doesn't have a one-method way to perform deep copies, but that post describes how to use marshalling and unmarshalling to make a deep copy.

clone and dup work very similarly, but there are some differences. According to the docs:

Object#clone

Produces a shallow copy of obj—the instance variables of obj are copied, but not the objects they reference. Copies the frozen and tainted state of obj. See also the discussion under Object#dup.

Object#dup

Produces a shallow copy of obj—the instance variables of obj are copied, but not the objects they reference. dup copies the tainted state of obj. See also the discussion under Object#clone. In general, clone and dup may have different semantics in descendant classes. While clone is used to duplicate an object, including its internal state, dup typically uses the class of the descendant object to create the new instance.

This method may have class-specific behavior. If so, that behavior will be documented under the #initialize_copy method of the class.

You can take a look at the dup and clone docs.

Edit

While my answer probably gives what the OP was looking for, it is not strictly correct, with respect to the semantics of passing by reference or value. See the other answers and comments on this page for some more discussion. You can also look at the discussion in the comments here and this post for more information.

Upvotes: 5

Denis de Bernardy
Denis de Bernardy

Reputation: 78523

Tossing in a formal answer the original question, on top of @Michael's excellent answer:

Is there a way to pass objects by value and not by reference in Ruby?

No. Absolutely not. Not possible. Really no, just forget it.

Or more precisely, if we nitpick on vocabulary, Ruby passes references to objects by value, and the passed objects are mutable as a result.

In Ruby, you must either:

  1. Manually clone/dup inputs and outputs as needed (see Michael's answer); or
  2. Assume that nobody will screw up the internals of your class should you ever decide to not do so.

Rubyists tend to pick option 2, because the internals are built that way (attr_reader, etc. return object state), because of performance reasons, and because creating deep copies of everything you return or manipulate is by no means trivial in practice.


Answering Jörg's comment:

def foo(bar) bar << ' is now mutated' end;
baz = 'Baz';
foo(baz);
puts baz # Baz is now mutated

Adding a further note/example on freezing, since this can give unexpected results as well:

foo = {foo: 'bar'}.freeze  # {:foo=>"bar"}
foo[:foo] += 'baz'         # RuntimeError: can't modify frozen Hash
foo[:foo] << 'baz'         # {:foo=>"barbaz"}

Upvotes: 2

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369536

Ruby is pass-by-value. Always. Here's a simple program which demonstrates that fact:

def foo(bar)
  bar = 'reference'
end

baz = 'value'

foo(baz)

puts "Ruby is pass-by-#{baz}"
# Ruby is pass-by-value

What you are seeing is simply shared mutable state: if an object is mutable, it can be mutated, and if that object is visible through multiple references, then that mutation will be visible through multiple references. Simple as that. It doesn't matter if you call me "Jörg" like my friends do or "son" like my mom does, if I cut my hair, both of you will see that.

If you don't want your objects to be mutated, you need to make them immutable. For example:

class Person
  attr_reader :name

  private

  attr_writer :name

  def initialize(name)
    self.name = name
  end
end

def get_name(obj)
  obj.name = "Bob"
  puts obj.name
end

jack = Person.new('Jack')

puts jack.name
# Jack

get_name(jack)
# NoMethodError: private method `name=' called for #<Person:0xdeadbeef081542 @name='Jack'>

puts jack.name
# Jack

Now, your Person objects can no longer be changed by other objects. However, note that objects referenced by your Person objects obviously can still be changed:

jack.name << ' the Ripper'

puts jack.name
# Jack the Ripper

jack.name.replace('Bob')

puts jack.name
# Bob

If you want to protect against that, you need to make sure that all the objects referenced by your Person objects are also immutable. For example like this:

class Person
  def initialize(name)
    self.name = name.freeze
  end
end

jack = Person.new('Jack)

jack.name << 'the Ripper'
# RuntimeError: can't modify frozen String

Now, your Person objects are truly immutable. (At least as "truly" as can be in a language with such powerful reflection capabilities as Ruby.)

Unfortunately, we have now done to someone else the very same thing we are trying to protect ourselves against: we are mutating their objects! In Person#initialize, we mutate the String that is passed in by freezeing it. If the method that created the Person wants to do something else with the String, they are in for a nasty surprise:

name = 'Jack'

jack = Person.new(name)

name << ' the Ripper'
# RuntimeError: can't modify frozen String

We can fix that by making a copy of the String first, before freezeing it:

class Person
  def initialize(name)
    self.name = name.dup.freeze
  end
end

name = 'Jack'

jack = Person.new(name)

name << ' the Ripper'
# => 'Jack the Ripper'

Upvotes: 2

Related Questions