In Ruby, how do dup and clone differ for singleton methods and frozen state, and what does clone(freeze:) change?
answer
- both copies are shallow
- clone copies the singleton class
- dup drops extend-ed modules
- dup of a frozen object: unfrozen
- freeze: true or false overrides
basics
~20 sBoth make shallow copies. clone also copies the singleton class (singleton methods and extended modules) and the frozen state; dup copies neither, so it returns a plain, unfrozen copy. clone(freeze: true) or clone(freeze: false) sets the copy's frozen state explicitly.
solid answer
~40 s`dup` and `clone` both allocate a new object of the same class, copy the instance variables (references, not the objects behind them) and call a copy hook. `clone` additionally copies the **singleton class**, so methods defined with `def obj.x` or added with `extend` survive, and by default it copies the **frozen state**. `dup` drops both, which is why `frozen_thing.dup` is the usual way to get an editable copy. Since Ruby 3.0, `clone(freeze: true)` returns a frozen copy even of an unfrozen receiver and `clone(freeze: false)` an unfrozen one; any other value raises `ArgumentError`. Integers, Floats, Symbols and similar special values return themselves from both methods, and `42.clone(freeze: false)` raises `ArgumentError` ("can't unfreeze Integer").
code
ruby · 17 linesmodule Watermarked
def watermark = "DRAFT"
end
template = Object.new
template.extend(Watermarked)
def template.preview = "preview"
template.freeze
template.dup.frozen? # => false
template.dup.respond_to?(:watermark) # => false
template.clone.frozen? # => true
template.clone.preview # => "preview"
template.clone(freeze: false).frozen? # => false
Object.new.clone(freeze: true).frozen? # => true (Ruby 3.0+)
5.dup.equal?(5) # => true
5.clone(freeze: false) # ArgumentError: can't unfreeze Integergo deeper
Recall that dup and clone both copy shallowly, and that only clone keeps singleton methods and the frozen state.
Explain the table: which of singleton methods, extended modules and frozen state each method copies, and what clone(freeze: true) and freeze: false override.
Choose dup for editable drafts of frozen masters and clone for exact replicas, and account for special values that return themselves from both.
Decide whether objects in a codebase should ever be decorated per instance, since singleton methods make dup and clone diverge in ways reviewers miss.
## One shared core `Kernel#dup` and `Kernel#clone` start the same way. For an ordinary object each one: 1. **allocates** a new, empty object of the receiver's class (it does not call `initialize`); 2. **copies the instance variables**, so the copy's `@sections` points at the very same Array as the original's; 3. calls a **copy hook** on the new object (`initialize_dup` for `dup`, `initialize_clone` for `clone`, and both of those call `initialize_copy`). Both are therefore **shallow**: nested objects are shared, not copied. The differences are in what else travels with the copy. ## What clone keeps and dup drops | Property of the original | `dup` | `clone` | `clone(freeze: false)` | `clone(freeze: true)` | |---|---|---|---|---| | Instance variables (as references) | copied | copied | copied | copied | | Singleton methods (`def obj.preview`) | dropped | kept | kept | kept | | Modules added with `extend` | dropped | kept | kept | kept | | Frozen state | never frozen | same as original | not frozen | frozen | So `clone` means "an exact replica of this object", and `dup` means "a fresh instance of the same class with the same state". The Ruby documentation's own `dup` example extends an object with a module and shows that the `dup` copy raises `NoMethodError` for the module's method while the `clone` copy still responds. ## A document template example Imagine a frozen master template that was decorated at runtime: - `MASTER = DocumentTemplate.new("Invoice", sections)` - `MASTER.extend(Watermarked)` adds a `watermark` method to that one object - `MASTER.freeze` protects it Then: - `MASTER.dup` is **unfrozen** and has **no** `watermark` method: a clean working draft. - `MASTER.clone` is **frozen** and still has `watermark`: an identical, read-only replica. - `MASTER.clone(freeze: false)` keeps `watermark` but is editable. ## clone(freeze:) The `freeze:` keyword accepts only `true`, `false` or `nil` (the default); anything else raises `ArgumentError` ("unexpected value for freeze"). Two behaviours are worth stating precisely: - `clone(freeze: false)` returns an unfrozen copy of a frozen object while keeping singleton methods, which `dup` cannot do. - Since **Ruby 3.0**, `clone(freeze: true)` returns a frozen copy even when the receiver is not frozen, and the `freeze:` value is passed on to `initialize_clone`, so a class that overrides `initialize_clone` must accept that keyword. ## Values that are never copied Some objects are immutable and unique by nature: `nil`, `true`, `false`, Integers, Floats, Symbols, Rationals and Complex numbers. For them: - `dup` and `clone` simply **return the receiver**, so `5.dup.equal?(5)` is `true`. - `clone(freeze: false)` raises `ArgumentError`, for example `can't unfreeze Integer`, because the value is always frozen. ## Traps interviewers probe - **"clone is the deep one."** Neither method copies nested objects; the difference is singleton class and frozen state only. - **"dup keeps the object frozen."** It never does, which is exactly why `dup` is the idiomatic way to get an editable copy of a frozen value. - **Singleton methods added in tests.** A spec that stubs a method on one object by defining a singleton method sees it vanish on a `dup` copy but survive on a `clone`, which can explain a puzzling test failure. - **Copy hooks differ.** `dup` calls `initialize_dup` and `clone` calls `initialize_clone`; both default to `initialize_copy`, so a class can make the two copies behave differently when it needs to. ## Choosing between them - Use **`dup`** when you want an editable working copy, especially of something frozen, and do not want per-object decorations to leak into it. - Use **`clone`** when the copy must behave exactly like the original, including singleton methods and frozen state. - Reach for **`clone(freeze: false)`** in the rare case where you need both the decorations and mutability. Whichever you choose, remember that nested objects are still shared. If the copy will mutate a nested Array or Hash, copy that level too, typically in `initialize_copy`.
- When would you use `clone(freeze: false)` instead of `dup`?When the copy must keep the original's singleton methods or extended modules but also be editable. `dup` gives an unfrozen copy but drops those per-object decorations, while plain `clone` keeps them but also keeps the frozen state. `clone(freeze: false)` is the only one of the three that keeps the decorations and returns an unfrozen object.
- What do `dup` and `clone` return for a Symbol or an Integer?The receiver itself. These values are immutable and have a single identity, so copying would be pointless: `:draft.dup.equal?(:draft)` is true. Both methods return the object, and asking for `clone(freeze: false)` raises ArgumentError because such values are always frozen.
saying these in an interview costs you the question
- dup makes a deep copy, while clone makes a shallow one.
- dup keeps singleton methods and modules added with extend.
- The dup of a frozen object is frozen too.
- clone always returns a frozen copy.
- Integer#dup raises TypeError because numbers cannot be copied.