In Ruby, how do you override initialize_copy so that dup and clone of a document template also copy its nested sections?
answer
- hook runs on the new copy
- ivars already copied before it
- call super, then replace ivars
- initialize never runs for copies
- private automatically
basics
~10 sDefine initialize_copy(source), call super, then replace the shared instance variables with copies, such as @sections = @sections.map(&:dup). dup reaches it through initialize_dup and clone through initialize_clone, after the ivars are copied; initialize never runs.
solid answer
~40 sBoth copy methods allocate the new object, copy the instance variable references, then call a hook on the **copy**: `dup` calls `initialize_dup(source)`, `clone` calls `initialize_clone(source)` (plus `freeze:` when given, since Ruby 3.0), and both defaults call `initialize_copy(source)`. When your `initialize_copy` runs, `@sections` already points at the source's Array, so you call `super` and reassign: `@sections = @sections.map(&:dup)`. `initialize` is not called, so anything it sets up (ids, timestamps, counters) must be handled here. Ruby makes `initialize_copy`, `initialize_dup` and `initialize_clone` private automatically. For `clone` the hook runs before the frozen state is copied, so it can assign ivars even when cloning a frozen master; override `initialize_dup` or `initialize_clone` only when the two copies must differ.
code
ruby · 22 linesSection = Struct.new(:heading, :body)
class DocumentTemplate
attr_reader :name, :sections
def initialize(name, sections)
@name = name
@sections = sections
end
def initialize_copy(source)
super
@sections = @sections.map(&:dup)
end
end
master = DocumentTemplate.new("Invoice", [Section.new("Header", "ACME")]).freeze
draft = master.dup
draft.sections.first.heading = "Title"
master.sections.first.heading # => "Header"
master.clone.frozen? # => true, the hook ran before the freeze
DocumentTemplate.private_method_defined?(:initialize_copy) # => truego deeper
Recall that initialize_copy is the hook both dup and clone call on the new copy, and that it should call super.
Explain the order: allocate, copy ivars, hook, then frozen state for clone, and why the hook reassigns ivars that still point at the source's objects.
Handle what initialize never does for copies, such as ids and counters, keep Array and Hash subclasses working with super, and accept freeze: in initialize_clone.
Decide which types own copy semantics and document them, so copies of shared aggregates behave predictably across teams.
## The copy protocol Every ordinary `dup` or `clone` follows the same steps inside CRuby: 1. **Allocate** an empty object of the source's class. `initialize` is **not** called. 2. **Copy instance variables**: each ivar on the copy now references the same object as on the source. 3. For `clone` only, copy the **singleton class**. 4. Call the **hook** on the copy: `initialize_dup(source)` for `dup`, `initialize_clone(source)` or `initialize_clone(source, freeze: ...)` for `clone`. 5. The default `initialize_dup` and `initialize_clone` both call **`initialize_copy(source)`**. 6. For `clone`, apply the **frozen state** (copied, or forced by `freeze:`) after the hook returns. Because step 2 already shared everything, the hook's job is to **un-share** what the copy will mutate. ## Writing initialize_copy For a document template whose sections are edited per draft: ```ruby class DocumentTemplate attr_reader :name, :sections def initialize(name, sections) @name = name @sections = sections end def initialize_copy(source) super @sections = @sections.map(&:dup) end end ``` - `source` is the original; `self` is the new copy. - `@sections` on the copy still references the source's Array when the hook starts, so `map(&:dup)` builds a new Array of copied sections. - `@name` stays shared, which is fine if names are never mutated in place. ## Why call super `Kernel#initialize_copy` checks that the source is the same kind of object and that the copy is not frozen, and core classes put real work there. `Array`, `Hash` and `String` define `initialize_copy` as their `replace` method, which is how a `dup` of them gets its contents. A subclass of `Array` or `Hash` that overrides the hook without `super` returns copies with **no elements**. Calling `super` first keeps every class in the chain working. ## Frozen sources and clone A frozen master template is the common case: - `master.dup` runs the hook on an unfrozen copy and returns it unfrozen. - `master.clone` runs the hook **before** the frozen state is applied to the copy, so assigning `@sections` works, and then the copy is frozen. - `master.clone(freeze: false)` runs the hook and leaves the copy unfrozen. The hook runs on the copy, never on the source, so a frozen source is never written to. ## initialize_dup vs initialize_clone | Hook | Called by | Default behaviour | Override when | |---|---|---|---| | `initialize_copy(source)` | both, via the two below | class check (plus `replace` in core collections) | the copy logic is the same for both | | `initialize_dup(source)` | `dup` | calls `initialize_copy` | only `dup` needs extra work | | `initialize_clone(source, freeze: nil)` | `clone` | calls `initialize_copy` | only `clone` needs extra work; accept `freeze:` since Ruby 3.0 | All three are made **private automatically**, like `initialize`, so there is no need to write `private` and callers cannot invoke them directly. ## Pitfalls - **Relying on `initialize`**: it never runs for copies. A template that assigns `@id = SecureRandom.uuid` in `initialize` will hand every copy the same id unless the hook assigns a new one. - **Copying too deep**: duplicating shared, immutable parts (a logo, a frozen style sheet) wastes memory; copy only what a copy mutates. - **Forgetting `freeze:`** in an `initialize_clone` override: `clone(freeze: true)` passes the keyword, and a signature without it raises `ArgumentError`. - **Mutating `source`**: the hook should read the source and write only to `self`. ## Testing the hook A copy hook is easy to break silently when someone adds a new mutable instance variable, so pin its behaviour with a few checks: - the copy's nested Array is not `equal?` to the source's; - editing a section on the copy leaves the source's section unchanged; - `clone` of a frozen source returns a frozen copy and does not raise; - `clone(freeze: false)` of a frozen source returns an editable copy. When a new instance variable is added to the class, the review question is always the same: is it shared on purpose, or must the hook copy it too?
- How should an `initialize_clone` override be written on Ruby 3.0 and later?Accept the keyword and pass it on: `def initialize_clone(source, freeze: nil)` followed by `super`. Since Ruby 3.0, `clone(freeze: true)` and `clone(freeze: false)` call `initialize_clone` with that keyword, so an override that takes only `source` raises ArgumentError for those calls.
- Why does a subclass of Hash lose its entries when its `initialize_copy` override skips `super`?The generic copy step copies instance variables, not a Hash's entries. `Hash#initialize_copy` is `Hash#replace`, which fills the new Hash from the source. Skipping `super` skips that call, so the copy starts empty.
saying these in an interview costs you the question
- dup runs initialize again, so copy logic belongs in initialize.
- initialize_copy must be made public for dup and clone to call it.
- Inside initialize_copy the copy's instance variables are still nil.
- initialize_copy runs on the source object, so it must not modify self.
- Cloning a frozen object makes any assignment in initialize_copy raise FrozenError.