In Ruby, how do <<, + and += differ when building a string that another variable also references?
answer
- one mutates, two allocate
- << returns the receiver itself
- returns a brand-new String
- += expands to s = s + x
- aliases see << but not +=
basics
~20 sString#<< appends in place and returns the same object, so every variable holding it sees the change. + returns a new string, and s += x is shorthand for s = s + x, which rebinds only s.
solid answer
~40 s`String#<<` mutates the receiver: it appends and returns the **same object**, so every variable or collection that holds that string sees the new text, and `buf << a << b` chains. `+` never touches the receiver; it returns a **new** `String`. `s += x` is only assignment shorthand for `s = s + x`: it builds a new string and rebinds the variable `s`, so another variable that pointed at the old string still sees the old text. Two consequences follow: `+=` works even when the original is frozen, while `<<` on a frozen string raises `FrozenError`; and in a loop, `<<` grows one buffer while `+=` allocates a fresh string on every pass. `<<` also treats an `Integer` argument as a codepoint (`s << 33` appends `"!"`), where `+` raises `TypeError`.
code
ruby · 14 linesa = +"hi"
b = a
b << "!"
a # => "hi!" (same object)
c = +"hi"
d = c
d += "!"
c # => "hi" (d was rebound)
d # => "hi!"
s = +"SMS "
s << 33 # => "SMS !" (Integer is a codepoint)
"SMS " + 33 # TypeError: no implicit conversion of Integer into Stringgo deeper
Recall that << changes the string itself and returns it, while + and += produce a new string. Be ready to predict what a second variable pointing at the same string shows.
Explain that += is rewritten to s = s + x, so it rebinds a variable rather than mutating an object. Walk through the aliasing example and the frozen-receiver difference.
Show where the difference bites in production: a helper appending to a caller's string or a constant, and loops that allocate a new string per iteration instead of reusing one buffer.
Frame the rule as ownership: mutate only buffers the current code created. Discuss how a team makes that visible through frozen literals and code review.
## Three operators, two behaviours Ruby strings are **mutable objects**. A variable does not contain the characters; it holds a reference to a `String` object, and several variables can hold references to the same one. That is why the choice between `<<`, `+` and `+=` matters: one of them changes the shared object, the other two leave it alone. | Expression | Receiver changed? | Returns | Variable rebound? | |---|---|---|---| | `s << x` | yes, appended in place | the same object `s` | no | | `s + x` | no | a new `String` | no | | `s += x` | no | a new `String` | yes, `s` now points at it | - **`String#<<`** (and its multi-argument cousin `String#concat`) is a mutator. It returns `self`, which is what makes `buf << a << b` work. - **`String#+`** is a pure function of its operands. It requires a string argument (it converts with the implicit `to_str` protocol) and returns a new object. - **`+=`** is not a method at all. The parser rewrites `s += x` into `s = s + x`, so it inherits `+`'s behaviour and adds an assignment. ## Aliasing: who sees the change **Aliasing** means two names referring to one object. It is where the three operators produce visibly different programs: ```ruby header = +"ALERT: " # a mutable string body = header # body and header name ONE object body << "Storm warning" header # => "ALERT: Storm warning" header = +"ALERT: " body = header body += "Storm warning" # body = body + "Storm warning" header # => "ALERT: " body # => "ALERT: Storm warning" ``` The same rule reaches across method calls. Ruby passes a reference to the object, so a method that runs `text << "..."` on its argument changes the caller's string, while a method that runs `text += "..."` only rebinds its own parameter. Common aliasing sources in real code: - a string stored in a constant and handed out to callers, - a string used as a default template and copied into several records, - a string placed in an array or hash and also kept in a local variable. ## The broadcast-message loop Consider assembling one large SMS broadcast body from many recipient lines: ```ruby # frozen_string_literal: true def broadcast_body(alert, recipients) body = +"" # mutable buffer body << "ALERT: " << alert << "\n" recipients.each { |r| body << r.phone << "\n" } body end ``` 1. `body` is one object for the whole loop; each `<<` extends it. 2. Written with `body += r.phone + "\n"`, each pass would allocate a new string and copy everything built so far into it, leaving the old one for the garbage collector. 3. The result is the same text; the allocation profile is not. For large broadcasts, `<<` on a private buffer (or `Array#join` over the parts) is the idiomatic choice. The `+""` at the top matters because the file has the `frozen_string_literal` comment: a bare `""` would be frozen and the first `<<` would raise `FrozenError`. ## Frozen receivers Because `+` and `+=` never modify the receiver, they work on frozen strings: - `s << "!"` on a frozen `s` raises `FrozenError` (`can't modify frozen String: "..."`). - `s += "!"` on the same `s` succeeds: it creates a new, unfrozen string and rebinds `s`. This is often the quickest way to tell the operators apart in a failing test. ## What each operator accepts - `s << "text"` appends the text. - `s << 33` treats the `Integer` as a **codepoint** and appends `"!"`; it does not call `to_s`. - `s + 33` raises `TypeError` (`no implicit conversion of Integer into String`). - `s.concat("a", "b")` appends several arguments in order and returns `s`. ## Choosing - Use `<<` when you **own** the buffer: a local string you created with `+""` or `String.new`, used to accumulate output. - Use `+` or `+=` when the string might be **shared** or frozen, or when you want the original preserved. - Never `<<` onto a string a caller passed in unless mutating it is the documented purpose of the method.
- Why does s += "!" succeed on a frozen string while s << "!" raises?`+=` expands to `s = s + "!"`. `String#+` only reads the frozen receiver and returns a new, unfrozen string, which is then bound to `s`. `<<` must write into the receiver itself, and writing into a frozen object raises `FrozenError`.
- What does String#<< return, and what does that enable?It returns the receiver itself, not a copy. That lets you chain appends such as `buf << name << ": " << text << "\n"`, all writing into one buffer. `String#concat` also returns the receiver and accepts several arguments at once.
- A method appends a suffix with << to its string argument. What changes for the caller?The caller's string changes too, because the method received a reference to the same object. If the caller passed a frozen string, the call raises `FrozenError` instead. Methods that do not own their argument should build a new string with `+` or interpolation.
<< is writing on the one shared whiteboard everyone in the room is looking at; += is copying the whiteboard onto a fresh sheet, adding your line, and walking off with the sheet.
saying these in an interview costs you the question
- << and += are interchangeable; += is just shorter syntax
- += modifies the string in place, so every alias sees the new text
- b = a copies the string, so appending to b leaves a unchanged
- s << 42 appends the characters 4 and 2 because << calls to_s
- String#+ raises FrozenError when its receiver is frozen