Overview
Ruby strings are mutable objects by default, meaning their content can be modified after creation. String freezing converts mutable strings into immutable objects that raise FrozenError
exceptions when modification is attempted. Ruby provides the freeze
method to make strings immutable and the frozen?
method to check immutability status.
Frozen strings offer significant memory optimization through string deduplication. When multiple frozen strings contain identical content, Ruby stores only one copy in memory. The String
class automatically freezes string literals when the frozen_string_literal: true
pragma is active, making this optimization transparent to developers.
Ruby implements string freezing at the object level rather than the class level. Individual string instances can be frozen while others remain mutable. Once frozen, a string cannot be unfrozen, making immutability permanent for that object.
str = "example"
str.frozen? # => false
str.freeze
str.frozen? # => true
str << " text" # => FrozenError
String freezing interacts with Ruby's automatic string deduplication system. Identical frozen strings reference the same memory location, reducing memory usage in applications with repeated string values.
a = "test".freeze
b = "test".freeze
a.object_id == b.object_id # => true (same object)
Basic Usage
The freeze
method converts any string into an immutable object. Frozen strings cannot be modified through destructive methods like <<
, []=
, sub!
, gsub!
, or any method ending with !
that modifies the receiver.
message = "Hello"
message.freeze
# These operations raise FrozenError
message[0] = "h" # => FrozenError
message << " World" # => FrozenError
message.upcase! # => FrozenError
message.strip! # => FrozenError
Non-destructive methods work normally on frozen strings, returning new string objects rather than modifying the original:
frozen_str = "Hello World".freeze
upper_str = frozen_str.upcase # => "HELLO WORLD"
sub_str = frozen_str.sub("Hello", "Hi") # => "Hi World"
frozen_str.frozen? # => true
upper_str.frozen? # => false
sub_str.frozen? # => false
The frozen_string_literal
pragma makes all string literals frozen by default within a file. This pragma affects only string literals, not strings created through interpolation or method calls:
# frozen_string_literal: true
literal = "frozen by default"
literal.frozen? # => true
interpolated = "not #{literal}" # => false
constructed = String.new("text") # => false
String interpolation creates mutable strings regardless of the pragma setting. Each interpolation produces a new mutable string object:
# frozen_string_literal: true
name = "Alice"
greeting = "Hello, #{name}!"
greeting.frozen? # => false
greeting.freeze
greeting.frozen? # => true
Advanced Usage
String freezing integrates with metaprogramming patterns through conditional freezing based on runtime conditions. Methods can accept mutable strings and return frozen versions for caching or optimization:
class StringProcessor
def self.process_and_cache(input)
result = input.dup.strip.downcase
@cache ||= {}
@cache[result] ||= result.freeze
end
end
processed = StringProcessor.process_and_cache(" INPUT ")
processed.frozen? # => true
Module and class constant strings should typically be frozen to prevent accidental modification. Ruby automatically freezes some constant strings, but explicit freezing ensures consistency:
module Constants
DEFAULT_MESSAGE = "System ready".freeze
ERROR_MESSAGES = {
not_found: "Record not found".freeze,
invalid: "Invalid input".freeze
}.freeze
def self.get_message(key)
ERROR_MESSAGES[key] || DEFAULT_MESSAGE
end
end
Method chaining with frozen strings requires careful handling since destructive methods raise exceptions. Builder patterns can use intermediate mutable strings and freeze the final result:
class TextBuilder
def initialize(text = "")
@parts = [text.dup]
end
def append(text)
@parts << text.to_s
self
end
def prepend(text)
@parts.unshift(text.to_s)
self
end
def build(frozen: true)
result = @parts.join
frozen ? result.freeze : result
end
end
builder = TextBuilder.new("Start")
.append(" middle")
.prepend("Before ")
.build(frozen: true)
Dynamic method creation can leverage frozen strings for method names and reduce memory allocation when defining multiple similar methods:
class AttributeAccessor
GETTER_TEMPLATE = "def %s; @%s; end".freeze
SETTER_TEMPLATE = "def %s=(val); @%s = val; end".freeze
def self.attr_accessor(*names)
names.each do |name|
name_str = name.to_s.freeze
class_eval(GETTER_TEMPLATE % [name_str, name_str])
class_eval(SETTER_TEMPLATE % [name_str, name_str])
end
end
end
Performance & Memory
String freezing provides substantial memory savings through automatic deduplication. Ruby maintains an internal table of frozen strings and reuses identical content across multiple string objects:
# Without freezing - separate objects
strings = Array.new(1000) { "duplicate content" }
strings.map(&:object_id).uniq.size # => 1000
# With freezing - shared objects
frozen_strings = Array.new(1000) { "duplicate content".freeze }
frozen_strings.map(&:object_id).uniq.size # => 1
Memory usage decreases significantly when applications contain repeated string values. Configuration strings, error messages, and constant values benefit most from freezing:
# High memory usage
def generate_responses(count)
Array.new(count) { "Success" }
end
# Optimized memory usage
SUCCESS_MESSAGE = "Success".freeze
def generate_responses(count)
Array.new(count) { SUCCESS_MESSAGE }
end
String concatenation performance differs between mutable and frozen strings. Frozen strings cannot use destructive concatenation, requiring alternative approaches for building large strings:
# Inefficient with frozen strings
def build_slow(parts)
result = "".freeze
parts.each { |part| result = result + part } # Creates many objects
result
end
# Efficient approach
def build_fast(parts)
parts.join.freeze # Single concatenation operation
end
The frozen_string_literal
pragma improves application startup time by reducing string object allocation. Applications with many string literals see measurable improvements:
# frozen_string_literal: true
# These share memory automatically
ERROR_404 = "Page not found"
DEFAULT_404 = "Page not found"
FALLBACK_404 = "Page not found"
ERROR_404.object_id == DEFAULT_404.object_id # => true
String freezing interacts with garbage collection by making string objects eligible for sharing. Long-running applications accumulate fewer string objects when using frozen string literals consistently.
Common Pitfalls
The most frequent error occurs when attempting to modify frozen strings through destructive methods. This typically happens when code expects mutable strings but receives frozen ones:
def process_input(text)
text.strip! # Assumes mutable string
text.downcase! # May raise FrozenError
text
end
# Safe approach
def process_input(text)
text.strip.downcase # Always returns new string
end
String interpolation creates mutable strings even with frozen string literals, causing confusion when developers expect all strings to be frozen:
# frozen_string_literal: true
base = "template" # frozen
name = "value" # frozen
result = "#{base}: #{name}" # mutable!
result.frozen? # => false
result << " more" # Works, no error
Frozen strings cannot be used as mutable buffers for string building operations. Code that accumulates content in a single string object fails with frozen strings:
# Problematic pattern
def collect_data(items)
buffer = ""
items.each { |item| buffer << item.to_s }
buffer
end
# With frozen_string_literal: true, buffer starts frozen
# First << operation raises FrozenError
Method parameters that modify strings in-place cause issues when frozen strings are passed. Libraries expecting mutable strings may fail unexpectedly:
def normalize_text!(text)
text.gsub!(/\s+/, " ") # Modifies in place
text.strip! # May raise FrozenError
end
# Caller passes frozen string
input = "messy text".freeze
normalize_text!(input) # => FrozenError
String assignment and freezing timing creates subtle bugs when objects are frozen after references are created:
original = "mutable"
reference = original
original.freeze
# reference points to now-frozen string
reference.frozen? # => true
reference << " text" # => FrozenError
Array and hash values containing frozen strings require careful handling during bulk operations:
data = ["item1".freeze, "item2".freeze, "item3".freeze]
# This fails if any string is frozen
data.each { |item| item.upcase! } # => FrozenError
# Safe alternative
data.map!(&:upcase) # Creates new strings
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#freeze |
None | self |
Makes string immutable, returns self |
#frozen? |
None | Boolean |
Returns true if string is frozen |
#+@ |
None | String |
Returns frozen copy of string |
#-@ |
None | String |
Returns unfrozen copy of string |
Destructive Methods That Raise FrozenError
Method | Description |
---|---|
#[]= |
Assigns character or substring at index |
#<< |
Appends string or character to end |
#concat |
Appends string content |
#sub! |
Substitutes first pattern match in place |
#gsub! |
Substitutes all pattern matches in place |
#strip! |
Removes leading/trailing whitespace in place |
#chomp! |
Removes trailing record separator in place |
#chop! |
Removes last character in place |
#upcase! |
Converts to uppercase in place |
#downcase! |
Converts to lowercase in place |
#swapcase! |
Swaps character case in place |
#capitalize! |
Capitalizes first character in place |
#reverse! |
Reverses character order in place |
#squeeze! |
Removes duplicate characters in place |
#tr! |
Translates characters in place |
#delete! |
Deletes specified characters in place |
#insert |
Inserts string at specified position |
#replace |
Replaces entire string content |
#clear |
Removes all characters |
Non-Destructive Alternatives
Destructive | Non-Destructive | Returns |
---|---|---|
#strip! |
#strip |
New string without whitespace |
#sub! |
#sub |
New string with substitution |
#gsub! |
#gsub |
New string with all substitutions |
#upcase! |
#upcase |
New uppercase string |
#downcase! |
#downcase |
New lowercase string |
#reverse! |
#reverse |
New reversed string |
#squeeze! |
#squeeze |
New string without duplicates |
String Literal Pragma
# frozen_string_literal: true
Effects:
- All string literals in file become frozen
- Interpolated strings remain mutable
- String.new() creates mutable strings
- Affects only current file scope
Memory Optimization Patterns
Pattern | Memory Usage | Performance |
---|---|---|
Repeated mutable strings | High (separate objects) | Fast creation |
Repeated frozen strings | Low (shared objects) | Slower first creation |
String interpolation | Medium (new objects) | Fast |
String concatenation (+) | High (temporary objects) | Slow for many operations |
Array#join | Low (single operation) | Fast for building |
Error Handling
begin
frozen_string.method_that_modifies!
rescue FrozenError => e
# Handle immutable string error
mutable_copy = frozen_string.dup
mutable_copy.method_that_modifies!
end
Thread Safety
- Frozen strings are thread-safe by default
- Multiple threads can read frozen strings simultaneously
- No synchronization needed for frozen string access
- Mutable strings require synchronization for concurrent modification