These free mini-courses will give you a strong foundation in web development. Track your progress and access advanced courses on HTML/CSS, Ruby and JavaScript for free inside our student portal.
Scroll down...
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.
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!
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!
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
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 dup
ing 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".
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.