Vol. 4 · № 03 · Un atelier RubyA Ruby workshop

dup vs clone, ou la copie qui ne descend pas dup vs clone, or the copy that doesn't go deep

Copier un objet ne copie pas son contenu imbriqué : dup et clone ne dupliquent qu'un niveau, les sous-objets restent partagés. Le réflexe : ma copie est-elle assez profonde pour ce que je vais en faire ? Et le piège du Marshal « pour être sûr ». Copying an object doesn't copy its nested contents: dup and clone duplicate one level only, the sub-objects stay shared. The reflex: is my copy deep enough for what I'm about to do? And the Marshal 'just to be sure' trap.

AudienceAudience
Dev qui copie un objet imbriqué et le croit isolé de l'original Dev who copies a nested object and believes it isolated from the original
Format
Self-paced
ChapitresChapters
5
Date
Mar 2026 Mar 2026
≈ 18 min ●●○○ dupcloneCopie profonde
SommaireContents ·
№ 01CadrageFraming3 min

dup copie l'objet — mais une seule couche.dup copies the object — but one layer only.

On a vu geler un niveau (freeze) ; voici copier un niveau (dup). dup crée bien un nouvel objet de premier niveau : on peut y ajouter, retirer, réordonner sans toucher l'original. Mais ses sous-objets ne sont pas copiés — ce sont les MÊMES, partagés avec l'original. Muter un sous-tableau de la copie mute celui de l'original, exactement l'aliasing du № 1, glissé un cran plus bas. La copie de surface règle le partage du contenant, jamais celui du contenu.We saw freezing one level (freeze); here's copying one level (dup). dup does create a new top-level object: you can add, remove, reorder without touching the original. But its sub-objects aren't copied — they're the SAME, shared with the original. Mutating a sub-array of the copy mutates the original's, exactly the № 1 aliasing, slipped one notch down. Shallow copy settles the container's sharing, never the contents'.

La copie est neuve en surface, partagée en dessousThe copy is new on top, shared underneath
original = [[1, 2], [3, 4]]
copy = original.dup        # une copie… mais de SURFACE

copy << [5, 6]             # le 1er niveau est bien distinct
original                   # => [[1, 2], [3, 4]]   intact

copy[0] << 99              # mais copy[0] EST original[0]
original                   # => [[1, 2, 99], [3, 4]]  ← muté à distance
copy[0].equal?(original[0])  # => true   le même sous-objet, partagé
Deux contenants, un seul contenuTwo containers, one set of contents
[ · , · ]
original
[ · , · ]
copy
⟶
[1, 2]
sous-objet partagéshared sub-object

Les deux contenants sont distincts, mais leurs flèches retombent sur les mêmes sous-objets. dup a recopié le tableau extérieur, pas ce qu'il contient — le partage a juste descendu d'un étage.The two containers are distinct, but their arrows land on the same sub-objects. dup copied the outer array, not what it holds — the sharing just moved down one floor.

Le réflexe du numéroThe issue's reflex

« Ma copie est-elle assez profonde pour ce que je vais en faire ? » Si je ne touche que le premier niveau, dup suffit. Si je vais muter un sous-objet, dup ne m'isole pas : il me faut copier aussi le niveau que je compte modifier."Is my copy deep enough for what I'm about to do?" If I only touch the first level, dup is enough. If I'm going to mutate a sub-object, dup doesn't isolate me: I must also copy the level I intend to change.

Le miroir de freeze.The mirror of freeze.

Le № 2 l'annonçait : freeze gèle une couche, dup en copie une. Les deux s'arrêtent au premier niveau et laissent le contenu imbriqué tel quel. La même question revient, posée à l'envers — « jusqu'où va l'opération ? » — et la même réponse : Ruby travaille au premier niveau, c'est à toi de décider du reste.№ 2 foreshadowed it: freeze freezes one layer, dup copies one. Both stop at the first level and leave nested contents as they are. The same question returns, asked in reverse — "how far does the operation reach?" — with the same answer: Ruby works at the first level, the rest is yours to decide.

№ 02La surpriseThe surprise4 min

Le « snapshot » qui change quand l'original change.The "snapshot" that changes when the original changes.

Le cas le plus courant : on veut garder une photo d'un état avant de le modifier — un undo, une valeur de référence, un cache. On fait .dup, rassuré, puis on mute l'original… et la photo bouge avec lui. C'est que dup n'a copié que le contenant : chaque sous-objet est resté partagé entre l'original et la copie. Un tableau de tableaux, un Hash de Hash, un objet qui tient d'autres objets — partout où il y a un deuxième niveau, dup le laisse en commun. La copie ciblée, elle, duplique aussi la couche qu'on va toucher.The most common case: you want a photo of a state before changing it — an undo, a reference value, a cache. You .dup, reassured, then mutate the original… and the photo moves with it. That's because dup copied only the container: each sub-object stayed shared between original and copy. An array of arrays, a Hash of Hashes, an object holding other objects — wherever there's a second level, dup leaves it in common. The targeted copy, instead, duplicates the layer you're going to touch too.

Le snapshot bouge avec l'originalThe snapshot moves with the original
# dup : un seul niveau copié, les sous-objets restent partagés
grid = [[0, 0], [0, 0]]
snapshot = grid.dup        # « je garde une copie de l'état avant calcul »

grid[0][1] = 9             # on modifie la grille de travail…

snapshot                   # => [[0, 9], [0, 0]]
                           #   ↑ le snapshot a changé AUSSI : sous-objets partagés
snapshot.equal?(grid)      # => false  contenants distincts
snapshot[0].equal?(grid[0])  # => true   MÊME ligne dessous

grid.dup crée un tableau extérieur neuf, mais snapshot[0] et grid[0] sont la même ligne. Écrire grid[0][1] = 9 mute cette ligne partagée — donc le snapshot aussi. La photo n'en était pas une : elle pointait sur le sujet vivant.grid.dup creates a fresh outer array, but snapshot[0] and grid[0] are the same row. Writing grid[0][1] = 9 mutates that shared row — so the snapshot too. The photo wasn't one: it pointed at the living subject.

Une copie de surface d'un objet imbriqué n'est pas une photo — c'est un second cadre autour du même sujet.A shallow copy of a nested object isn't a photo — it's a second frame around the same subject.
№ 03La mécaniqueThe mechanics5 min

dup et clone copient pareil — sauf deux choses.dup and clone copy the same — except for two things.

Les deux font une copie de surface, à l'identique pour le contenu. Leur différence tient en deux points, tous deux liés au Vol 4. Un : l'état gelé. clone préserve le frozen? de l'original (cloner un objet gelé donne un objet gelé) ; dup rend toujours une copie libre, mutable. Deux : la classe singleton — les méthodes définies sur cet objet précis. clone les emporte, dup les laisse. La règle pratique : clone pour une copie fidèle à l'identité de l'objet, dup pour repartir d'une copie nue et modifiable.Both make a shallow copy, identical for the contents. Their difference is two points, both tied to Vol 4. One: the frozen state. clone preserves the original's frozen? (cloning a frozen object yields a frozen one); dup always returns a free, mutable copy. Two: the singleton class — methods defined on that precise object. clone carries them, dup leaves them. The practical rule: clone for a copy faithful to the object's identity, dup to start from a bare, modifiable copy.

L'état gelé : clone le garde, dup le perdThe frozen state: clone keeps it, dup drops it
# Différence 1 — l'état gelé : clone le copie, dup l'oublie
s = "ruby".freeze
s.dup.frozen?     # => false  dup rend une copie LIBRE
s.clone.frozen?   # => true   clone préserve le gel
s.clone(freeze: false).frozen?  # => false  on peut le forcer
La classe singleton : clone l'emporteThe singleton class: clone carries it
# Différence 2 — la classe singleton : clone la copie, dup non
obj = Object.new
def obj.special = 42        # une méthode définie sur CE seul objet

obj.clone.special          # => 42   clone emporte la classe singleton
obj.dup.special
# NoMethodError: undefined method 'special'   dup ne l'emporte pas
Fidèle vs nu.Faithful vs bare.

clone vise la copie la plus fidèle : même contenu, même gel, mêmes méthodes singleton — utile pour dupliquer un objet déjà « équipé ». dup vise une copie nue et exploitable : on repart du contenu, libre de muter, sans traîner l'identité particulière de l'original. Le paramètre clone(freeze: false) est le pont entre les deux quand on veut la fidélité du singleton mais pas le gel.clone aims for the most faithful copy: same contents, same freeze, same singleton methods — useful to duplicate an already 'equipped' object. dup aims for a bare, workable copy: you restart from the contents, free to mutate, without dragging the original's particular identity. The clone(freeze: false) option is the bridge between the two when you want the singleton fidelity but not the freeze.

Dans 95 % des cas on veut dup : on copie un Array ou un Hash pour le modifier sans toucher l'original, et ni le gel ni le singleton n'entrent en jeu. clone sert quand l'objet est son identité — un objet configuré avec des méthodes propres, ou dont on veut préserver le caractère gelé. Mais aucun des deux ne résout le vrai sujet du numéro : la profondeur. Surface ou fidélité, ils s'arrêtent tous deux au premier niveau.In 95% of cases you want dup: you copy an Array or Hash to modify it without touching the original, and neither freeze nor singleton come into play. clone serves when the object is its identity — an object configured with its own methods, or whose frozen state you want preserved. But neither solves the issue's real subject: depth. Shallow or faithful, both stop at the first level.

№ 04Le piègeThe trap4 min

La copie profonde « au cas où » : un bulldozer pour un seul niveau.The "just in case" deep copy: a bulldozer for one single level.

Le contre-exemple emblématique du numéro. Échaudé par le snapshot qui bougeait, on dégaine le réflexe Marshal.load(Marshal.dump(x)) « pour être vraiment sûr » — une copie profonde universelle. Mais c'est un bulldozer : il sérialise l'objet entier, y compris les vingt niveaux qu'on ne touchera jamais, coûte un ordre de grandeur de plus qu'une copie ciblée, et lève dès qu'un Proc, une IO ou une classe singleton se trouve dans le graphe. Quand on sait quel niveau on va muter — et on le sait presque toujours — une copie d'un cran à cet endroit précis suffit, et coûte mille fois moins.The issue's emblematic counter-example. Burned by the snapshot that moved, you reach for the Marshal.load(Marshal.dump(x)) reflex 'to be really sure' — a universal deep copy. But it's a bulldozer: it serializes the whole object, including the twenty levels you'll never touch, costs an order of magnitude more than a targeted copy, and raises the moment a Proc, an IO or a singleton class sits in the graph. When you know which level you'll mutate — and you almost always do — a one-notch copy at that precise spot is enough, and costs a thousand times less.

Marshal sérialise tout, même l'inutileMarshal serializes everything, even the useless
# Le réflexe « pour être sûr » : sérialiser TOUT l'objet, aller-retour
def snapshot(state)
  Marshal.load(Marshal.dump(state))   # copie profonde universelle
end

# … mais on ne mutait qu'UNE clé :
config = { name: "api", retries: 3, tags: ["prod"] }
copy = snapshot(config)
copy[:tags] << "beta"

# le coût caché de Marshal :
#  · sérialise l'objet ENTIER, même les niveaux qu'on ne touche pas
#  · lève sur un Proc, une IO, une lambda, une classe singleton
#  · plus lent d'un ordre de grandeur qu'une copie ciblée
Copier juste le niveau qu'on va toucherCopy just the level you'll touch
# Copie ciblée : on duplique exactement le niveau qu'on va muter
config = { name: "api", retries: 3, tags: ["prod"] }
copy = config.merge(tags: config[:tags].dup)   # 1 niveau + le seul sous-objet visé

copy[:tags] << "beta"
config[:tags]   # => ["prod"]   ← intact, sans sérialiser quoi que ce soit

# si la forme est vraiment arbitraire et profonde, Marshal se justifie —
# mais comme choix mesuré, pas comme réflexe « au cas où »
Le contre-exemple obligatoire.The mandatory counter-example.

Marshal.load(Marshal.dump(x)) est une copie profonde réelle, mais générale et coûteuse — le sérialiseur ignore quels niveaux comptent, donc il les prend tous. Dégainé par réflexe, c'est payer une sérialisation complète pour isoler une seule clé. Demande-toi d'abord quel niveau précis tu vas muter, et copie celui-là : merge(tags: ….dup), map(&:dup). Marshal reste une option valable pour un graphe vraiment arbitraire et profond — mais comme choix mesuré, jamais comme assurance tous risques.Marshal.load(Marshal.dump(x)) is a real deep copy, but general and costly — the serializer doesn't know which levels matter, so it takes them all. Reached for reflexively, it's paying a full serialization to isolate one key. First ask which precise level you'll mutate, and copy that one: merge(tags: ….dup), map(&:dup). Marshal stays a valid option for a genuinely arbitrary, deep graph — but as a measured choice, never as all-risk insurance.

« Copie profonde » n'est pas un synonyme de « sûr ». C'est copier tout ce qu'on ne va pas toucher — au prix fort."Deep copy" isn't a synonym for "safe". It's copying everything you won't touch — at full price.
№ 05Bilan & éditoWrap-up & editorial2 min

Copie la profondeur dont tu as besoin — ni plus, ni moins.Copy the depth you need — no more, no less.

dup et clone copient un seul niveau : le contenant est neuf, les sous-objets restent partagés avec l'original. Le réflexe n'est pas « copie toujours plus profond pour être sûr », c'est « quel niveau vais-je muter ? » — et copie celui-là, et lui seul. Entre dup (copie nue) et clone (copie fidèle au gel et au singleton), choisis selon l'identité que tu veux préserver. Et garde Marshal pour les graphes vraiment arbitraires, comme un choix mesuré, pas comme une assurance réflexe.dup and clone copy one level: the container is fresh, the sub-objects stay shared with the original. The reflex isn't 'always copy deeper to be safe', it's 'which level will I mutate?' — and copy that one, and only it. Between dup (bare copy) and clone (copy faithful to freeze and singleton), choose by the identity you want to preserve. And keep Marshal for genuinely arbitrary graphs, as a measured choice, not a reflexive insurance.

La questionThe questionL'outilThe toolLe réflexeThe reflex
Modifier un 1er niveauModify a first leveldup — copie de surface, libre et mutable.dup — shallow copy, free and mutable.Suffit si tu ne touches pas aux sous-objets.Enough if you don't touch the sub-objects.
Préserver gel & singletonPreserve freeze & singletonclone — copie fidèle ; clone(freeze: false) pour dégeler.clone — faithful copy; clone(freeze: false) to thaw.Quand l'objet EST son identité équipée.When the object IS its equipped identity.
Muter un sous-objetMutate a sub-objectCopie ciblée : map(&:dup), merge(k: v.dup).Targeted copy: map(&:dup), merge(k: v.dup).Copie exactement le niveau que tu vas toucher.Copy exactly the level you'll touch.
Graphe profond arbitraireArbitrary deep graphMarshal.load(Marshal.dump(x)) — copie profonde générale.Marshal.load(Marshal.dump(x)) — general deep copy.Choix mesuré, jamais réflexe « au cas où ».Measured choice, never a 'just in case' reflex.
La bonne copie n'est pas la plus profonde : c'est celle qui isole exactement le niveau que tu vas muter.The right copy isn't the deepest: it's the one isolating exactly the level you'll mutate.
Mot de l'éditeurFrom the editor

Fin du Vol 4 : l'objet est partagé, à toi de décider jusqu'où.End of Vol 4: the object is shared — you decide how far.

Trois numéros pour une seule prise de conscience : en Ruby, une variable ne contient pas un objet, elle pointe vers lui, et cet objet est presque toujours partagé. Le № 1 a nommé le danger — muter à distance. Le № 2 a donné la parade par le gel — un objet immuable se partage sans risque. Le № 3 a montré que copier ne sauve qu'au premier niveau. Le fil commun, c'est un seul mot : la profondeur. Aliasing, freeze, dup — les trois opèrent à la surface, et te laissent juge du contenu. La discipline du volume n'est pas « copie tout » ni « gèle tout », c'est : sache qui partage quoi, et n'agis qu'au niveau qui compte.Three issues for a single realization: in Ruby a variable doesn't contain an object, it points at one, and that object is almost always shared. № 1 named the danger — mutating at a distance. № 2 gave the freeze remedy — an immutable object shares without risk. № 3 showed that copying only saves at the first level. The common thread is one word: depth. Aliasing, freeze, dup — all three operate at the surface, and leave you to judge the contents. The volume's discipline isn't 'copy everything' nor 'freeze everything', it's: know who shares what, and act only at the level that matters.

Prochain volume : modules & mixins. On a passé quatre volumes à manipuler des objets ; le Vol 5 demande d'où ces objets tiennent leur comportement. Ruby n'a qu'un héritage simple, mais des modules mixés sans limite — et le vrai arbitrage n'est pas « comment partager du code », c'est « est-ce une identité (héritage) ou une capacité (mixin) ? ». On y verra la chaîne d'ancêtres, ce parcours exact que tout message emprunte — et qui boucle, enfin, sur le « tout est message » du Vol 1.Next volume: modules & mixins. We spent four volumes manipulating objects; Vol 5 asks where those objects get their behavior. Ruby has only single inheritance, but modules mixed in without limit — and the real call isn't 'how to share code', it's 'is this an identity (inheritance) or a capability (mixin)?'. We'll meet the ancestor chain, that exact path every message walks — looping back, at last, to Vol 1's 'everything is a message'.