In Ruby, why does editing a nested section of a dup-ed document template also change the original template?
answer
- instance variables copied as references
- two objects, one sections Array
- Array#dup copies element references
- mutate vs reassign
- equal? proves the sharing
basics
~20 sdup copies the template's instance variables, not the objects they reference, so copy and original share one sections Array and the same section objects. Mutating a shared section changes both; reassigning an attribute on the copy does not.
solid answer
~40 sA shallow copy duplicates only the outer object. After `draft = master.dup`, `draft.sections.equal?(master.sections)` is `true`: two variables, one Array. So `draft.sections << appendix` and `draft.sections.first.heading = "Title"` both show up in `master`. Reassigning an instance variable is different: `draft.name = "Quote"` points the copy's `@name` somewhere new and leaves the original alone. `Array#dup` and `Hash#dup` are shallow too: `sections.dup` gives a new Array holding the **same** section objects, so adding to it is independent but editing an element is not. The fix is to copy each level you intend to mutate, for example `@sections = @sections.map(&:dup)` in `initialize_copy`, or to build new objects instead of mutating shared ones.
code
ruby · 19 linesSection = Struct.new(:heading, :body)
class DocumentTemplate
attr_accessor :name, :sections
def initialize(name, sections)
@name = name
@sections = sections
end
end
master = DocumentTemplate.new("Invoice", [Section.new("Header", "ACME")])
draft = master.dup
draft.sections.equal?(master.sections) # => true, one shared Array
draft.sections.first.heading = "Title"
master.sections.first.heading # => "Title", the Section is shared
draft.name = "Quote"
master.name # => "Invoice", reassignment is localgo deeper
Recall that dup copies references, so nested Arrays, Hashes and objects are shared between the copy and the original.
Explain the difference between mutating a shared object and reassigning an instance variable, and show that Array#dup and Hash#dup are shallow too.
Diagnose leaking edits with equal? and object_id, then copy exactly the levels a copy will mutate instead of copying everything.
Prefer designs where copies rarely need deep duplication, such as immutable nested values, and set conventions for which types own copying logic.
## What a shallow copy copies In Ruby a variable or instance variable holds a **reference** to an object. When `dup` or `clone` copies an object it: 1. creates a new object of the same class; 2. copies each **instance variable's reference** into the new object; 3. leaves every referenced object exactly where it was. So after `draft = master.dup` there are two template objects but only one `@sections` Array, one set of section objects and one `@name` String, all shared. That is what **shallow** means. ## Level by level Take a template with `@name` and `@sections`, an Array of `Section` objects each with a `heading`. After `draft = master.dup`: | Operation on `draft` | Changes `master`? | Why | |---|---|---| | `draft.name = "Quote"` (writer reassigns `@name`) | no | the copy's ivar now points elsewhere | | `draft.sections << appendix` | yes | both ivars reference the same Array | | `draft.sections.first.heading = "Title"` | yes | the Section object is shared | | `draft.sections = draft.sections + [appendix]` | no | `+` builds a new Array, then the ivar is reassigned | The rule that falls out: **mutating** a shared object is visible through every reference; **reassigning** a reference is local to the object that holds it. ## Collections are shallow too `Array#dup` and `Hash#dup` follow the same rule one level down: - `copy = sections.dup` is a **new Array**, so `copy << appendix` does not touch `sections`. - Its elements are the **same objects**, so `copy.first.heading = "Title"` changes what `sections.first` sees. - Hash values behave the same way: `settings.dup[:margins] << 5` changes the original's Array value. A nested Array shows it in two lines: with `a = [[1], [2]]` and `b = a.dup`, `b << [3]` leaves `a` alone, but `b[0] << 9` makes `a` equal to `[[1, 9], [2]]`. ## Seeing the sharing When a copy seems to leak edits, check identities rather than contents: - `draft.sections.equal?(master.sections)` returns `true` when both hold the same Array. - `draft.sections.first.equal?(master.sections.first)` checks one level further down. - `object_id` gives the same answer as a number, handy in a debugger. `==` is no help here, because equal contents compare equal whether or not they are shared. ## Fixing it Choose the fix by how the copy will be used: - **Copy the levels you will mutate.** A document template usually needs its sections Array and each section copied: `@sections = @sections.map(&:dup)`. Putting that in `initialize_copy` makes every `dup` and `clone` do it automatically. - **Avoid mutating shared structure.** Build new Arrays with `+`, `map` or `reject` and reassign, so the copy never writes into shared objects. - **Freeze the master.** A frozen master template makes accidental writes raise `FrozenError`, but `freeze` is also one level deep: frozen `@sections` stops `<<`, yet a Section object inside it stays mutable unless it is frozen too. ## A worked scenario An invoicing feature keeps one master template and lets each user start a draft from it: 1. The master is loaded once at boot and shared by every request. 2. A request calls `draft = MASTER.dup` and renames the draft; the master is unaffected, because renaming reassigns an instance variable. 3. The user edits the first section's heading on the draft. The Section object is shared, so the master's heading changes too, and every later draft starts from the edited heading. 4. The bug appears only after the first edit, which makes it look intermittent. The fix is either to copy the sections in the template's copy hook or to freeze the master deeply so step 3 raises `FrozenError` at the point of the mistake. Copying every level blindly is not the goal. Copy exactly the parts the new object will change, and share the rest, which saves memory and keeps identity where it matters (for example a shared, immutable company logo).
- Given `a = [[1], [2]]` and `b = a.dup`, what are `a` and `b` after `b << [3]` and `b[0] << 9`?`a` is `[[1, 9], [2]]` and `b` is `[[1, 9], [2], [3]]`. `b << [3]` changes only the new outer Array, but `b[0]` is the same inner Array object that `a[0]` references, so appending 9 to it is visible through both.
- Why doesn't `==` help you find out whether two templates share their sections?`==` compares contents, and shared or merely identical Arrays have the same contents, so it returns true either way. Identity is what matters for mutation leaks, so use `equal?` or compare `object_id` on the nested objects.
saying these in an interview costs you the question
- dup copies every nested object, so the copy is fully independent.
- Array#dup gives new element objects as well as a new Array.
- Reassigning an attribute on the copy also changes the original.
- Freezing the copy protects the original's nested sections from edits.
- Two templates with == sections cannot be sharing the same Array.