Pass by Reference

What you're actually modifying when you pass arguments to methods and the implications it has for duplication.

Scroll down...

Content

Resources

Comments

When you pass an argument to a method, what are you actually passing? In this very brief lesson, we'll look at how these arguments are actually passed and why that might trip you up.

The key here is the idea of Pass by Reference. As you know, an object is really just a pointer to a lump of memory somewhere in the computer which actually contains its data. When you call a method and pass it an Array, for instance, you're really passing in something like 70270188437400 instead of [1,2,3,4]. This becomes important because, if you're not careful, you might accidentally modify the original object.

What Passing a Reference Does

Ruby is technically a strictly "pass-by-value" language. Yes, we've just said that it's "pass-by-reference", but it technically only acts like a pass-by-reference language. Yeah, that's meant to be deliberately confusing. For our intents and purposes, just assume it's purely pass-by-reference. Let's investigate.

When you pass an object to a method and then modify it inside the method, the original object's value is also changed:

def string_changer(str)
  str << " adding evil things"
end

> my_str = "This is my string"
> string_changer(my_str)
#=> nil
> my_str
#=> "This is my string adding evil things"

Uh oh! Our method actually modified the original string. If you read above, you're not supposed to modify the original arguments! But how does this happen?

It's because when you pass an argument to a method in Ruby, Ruby makes a copy of its value to pass into the method. But wait, wouldn't you therefore expect str in the example above to equal "This is my string?

Well, actually what str represents is a pointer (reference) to the place in memory where the actual string is stored. So the value being passed to the method as an argument is actually that reference! If you change the original string in memory, both "strings" that point to it will be changed as well.

So... if you pass an object into a method, don't use destructive functions to modify it unless you're sure you want to because you'll also be changing the original object!

Digging Deeper with Object_id

If you're having some trouble with this, use the Object#object_id method to output the reference that's actually being pointed to. Let's explore what two strings point to while we play with them:

> str = "hello"
#=> "hello"
> str.object_id
#=> 70339448145700
> str2 = "howdy"
#=> "howdy"
> str2.object_id
#=> 70339448099460  # Different...
> str = str2
#=> "howdy"
> str.object_id
#=> 70339448099460  # Same!!!
> str2 << " pardner!"  # << modifies the original string
#=> "howdy pardner!"
> str
#=> "howdy pardner!"  # Yikes! Modified str by modifying str2!
> str2 = "enough of this!"  # = creates a new string
#=> "enough of this"
> str2.object_id
#=> 70339447882820    # new id, so no longer attached to str
> str
#=> "howdy pardner!"  # No longer being modified by str2, phew.

See this Stack Overflow answer for a better and more visual clarification.

If you caught all that, it should be obvious now why passing a string into a method is effectively doing the same thing:

def string_changer(str)
  puts str.object_id
  str << " adding evil things"
  return nil
end

> str = "hi"
#=> "hi"
> str.object_id
#=> 70339447856940
> string_changer(str)
70339447856940  # see, we fed the reference into the method...
#=> nil
> str
#=> "hi adding evil things"  #... and modified the original

This same thing applies to arrays, hashes, or any other Ruby object you might pass into a method.

Why haven't you run into this before? You've probably been sticking to mostly non-destructive operations like the plain old = operator. = doesn't actually modify the original object... it first creates a brand new object in memory and then sets the original one equal to the new one.

This is in contrast to something like the shovel operator <<, which always modifies the original object in memory:

> arr = [1,2,3]
#=> [1,2,3]
> arr.object_id
#=> 70339447640040
> arr << 4
#=> [1,2,3,4]
> arr.object_id
#=> 70339447640040      # still the same...
> arr = [1,2,3,4]
#=> [1,2,3,4]
> arr.object_id
#=> 70339447477880      # new object_id!

Duplicating Objects

So how do you avoid issues with method inputs?

If you're trying to use destructive functions on the inputs to your method, there's a method called dup which will save your bacon. It duplicates an object in memory for you so you're no longer dealing with the original reference.

> arr = [1,2,3]
#=> [1,2,3]
> arr.object_id
#=> 70339447150560
> arr2 = arr.dup
#=> [1,2,3]
> arr2.object_id
#=> 70339447097360

You can use this in your methods to make sure any references that are passed are duplicates instead of the originals:

def add_to_array(arr)
  puts "arr's OLD object_id is: #{arr.object_id}!"
  arr = arr.dup
  puts "arr's NEW object_id is: #{arr.object_id}!"
  arr << "Extra item!!!"
end

> my_arr = [1,2,3]
#=> [1,2,3]
> my_arr.object_id
#=> 70339448293960
> add_to_array(my_arr)
arr's OLD object_id is: 70339448293960!
arr's NEW object_id is: 70339447999420!
#=> [1, 2, 3, "Extra item!!!"] 
> my_arr
#=> [1, 2, 3] 
> my_arr.object_id
#=> 70339448293960

Deep Duplication

A final word of caution -- dup only creates a shallow duplicate! Meaning that, if you have objects nested inside each other, it will only duplicate the outermost object while keeping any inner object references untouched.

This might come up if you try duping nested arrays, for instance:

> a = [1,2]
#=> [1,2]
> nest = [a]
#=> [[1,2]]
> nest.object_id
#=> 70339447839280
> nest[0].object_id
#=> 70339447916140
> nest_dup = nest.dup
#=> [[1,2]]
> nest_dup.object_id
#=> 70339447527260       # New ID for the outer one...
> nest_dup[0].object_id
#=> 70339447916140       # ...but not the inner one :{

This is like Inception -- the pass-by-reference problem all over again the next level down!

A classic programming exercise (and potential interview question) which we won't cover here is to write a script which performs a "deep dupe".

Wrapping Up

You don't need to be an expert at pass-by-reference right off the bat. Just try to get a basic feel for what's happening behind the scenes. In 98% of cases, this isn't an issue but in 2% it's highly frustrating if you don't know what to look for. Once you hit a few of those cases (like doing the "deep-dupes" we saw above), it'll become clearer.

See the Resources tab for more information if you'd like to dive deeper.



Sign up to track your progress for free

There are ( ) additional resources for this lesson. Check them out!

Sorry, comments aren't active just yet!

Next Lesson: Demo: Working with References