LaunchKit
All quizzes
Ruby 288 views

Calling += on a frozen String

Question

What is the return value of the following Ruby code?

str = "abc".freeze

str += "def"

The correct answer is

Explanation

TL;DR

str += "def" is syntactic sugar for str = str + "def". String#+ never mutates its receiver; it returns a new String, and the assignment rebinds the local variable str to that new object. The frozen "abc" is never modified, so no FrozenError is raised and the expression returns "abcdef".

Step by step

str = "abc".freeze
original = str

str += "def"          # => "abcdef"

str                   # => "abcdef"
original              # => "abc"
str.equal?(original)  # => false
str.frozen?           # => false

str + "def" allocates a fresh String containing "abcdef", then the assignment points str at it. The original frozen "abc" still exists, untouched, here kept reachable through original. Note that the result is not frozen: String#+ returns a plain new String regardless of whether its operands are frozen.

Under the hood

There is no += method in Ruby. The parser rewrites a += b into a = a + b before compilation, so the only method call involved is String#+, and the rest is variable assignment. freeze forbids mutating an object; it says nothing about the variables that reference it. Variables are just names in the local table and can always be rebound. Objects are frozen, variables are not.

Edge cases

Operations that mutate the receiver in place do raise:

str = "abc".freeze

begin
  str << "def"
rescue FrozenError => e
  e.message  # => "can't modify frozen String: \"abc\""
end

The same goes for concat, upcase!, gsub!, and every other in-place mutator: they all raise FrozenError on a frozen string. The rule of thumb: += builds a new object, << edits the existing one.

The # frozen_string_literal: true magic comment freezes every string literal in the file, and += still works there for the same reason:

# frozen_string_literal: true

str = "abc"
str.frozen?   # => true
str += "def"  # => "abcdef"
str.frozen?   # => false

Version notes

FrozenError was added in Ruby 2.5 as a subclass of RuntimeError; before that, mutating a frozen object raised a plain RuntimeError with the same message. Ruby 3.4 introduced chilled string literals: in files without a frozen_string_literal comment, mutating a string literal emits a deprecation warning (when deprecation warnings are enabled) as a step toward frozen-by-default literals.

Share this quiz

Comments

No comments yet. Be the first.

Only used to confirm and publish your comment. Never shown publicly, never shared.

Markdown: **bold**, `code`, ```fenced blocks```, > quotes, [links](url). HTML and images are not rendered.