Setting a class variable @@foo
in two classes B
and C
, where neither is a subclass of the other but they both include a common module A
, seems to create @@foo
separately for B
and C
, which A
cannot access:
module A; end
class B; include A; @@foo = 1 end
class C; include A; @@foo = 2 end
module A; p @@foo end # => NameError: uninitialized class variable @@foo in A
class B; p @@foo end # => 1
class C; p @@foo end # => 2
But when @@foo
is assigned in A
, which works as an ancestor to both B
and C
, the @@foo
that B
and C
access becomes the @@foo
of A
.
module A; @@foo = 3 end
class B; p @@foo end # => 3
class C; p @@foo end # => 3
What happened to the @@foo
of B
and C
? Are they deleted when any of its ancestor's @@foo
is assigned?
My notes below are taken from Metaprogramming Ruby (by Paolo Perrotta), which I happened to be reading right now just as I came across your question. I hope that these excerpts (page numbers will be in parentheses) and my explanation are helpful to you.
Keep in mind that class variables are different from class instance variables.
The class variable, on the other hand, belongs to class hierarchies. That means that it belongs to any class as well as all descendants of that class.
Here is an example from the author:
But also, since your specific question has to do not only with classes but also with modules:
So, as you look at
B.ancestors
, you will see:Similarly, for
C.ancestors
, you will see:If we keep in mind that class variables belong to class hierarchies, then the class variable
@@foo
, as soon as it is defined inModule A
(and so, the anonymous class just aboveB
that is created as soon asB
includesA
), will belong toB
(and also toC
, since it includesA
).To put it simply:
@@foo
was only defined inB
and inC
(but not inA
), thenB
had a class variable@@foo
that was different than the class variable@@foo
inC
. This is because the class variables are only accessible to that class and to all descendants. ButB
andC
are related through their ancestorA
, and not through their descendants.@@foo
was defined inA
, that class variable became inherited by all descendants ofA
- that is,B
andC
. From here on out, the reference to@@foo
in classB
is really referencing the class variable that belongs toA
. The original@@foo
which was defined inB
has beenoverwrittenreplaced (taken over by its ancestor). The same has happened to the@@foo
inC
.B
andC
can both write to and read from the same class variable@@foo
, since it belongs to their common ancestor,A
.At this point, anyone of
A
,B
, orC
can all modify@@foo
. For example:This code appears in both
rb_cvar_set
andrb_cvar_get
in MRI'svariable.c
:id
is the C-internal representation of the variable name (@@foo
).front
is the class in which the variable is currently being accessed (B
/C
).target
is the most distant ancestor in which the variable has also ever been defined (A
).If
front
andtarget
are not the same, Ruby warns thatclass variable #{id} of #{front} is overtaken by #{target}
.The variable name is then literally deleted from
front
's RCLASS_IV_TBL, so that on subsequent lookups, the search for that variable name "falls through" or "bubbles up" to the most distant ancestor in which the variable is defined.Note that this check and deletion happen not just on cvar gets, but on sets as well:
In this example, even though it's
A
's value of3
being overwritten by the value1
being set inB
, we still receive the same warning that it'sB
's class variable being overtaken byA
!While it is usually more surprising to the average Ruby coder to find that the value of their variable is changing in various, perhaps unexpected, places (i.e. in "parent"/"grandparent"/"uncle"/"cousin"/"sister" modules and classes), the trigger and the wording both indicate that the warning is actually intended to inform the coder that the variable's "source of truth" has changed.