In Ruby, why does a seating chart built with Array.new(3, []) seat a guest in every row at once, and how do you fix it?
answer
- one object, many references
- the default value is not copied
- block runs once per index
- [[]] * 3 has the same trap
- << mutates, += rebinds
basics
~20 sArray.new(3, []) stores the same array object at all three indexes, so appending to one row with << changes every row. Array.new(3) { [] } runs the block once per index and builds three distinct rows.
solid answer
~40 s`Array.new(size, default)` evaluates `default` once and stores that **one object** in every slot. With an immutable default such as `nil` or `0` that is harmless, but with `[]` all three rows are the same array, so `chart[0] << "A1"` shows up in every row. The fix is the block form, `Array.new(3) { [] }`, which calls the block once per index and stores each return value; nested charts are `Array.new(3) { Array.new(4) }`. `[[]] * 3` and `Array.new(3, Array.new(4))` share in the same way. The bug hides behind `chart[0] += ["A1"]`, which builds a new array and rebinds only slot 0, so the rows look independent until someone uses a mutating method. `chart[0].equal?(chart[1])` confirms the sharing.
code
ruby · 14 linesshared = Array.new(3, [])
shared[0] << "A1"
shared # => [["A1"], ["A1"], ["A1"]]
shared[0].equal?(shared[1]) # => true
hidden = Array.new(3, [])
hidden[0] += ["A1"] # new array, stored in slot 0 only
hidden # => [["A1"], [], []]
chart = Array.new(3) { Array.new(4) }
chart[1][3] = "B4"
chart.dig(1, 3) # => "B4"
chart.dig(7, 0) # => nil
chart[0].equal?(chart[1]) # => falsego deeper
Recall that the second argument to Array.new is one object placed in every slot, and that the block form builds a new value per index.
Explain references versus copies, why << exposes the sharing while += hides it, and why immutable defaults are safe.
Spot the shared-default pattern in review, including [[]] * 3 and nested Array.new calls, and add a test that mutates one row and checks the rest.
Decide whether shared mutable defaults justify a lint rule or a team idiom, weighing the rare legitimate sharing against the recurring bug.
## What the second argument to Array.new does `Array.new` has several forms: - `Array.new(size)` fills `size` slots with `nil`. - `Array.new(size, default_value)` fills every slot with **the same object**, `default_value`. - `Array.new(size) { |index| ... }` calls the block once per index and stores each **return value**. The second form is the trap. Ruby evaluates the argument expression once, before `Array.new` runs, so `Array.new(3, [])` receives one empty array and places a reference to it in slots 0, 1 and 2. Ruby's own documentation calls this "a common pitfall for new Rubyists". ## A theatre chart that seats everyone at once Imagine storing a theatre's seating chart as an array of rows, each row an array of seat labels: 1. `chart = Array.new(3, [])` looks like three empty rows. 2. `chart[0] << "A1"` appends to the array in slot 0. 3. Slots 1 and 2 hold the same array, so `chart` is now `[["A1"], ["A1"], ["A1"]]`. Nothing was copied because Ruby variables and array slots hold **references**. Three slots referring to one array means one array, printed three times. ## Why << reveals it and += hides it | Operation on `chart[0]` | What happens | Other rows affected? | |---|---|---| | `chart[0] << "A1"` | mutates the shared array in place | yes | | `chart[0].insert(0, "A1")` | mutates the shared array in place | yes | | `chart[0] += ["A1"]` | builds a new array, stores it in slot 0 only | no | | `chart[0] = ["A1"]` | replaces slot 0 | no | `a += b` expands to `a = a + b`, and `Array#+` returns a new array. Code that only ever uses `+=` works by accident, and the bug appears the day someone switches to `<<` for speed. ## The fix: the block form `Array.new(3) { [] }` evaluates `[]` three times, once per call of the block, so each slot gets its own array. For a two-dimensional chart nest it: `Array.new(rows) { Array.new(seats_per_row) }`. The block receives the index, which is handy for labelling: `Array.new(3) { |i| Array.new(4) { |j| "#{(65 + i).chr}#{j + 1}" } }`. Other constructions share in the same way and need the same fix: - `[[]] * 3` repeats the reference to one inner array. - `Array.new(3, Array.new(4))` shares one inner row of four `nil`s. - `Array.new(3, [].dup)` still shares, because `dup` also runs only once. Immutable defaults are fine: `Array.new(5, 0)` shares one `Integer`, but `counts[0] += 1` stores a new integer in slot 0 and nothing can mutate the shared `0`. ## Jagged rows and labelled seats Real theatres rarely have equal rows. The block form handles that too, because each call can return a row of a different length: ```ruby seats_per_row = [8, 10, 12] chart = Array.new(seats_per_row.size) { |i| Array.new(seats_per_row[i]) } chart.map(&:size) # => [8, 10, 12] ``` Each inner `Array.new(n)` fills its row with `nil`, which is shared too, but `nil` is immutable, so assigning `chart[0][3] = "A4"` replaces one slot and touches nothing else. The rule of thumb: **sharing is only a bug when the shared object can be mutated**. ## Detecting the sharing - `chart[0].equal?(chart[1])` is `true` when two slots hold the same object. - `chart.map(&:object_id).uniq.size` counts distinct row objects. - In a test, mutate one row and assert the others are unchanged. ## Reading seats back with dig Once the chart is built correctly, `Array#dig` reads through the nesting: `chart.dig(1, 3)` is row B, seat 4. `dig` returns `nil` when a row or seat index is out of range, instead of raising `NoMethodError` on `nil` as `chart[9][3]` would. It raises `TypeError` (`String does not have #dig method`) when an intermediate element is not a container, for example `chart.dig(0, 0, 0)` when seats are strings.
- Why is Array.new(3, 0) safe for counters while Array.new(3, []) is not?Both put one object in every slot, but an `Integer` is immutable. `counts[0] += 1` computes a new integer and stores it in slot 0 only; nothing can change the shared `0` in place. Only a mutable default such as an array, a hash or a string leaks changes across slots.
- When does Array#dig return nil and when does it raise on a nested seating chart?`chart.dig(9, 0)` returns `nil` as soon as an index is out of range, so a missing row does not cause `NoMethodError`. If an intermediate value is not diggable, such as a seat string when you pass a third index, `dig` raises `TypeError` with `String does not have #dig method`.
Array.new(3, []) is like handing three ushers the same clipboard: a name written by one usher appears on all three lists. The block form hands each usher a fresh clipboard.
saying these in an interview costs you the question
- Array.new(3, []) copies the default array into each slot
- Array.new(3, [].dup) gives each row its own array
- rows[0] += ['A1'] working proves the rows are independent
- [[]] * 3 builds three separate empty rows
- Array.new(3, 0) has the same sharing bug for counters
- Array#dig raises IndexError when a row index is missing