Overview
String concatenation in Ruby combines multiple string objects into a single string. Ruby provides several concatenation operators and methods, each with distinct behavior regarding memory allocation and object mutation. The primary concatenation approaches include the +
operator for creating new strings, the <<
operator and concat
method for mutating existing strings, and string interpolation using #{}
syntax.
Ruby strings are mutable objects, making concatenation behavior dependent on whether operations modify existing strings or create new ones. This distinction affects memory usage, performance, and object identity throughout application execution.
# Creating new strings
str1 = "Hello"
str2 = str1 + " World"
str1.object_id == str2.object_id # => false
# Mutating existing strings
str1 = "Hello"
str1 << " World"
# str1 is now "Hello World", same object_id
The String
class implements concatenation through multiple pathways. The +
operator creates new string objects, while <<
and concat
modify the receiver in-place. String interpolation combines concatenation with expression evaluation, creating new strings from template patterns.
name = "Ruby"
version = 3.0
# String interpolation
message = "#{name} version #{version}" # => "Ruby version 3.0"
# Equivalent concatenation
message = name + " version " + version.to_s
Ruby's concatenation methods handle encoding automatically, converting operands to compatible encodings when possible. Concatenation operations raise Encoding::CompatibilityError
when strings have incompatible encodings that cannot be resolved.
Basic Usage
The +
operator creates new string objects by combining the contents of two strings. This operator never modifies either operand, returning a fresh string containing concatenated content.
first = "Hello"
second = "World"
result = first + " " + second # => "Hello World"
# Original strings remain unchanged
first # => "Hello"
second # => "World"
The <<
operator appends content to the receiver string, modifying the original object. This operator accepts strings, integers representing codepoints, and other objects that respond to to_str
.
message = "Processing"
message << " data" # message becomes "Processing data"
message << 32 # Append space character (ASCII 32)
message << "complete" # message becomes "Processing data complete"
The concat
method provides the same functionality as <<
with explicit method syntax. This method accepts single arguments and returns the modified receiver.
buffer = "Output: "
buffer.concat("result")
buffer.concat(" successful")
# buffer is now "Output: result successful"
Multiple concatenation operations can be chained when using mutating methods, since they return the receiver object.
log_entry = "INFO"
log_entry << " [" << Time.now.to_s << "] " << "Application started"
String interpolation provides concatenation with expression evaluation inside #{}
delimiters. Ruby calls to_s
on interpolated expressions automatically.
user_id = 1234
status = :active
profile = "User #{user_id} is #{status}" # => "User 1234 is active"
# Complex interpolation
price = 29.99
formatted = "Price: $#{'%.2f' % price} (includes tax)"
The *
operator repeats strings a specified number of times, creating new string objects.
separator = "-" * 20 # => "--------------------"
padding = " " * 4 # => " "
banner = ("=" * 40) + "\n" + "TITLE".center(40) + "\n" + ("=" * 40)
Array join operations concatenate multiple strings using a specified separator, providing an alternative to multiple concatenation operations.
words = ["Ruby", "string", "concatenation"]
sentence = words.join(" ") # => "Ruby string concatenation"
# With custom separators
csv_row = ["John", "Doe", "30"].join(",") # => "John,Doe,30"
Performance & Memory
String concatenation performance varies significantly between methods due to memory allocation patterns. The +
operator creates new string objects for each operation, leading to memory overhead and garbage collection pressure in loops or repeated operations.
# Inefficient: creates multiple temporary strings
result = ""
1000.times do |i|
result = result + "item #{i} " # Creates new string each iteration
end
The <<
operator and concat
method modify strings in-place, avoiding object creation overhead. These methods reallocate the underlying character buffer when additional capacity is needed, but reuse existing buffers when sufficient space exists.
# Efficient: modifies existing string
result = ""
1000.times do |i|
result << "item #{i} " # Modifies existing string buffer
end
String interpolation creates new strings but optimizes the allocation process by calculating required buffer size before constructing the result. This approach minimizes memory fragmentation compared to multiple +
operations.
# Optimized single allocation
name = "Ruby"
version = "3.0"
message = "Running #{name} #{version} application"
Buffer capacity management affects concatenation performance. Ruby allocates string buffers with extra capacity to accommodate future modifications, reducing reallocation frequency.
# Pre-allocate buffer capacity
buffer = String.new(capacity: 1024)
100.times { |i| buffer << "data point #{i}\n" }
Large string concatenation benefits from capacity pre-allocation or array collection with single join operations.
# Collect then join approach
parts = []
1000.times { |i| parts << "segment #{i}" }
result = parts.join(" ") # Single allocation for final result
# Pre-allocated buffer approach
result = String.new(capacity: estimated_size)
1000.times { |i| result << "segment #{i} " }
Memory usage patterns differ between concatenation methods. The +
operator's temporary objects become garbage collection candidates immediately, while mutating methods may hold larger buffers with unused capacity.
# Memory usage demonstration
def measure_concatenation(method_type)
before = GC.stat[:total_allocated_objects]
result = ""
1000.times do |i|
case method_type
when :plus
result = result + "x"
when :append
result << "x"
end
end
after = GC.stat[:total_allocated_objects]
puts "#{method_type}: #{after - before} objects allocated"
result.length
end
Encoding conversion during concatenation adds processing overhead. Operations between strings with compatible encodings avoid conversion costs, while incompatible encodings require transcoding.
# Same encoding - fast
utf8_str = "Hello".encode('UTF-8')
result = utf8_str + " World" # No encoding conversion
# Different encodings - slower
latin1_str = "Café".encode('ISO-8859-1')
utf8_str = "世界".encode('UTF-8')
# This would raise Encoding::CompatibilityError
Common Pitfalls
String concatenation in Ruby presents several subtle behaviors that can lead to unexpected results or performance problems. Understanding these patterns prevents common mistakes in string manipulation code.
Frozen string literals affect concatenation operations differently depending on the method used. The +
operator works with frozen strings since it creates new objects, but <<
and concat
raise FrozenError
when attempting to modify frozen strings.
# With frozen string literals enabled
str1 = "Hello" # Frozen literal
str2 = str1 + " World" # Works - creates new string
str3 = "Hello" # Frozen literal
str3 << " World" # Raises FrozenError - cannot modify frozen string
# Workaround: create mutable copy
str4 = "Hello".dup # Creates mutable copy
str4 << " World" # Works - modifying mutable string
Object identity confusion occurs when developers expect +
operations to modify existing strings. This misunderstanding leads to lost references and unexpected behavior in string manipulation logic.
def broken_accumulator(items)
result = ""
items.each do |item|
result = result + item # Creates new string, loses reference
end
result
end
def working_accumulator(items)
result = ""
items.each do |item|
result << item # Modifies existing string
end
result
end
Encoding compatibility errors surface during concatenation when strings have incompatible character encodings. Ruby cannot automatically resolve encoding conflicts between ASCII-compatible and non-ASCII-compatible encodings.
binary_data = "\xFF\xFE".force_encoding('BINARY')
text_data = "Hello" # UTF-8 encoding
# This raises Encoding::CompatibilityError
combined = binary_data + text_data
# Workaround: explicit encoding conversion
text_as_binary = text_data.force_encoding('BINARY')
combined = binary_data + text_as_binary
Nil concatenation attempts cause TypeError
exceptions when nil values are passed to concatenation operations. String interpolation handles nil gracefully by calling to_s
, but direct concatenation does not.
name = nil
greeting = "Hello " + name # Raises TypeError: no implicit conversion
# Safe interpolation approach
greeting = "Hello #{name}" # => "Hello "
# Explicit nil handling
greeting = "Hello " + (name || "Anonymous")
Buffer sharing between strings can cause unexpected mutations when strings share underlying character data. This occurs primarily with substring operations that share buffers with their parent strings.
original = "Hello World"
substring = original[0, 5] # May share buffer with original
substring << "!" # Could modify shared buffer
# Safe approach: force string copy
substring = original[0, 5].dup
substring << "!" # Modifies independent copy
Performance degradation in loops occurs when using +
operator for accumulation, creating quadratic time complexity due to repeated string copying.
# Quadratic performance - avoid
def slow_join(words)
result = ""
words.each { |word| result = result + word }
result
end
# Linear performance - preferred
def fast_join(words)
result = ""
words.each { |word| result << word }
result
end
Memory leaks can occur with long-lived strings that have been concatenated many times, retaining large internal buffers with significant unused capacity.
# Potential memory leak
log_buffer = ""
loop do
log_buffer << generate_log_entry()
# Buffer grows indefinitely, may retain excess capacity
# Periodic cleanup to reclaim memory
if log_buffer.length > 10_000
log_buffer = log_buffer.dup # Creates right-sized copy
end
end
Unicode normalization issues arise when concatenating strings with different Unicode forms, creating visually identical but byte-wise different results.
str1 = "café" # Composed form (é as single codepoint)
str2 = "cafe\u0301" # Decomposed form (e + combining acute accent)
combined = str1 + str2 # Visually "cafécafé" but different byte sequences
combined.length # => 9 (not 8 as might be expected)
# Normalize before concatenation
normalized = str1.unicode_normalize + str2.unicode_normalize
Reference
Core Concatenation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#+ |
other_str (String) |
String |
Creates new string by concatenating receiver with other_str |
#<< |
obj (String, Integer) |
String |
Appends obj to receiver, returns modified receiver |
#concat |
obj (String, Integer) |
String |
Appends obj to receiver, returns modified receiver |
#prepend |
obj (String, Integer) |
String |
Prepends obj to receiver, returns modified receiver |
#* |
integer (Integer) |
String |
Returns new string with receiver repeated integer times |
#% |
obj (Object, Array) |
String |
Returns formatted string using receiver as template |
Array and Enumerable Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Array#join |
separator="" (String) |
String |
Concatenates array elements with separator |
Enumerable#sum |
init="" (String) |
String |
Concatenates enumerable elements starting with init |
String Construction
Method | Parameters | Returns | Description |
---|---|---|---|
String.new |
str="" , encoding: , capacity: |
String |
Creates new string with optional capacity |
String#dup |
None | String |
Creates mutable copy of receiver |
String#clone |
freeze: (Boolean) |
String |
Creates copy of receiver preserving frozen state |
Encoding Handling
Method | Parameters | Returns | Description |
---|---|---|---|
#encode |
encoding , options (Hash) |
String |
Returns string converted to specified encoding |
#force_encoding |
encoding |
String |
Changes encoding without converting bytes |
#ascii_only? |
None | Boolean |
Returns true if string contains only ASCII characters |
#valid_encoding? |
None | Boolean |
Returns true if string is valid in its encoding |
Interpolation and Formatting
Syntax | Description | Example |
---|---|---|
"#{expr}" |
String interpolation with expression | "Value: #{var}" |
%Q{text} |
Double-quoted string with custom delimiter | %Q{He said "Hello"} |
%q{text} |
Single-quoted string with custom delimiter | %q{No interpolation here} |
"text" % args |
Printf-style formatting | "Name: %s" % name |
Performance Characteristics
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
str1 + str2 |
O(n + m) | O(n + m) | Creates new string |
str << other |
O(m) amortized | O(1) amortized | May reallocate buffer |
"#{str1}#{str2}" |
O(n + m) | O(n + m) | Single allocation |
[str1, str2].join |
O(n + m) | O(n + m) | Efficient for multiple strings |
str * n |
O(n × m) | O(n × m) | Repeats string content |
Error Conditions
Exception | Cause | Prevention |
---|---|---|
TypeError |
Concatenating incompatible types | Use to_s or explicit conversion |
Encoding::CompatibilityError |
Incompatible string encodings | Convert encodings before concatenation |
FrozenError |
Modifying frozen string with << |
Use + operator or create mutable copy |
ArgumentError |
Invalid encoding parameters | Validate encoding compatibility |
RangeError |
String size exceeds system limits | Check string lengths before operations |
Encoding Compatibility Matrix
Encoding 1 | Encoding 2 | Concatenation Result | Notes |
---|---|---|---|
UTF-8 | UTF-8 | UTF-8 | Direct concatenation |
ASCII | UTF-8 | UTF-8 | ASCII promoted to UTF-8 |
ISO-8859-1 | UTF-8 | Error | Incompatible encodings |
BINARY | UTF-8 | Error | Binary incompatible with text |
US-ASCII | ISO-8859-1 | ISO-8859-1 | ASCII compatible |
Memory Management Options
String Method | Capacity Behavior | When to Use |
---|---|---|
String.new(capacity: n) |
Pre-allocates buffer | Known final size |
str.dup |
Minimal buffer size | Remove excess capacity |
str << other |
Grows as needed | Incremental building |
parts.join(sep) |
Single allocation | Multiple parts known |
Thread Safety Considerations
Operation | Thread Safety | Notes |
---|---|---|
str1 + str2 |
Safe | Creates new object |
str << other |
Unsafe | Modifies shared object |
"#{interpolation}" |
Safe | Creates new object |
str.freeze |
Safe after freeze | Immutable operations only |