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

Référence partagée, ou a = b ne copie rien Shared reference, or a = b copies nothing

En Ruby, a = b ne copie pas l'objet : les deux noms désignent le même. Muter par l'un (<<, gsub!) se voit par l'autre — d'où le tableau « local » qu'un appelant retrouve modifié à distance. Le réflexe : avant de muter, qui d'autre tient ce même objet ? In Ruby, a = b doesn't copy the object: both names point at the same one. Mutating through one (<<, gsub!) shows through the other — hence the 'local' array a caller finds modified at a distance. The reflex: before mutating, who else holds this same object?

AudienceAudience
Dev venant d'un langage à valeurs, surpris par le partage de référence Dev from a value-language, surprised by reference sharing
Format
Self-paced
ChapitresChapters
5
Date
Mar 2026 Mar 2026
≈ 18 min ●●○○ Aliasingobject_idMutabilité
SommaireContents ·
№ 01CadrageFraming3 min

a = b ne copie rien. Deux noms, un seul objet.a = b copies nothing. Two names, one object.

Le dev venant de Java, Go ou C lit a = b comme « range dans b une copie de a ». En Ruby, c'est faux : une variable ne tient jamais un objet, elle tient une référence vers lui. a = b fait pointer b vers le même objet que a. Tant qu'on ne réassigne pas, muter l'objet par un nom le mute pour tous les autres — et c'est ainsi qu'un tableau « local » se retrouve modifié à distance par un appelant qu'on avait oublié.A dev from Java, Go or C reads a = b as 'put a copy of a into b'. In Ruby, that's wrong: a variable never holds an object, it holds a reference to one. a = b makes b point at the same object as a. As long as you don't reassign, mutating the object through one name mutates it for all the others — and that's how a 'local' array ends up modified at a distance by a caller you'd forgotten.

On mute a, et b change tout seulMutate a, and b changes on its own
a = "ruby"      # un objet String, quelque part en mémoire
b = a           # b ne COPIE rien : b et a désignent le MÊME objet

a << " loop"    # on mute l'objet en place (pas de réassignation)

a               # => "ruby loop"
b               # => "ruby loop"   ← b a changé sans qu'on y touche
a.object_id == b.object_id   # => true   un seul objet, deux noms
Le même objet, deux nomsOne object, two names
a
nomname
b
nomname
⟶
"ruby loop"
objet · object_id 240object · object_id 240

Les deux flèches arrivent sur le même objet. Le réassigner (a = "neuf") ne déplace que la flèche de a ; le muter (a << …) change l'objet que b regarde encore.Both arrows land on the same object. Reassigning it (a = "new") moves only a's arrow; mutating it (a << …) changes the object b is still looking at.

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

« Est-ce que je mute en place (!, <<, gsub!) un objet qui ne m'appartient pas ? » Avant chaque mutation, demande-toi qui d'autre tient ce même objet. Si la réponse est « l'appelant », tu es sur le point de le surprendre."Am I mutating in place (!, <<, gsub!) an object that isn't mine?" Before each mutation, ask who else holds this same object. If the answer is "the caller", you're about to surprise them.

Référence, pas valeur.Reference, not value.

Ruby n'a pas de « variables qui contiennent » : tout nom est une étiquette collée sur un objet. Plusieurs étiquettes peuvent désigner le même objet — c'est l'aliasing. Tant qu'on ne fait que lire ou réassigner, l'aliasing est invisible et gratuit. Le jour où l'un des détenteurs mute l'objet en place, il devient une source de bugs silencieux — visible partout, attribuable nulle part.Ruby has no 'variables that contain': every name is a label stuck on an object. Several labels can name the same object — that's aliasing. As long as you only read or reassign, aliasing is invisible and free. The day one holder mutates the object in place, it becomes a source of silent bugs — visible everywhere, attributable nowhere.

№ 02La surpriseThe surprise4 min

Muter en place, c'est modifier l'objet de tous ceux qui le tiennent.Mutating in place modifies the object of everyone who holds it.

Une méthode reçoit un objet en argument et le garde. Si elle le mute en place — un << sur un tableau, un gsub! sur une chaîne — elle ne modifie pas « sa » donnée : elle modifie l'unique objet que l'appelant tient aussi. Le bug ne saute pas aux yeux, car la mutation est correcte de son point de vue ; c'est l'appelant qui, plus tard, retrouve son tableau gonflé sans avoir rien fait. La parade tient en un mot : ne mute pas ce qu'on t'a prêté — copie d'abord, ou produis du neuf.A method receives an object as an argument and keeps it. If it mutates it in place — a << on an array, a gsub! on a string — it isn't modifying 'its' data: it's modifying the single object the caller holds too. The bug isn't obvious, because the mutation is correct from its own point of view; it's the caller who, later, finds their array swollen without having done anything. The remedy is one word: don't mutate what you were lent — copy first, or produce something new.

<< mute le tableau de l'appelant, à distance<< mutates the caller's array, at a distance
# La méthode mute l'objet que l'appelant lui a confié
class Cart
  def initialize(items) = @items = items   # on garde la référence reçue
  def add(x) = @items << x                  # << mute en place l'objet de l'appelant
end

list = ["pain"]
cart = Cart.new(list)
cart.add("lait")

list   # => ["pain", "lait"]   ← list a changé sans qu'on la touche
       #    le Cart et l'appelant tiennent le MÊME tableau

@items = items ne copie pas : @items et la variable list de l'appelant désignent le même tableau. Le << dans add le mute en place — donc list grossit aussi. Le Cart croit toucher sa donnée privée ; il touche celle de tout le monde.@items = items doesn't copy: @items and the caller's list variable name the same array. The << in add mutates it in place — so list grows too. The Cart thinks it's touching its private data; it's touching everyone's.

Mute en place vs réassigneMutate in place vs reassign

Les méthodes qui finissent par !, et <<, push, gsub!, sort!, clear, []= mutent l'objet en place : visibles par tous ses détenteurs. +, +=, map, gsub, sort retournent un nouvel objet et laissent l'ancien intact.Methods ending in !, plus <<, push, gsub!, sort!, clear, []= mutate the object in place: visible to all its holders. +, +=, map, gsub, sort return a new object and leave the old one intact.

Le ! ne veut pas dire « dangereux ».The ! doesn't mean 'dangerous'.

La convention ! signale une variante plus surprenante que sa jumelle sans bang — souvent parce qu'elle mute en place ou peut retourner nil. Mais << et push mutent sans bang, et exit! ne mute rien. Le vrai critère n'est pas le point d'exclamation : c'est « est-ce que cette méthode change l'objet, ou en produit un nouveau ? ».The ! convention flags a variant more surprising than its bang-less twin — often because it mutates in place or may return nil. But << and push mutate without a bang, and exit! mutates nothing. The real test isn't the exclamation mark: it's "does this method change the object, or produce a new one?".

Recevoir un objet n'est pas le posséder. Le muter, c'est écrire dans la mémoire d'un autre.Receiving an object isn't owning it. Mutating it is writing into someone else's memory.
№ 03La mécaniqueThe mechanics5 min

« Même contenu » et « le même objet » sont deux questions différentes."Same contents" and "the same object" are two different questions.

Quand l'aliasing devient un soupçon, Ruby donne de quoi trancher. object_id donne l'identité de chaque objet sous forme de nombre : deux noms qui partagent un object_id désignent le même objet, et muter par l'un se verra par l'autre. Et trois méthodes répondent à trois questions qu'on confond souvent : == compare la valeur, eql? la valeur et le type, equal? l'identité. Savoir laquelle on pose, c'est savoir si on parle de contenu ou d'objet.When aliasing becomes a suspicion, Ruby gives you what you need to settle it. object_id gives each object's identity as a number: two names sharing an object_id name the same object, and mutating through one will be seen through the other. And three methods answer three questions we often conflate: == compares value, eql? value and type, equal? identity. Knowing which one you're asking is knowing whether you're talking about contents or about an object.

object_id — l'identité d'un objet, en clairobject_id — an object's identity, made plain
a = "ruby"
b = "ruby"        # un SECOND objet, même contenu
c = a             # le MÊME objet que a

a == b            # => true    même contenu (équivalence)
a.equal?(b)       # => false   objets différents (identité)
a.equal?(c)       # => true    un seul et même objet

a.object_id       # => 240     l'identité, sous forme de nombre
b.object_id       # => 260     ← différent : ce n'est pas le même objet
c.object_id       # => 240     ← identique à a
==, eql?, equal? — trois questions==, eql?, equal? — three questions
# Trois questions, trois méthodes
a == b            # « même valeur ? »            — surchargeable, le défaut du code
a.eql?(b)         # « même valeur ET même type ? » — le test des clés de Hash
a.equal?(b)       # « le même objet ? »          — l'identité, ne JAMAIS surcharger

1 == 1.0          # => true    même valeur
1.eql?(1.0)       # => false   Integer vs Float : types différents
1.equal?(1.0)     # => false   objets différents
Identité vs équivalence.Identity vs equivalence.

equal? teste l'identité : « est-ce le même objet en mémoire ? ». Ruby le définit une fois pour toutes — ne le redéfinis jamais. == teste l'équivalence de valeur ; c'est lui qu'on surcharge pour ses propres classes. eql? est le == plus strict sur le type, qu'utilise Hash pour comparer ses clés (avec hash). Confondre les trois, c'est confondre « se ressemblent » et « sont le même ».equal? tests identity: "is this the same object in memory?". Ruby defines it once and for all — never redefine it. == tests value equivalence; that's the one you override for your own classes. eql? is the type-stricter == that Hash uses to compare its keys (with hash). Conflating the three conflates "look alike" and "are the same".

Détail qui éclaire le Vol 1 : certains objets n'ont pas d'identité propre. 1.object_id renvoie toujours le même nombre, partout, parce que les petits Integer, nil, true et false sont des objets immuables et uniques — il n'existe qu'un seul nil dans tout le programme. On ne peut pas les muter, donc l'aliasing sur eux est sans danger. Le piège ne concerne que les objets mutables : String, Array, Hash, et tes propres classes.A detail that lights up Vol 1: some objects have no identity of their own. 1.object_id always returns the same number, everywhere, because small Integers, nil, true and false are immutable, unique objects — there's only one nil in the whole program. You can't mutate them, so aliasing them is harmless. The trap concerns only mutable objects: String, Array, Hash, and your own classes.

№ 04Le piègeThe trap4 min

Le défaut mutable partagé : un seul tableau pour toutes les instances.The shared mutable default: one array for every instance.

Le contre-exemple emblématique du volume. On donne une valeur par défaut « vide » à un objet — une constante EMPTY = [], un attribut de classe — en croyant offrir à chaque instance son propre conteneur. Mais c'est le même objet, évalué une seule fois, que toutes reçoivent : muter l'une mute la constante, et chaque nouvelle instance naît déjà polluée par les précédentes. L'accumulation est mystérieuse précisément parce que le coupable est invisible : personne n'a « repassé » la donnée, elles partageaient toutes le même tableau depuis le début.The volume's emblematic counter-example. You give an object a default 'empty' value — a constant EMPTY = [], a class-level attribute — believing you're handing each instance its own container. But it's the same object, evaluated once, that all of them receive: mutating one mutates the constant, and each new instance is born already polluted by the previous ones. The accumulation is mysterious precisely because the culprit is invisible: no one 'passed the data along', they all shared the same array from the start.

EMPTY est unique : toutes les instances le partagentEMPTY is unique: every instance shares it
# Une constante mutable, partagée par TOUTES les instances
class Bucket
  EMPTY = []                       # UN seul tableau, pour tout le programme
  def initialize(items = EMPTY) = @items = items
  def add(x) = @items << x          # << mute EMPTY en place
  def items = @items
end

a = Bucket.new
b = Bucket.new                      # a et b reçoivent le MÊME EMPTY
a.add("pomme")

b.items          # => ["pomme"]   ← b a hérité du "pomme" de a
Bucket::EMPTY    # => ["pomme"]   ← la constante elle-même a gonflé
Un défaut [] réévalué à chaque appelA default [] re-evaluated on each call
# Un littéral par appel : chaque instance a son tableau
class Bucket
  def initialize(items = []) = @items = items  # [] réévalué à CHAQUE new
  def add(x) = @items << x
  def items = @items
end

a = Bucket.new
b = Bucket.new                      # deux tableaux distincts
a.add("pomme")

b.items   # => []   ← intact : b n'a jamais partagé le tableau de a
Le contre-exemple obligatoire.The mandatory counter-example.

La différence est minuscule et décisive. EMPTY = [] est une constante : un objet, créé une fois au chargement de la classe ; items = EMPTY le partage entre toutes les instances. Au contraire, items = [] est une expression de défaut, réévaluée à chaque appel de new qui omet l'argument — donc un tableau neuf par instance. Quand le défaut est mutable et destiné à être muté, ne le fige jamais dans une constante : laisse Ruby en fabriquer un frais à chaque fois.The difference is tiny and decisive. EMPTY = [] is a constant: one object, created once when the class loads; items = EMPTY shares it across every instance. By contrast, items = [] is a default expression, re-evaluated on each call to new that omits the argument — so a fresh array per instance. When the default is mutable and meant to be mutated, never freeze it into a constant: let Ruby make a fresh one each time.

Un défaut « vide » figé dans une constante n'est pas vide longtemps : c'est le même tableau que tout le monde remplit.An "empty" default frozen in a constant isn't empty for long: it's the one array everyone keeps filling.
№ 05Bilan & éditoWrap-up & editorial2 min

Avant de muter, demande : qui d'autre tient ce même objet ?Before mutating, ask: who else holds this same object?

En Ruby, a = b ne copie rien : les deux noms désignent le même objet, et muter par l'un se voit par l'autre. C'est gratuit tant qu'on lit ; ça devient un bug silencieux dès qu'on mute en place un objet qu'un autre tient encore. La discipline n'est pas « ne jamais muter » — c'est savoir à qui appartient l'objet : le mien, je le mute ; celui qu'on m'a prêté, je le copie ou j'en produis un neuf.In Ruby, a = b copies nothing: both names name the same object, and mutating through one shows through the other. It's free while you read; it becomes a silent bug the moment you mutate in place an object another still holds. The discipline isn't 'never mutate' — it's knowing who owns the object: mine, I mutate; the one I was lent, I copy or produce anew.

La questionThe questionL'outilThe toolLe réflexeThe reflex
« le même objet ? »'the same object?'equal? / object_id — l'identité.equal? / object_id — identity.Doute sur l'aliasing ? Compare les object_id.Doubt about aliasing? Compare the object_ids.
« même contenu ? »'same contents?'== (valeur), eql? (valeur + type).== (value), eql? (value + type).Surcharge ==, jamais equal?, pour tes classes.Override ==, never equal?, for your classes.
Je mute un argumentI'm mutating an argument<<, gsub!, sort! changent l'objet reçu.<<, gsub!, sort! change the received object.Pas le mien ? dup d'abord, ou += pour du neuf.Not mine? dup first, or += for something new.
Un défaut « vide »An 'empty' defaultConstante = [] partagée vs défaut = [] par appel.Shared constant = [] vs per-call default = [].Mutable et muté ? jamais une constante.Mutable and mutated? never a constant.
Une variable ne contient pas un objet — elle pointe vers lui. Et plusieurs doigts peuvent pointer le même.A variable doesn't contain an object — it points at one. And several fingers can point at the same one.
Mot de l'éditeurFrom the editor

Ouverture du Vol 4 : l'objet partagé qu'on modifie sans le savoir.Opening Vol 4: the shared object you modify without knowing it.

Trois volumes durant, on a envoyé des messages à des objets sans trop se demander qui d'autre les tenait. Le Vol 4 ouvre cette question, et c'est celle des bugs les plus silencieux de Ruby. Le dev venant d'un langage à valeurs — où passer un argument le copie — doit désapprendre un réflexe : ici, passer un objet, c'est en partager l'accès. La bonne nouvelle, c'est que la règle est simple et tient en une image : un nom est une flèche vers un objet, plusieurs flèches peuvent viser le même, et muter l'objet se voit au bout de toutes les flèches. Tout le volume découle de là.For three volumes we sent messages to objects without much wondering who else held them. Vol 4 opens that question, and it's the question behind Ruby's quietest bugs. A dev from a value-language — where passing an argument copies it — has to unlearn a reflex: here, passing an object shares access to it. The good news is the rule is simple and fits an image: a name is an arrow to an object, several arrows can aim at the same one, and mutating the object shows at the end of every arrow. The whole volume follows from there.

Prochain numéro : freeze & l'immutabilité. On vient de voir le danger de l'objet mutable partagé ; le numéro suivant montre l'autre côté — geler un objet pour rendre la mutation impossible, d'où les frozen string literals et le FrozenError. Si l'objet ne peut plus changer, le partager redevient parfaitement sûr : l'immutabilité, c'est l'aliasing sans le piège.Next issue: freeze & immutability. We've just seen the danger of the shared mutable object; the next issue shows the other side — freezing an object to make mutation impossible, hence frozen string literals and FrozenError. If the object can no longer change, sharing it becomes perfectly safe again: immutability is aliasing without the trap.