Ruby: Pass-by-Reference-Value
Source: Dev.to
Most Ruby developers are familiar with the concept often referred to as “pass‑by‑reference‑value.”
When a mutable object (e.g., a String) is assigned to another variable, both variables reference the same object. Mutating the object through one reference is reflected in the other.
Simple Assignment Example
var1 = "Hello, World"
var2 = var1
var2.gsub!("World", "Ruby")
puts var1 # => Hello, Ruby
Service Class Example
class DummyClass
attr_accessor :value
def initialize(value)
@value = value
end
end
dummy = DummyClass.new("Hello, World!")
puts "Before service action:"
puts dummy.value # => Hello, World!
class ServiceClass
def initialize(value)
@value = value
end
def perform_action
local_value = @value
local_value.gsub!("World", "Ruby")
end
end
ServiceClass.new(dummy.value).perform_action
puts "After service action:"
puts dummy.value # => Hello, Ruby!
Real‑World Pitfall
In a production codebase, the same behavior can become a stealthy bug. Consider an ActiveRecord model with a string attribute that is passed to a service responsible for interpolating placeholders (e.g., %%BRAND%%) within a template string:
# Hypothetical usage
Interpolator.custom_interpolate(source, brand_name)
The method name custom_interpolate suggests it returns a new string, but if the implementation uses a mutating method like gsub! on the source object, the model’s attribute is modified in memory before the record is saved. This side effect can lead to unexpected data changes throughout the application.
Best Practices
-
Prefer non‑mutating methods (
gsubinstead ofgsub!) when you only need a transformed value. -
Duplicate inputs when you want to protect the original object:
def safe_interpolate(str, replacements) str = str.dup # perform non‑mutating transformations str.gsub!("%BRAND%", replacements[:brand]) str end -
Document side effects clearly if a method intentionally mutates its arguments.
-
Write tests that verify objects are not unintentionally altered after service calls.
By being explicit about mutability and using defensive copying, you can avoid subtle bugs caused by Ruby’s pass‑by‑reference‑value semantics.