Question

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?

Was it helpful?

Solution

This code appears in both rb_cvar_set and rb_cvar_get in MRI's variable.c:

if (front && target != front) {
    st_data_t did = id;

    if (RTEST(ruby_verbose)) {
        rb_warning("class variable %"PRIsVALUE" of %"PRIsVALUE" is overtaken by %"PRIsVALUE"",
          QUOTE_ID(id), rb_class_name(original_module(front)),
          rb_class_name(original_module(target)));
    }
    if (BUILTIN_TYPE(front) == T_CLASS) {
        st_delete(RCLASS_IV_TBL(front),&did,0);
    }
}

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 and target are not the same, Ruby warns that class 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:

$VERBOSE = true

module A; end
class B; include A; @@foo = 1; end # => 1

module A; @@foo = 3 end # => 3
class B; p @@foo = 1 end # => 1
#=> warning: class variable @@foo of B is overtaken by A


module A; p @@foo end # => 1

In this example, even though it's A's value of 3 being overwritten by the value 1 being set in B, we still receive the same warning that it's B's class variable being overtaken by A!

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.

OTHER TIPS

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.

A Class instance variable belongs to an object of class Class, and is accessible only by the class itself - not by an instance or by a subclass. (106)

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:

@@v = 1

class MyClass
  @@v = 2
end

@@v    # => 2

You get this result because class variables don't really belong to classes - they belong to class hierarchies. Since @@v is defined in the context of main, it belongs to main's class Object... and to all the descendants of Object. MyClass inherits from Object, so it ends up sharing the same class variable. (107)

But also, since your specific question has to do not only with classes but also with modules:

When you include a module in a class, Ruby will create an anonymous class that wraps the module and inserts the anonymous class in the chain, just above the including class itself. (26)

So, as you look at B.ancestors, you will see:

=> [B, A, Object, Kernel, BasicObject]

Similarly, for C.ancestors, you will see:

=> [C, A, Object, Kernel, BasicObject]

If we keep in mind that class variables belong to class hierarchies, then the class variable @@foo, as soon as it is defined in Module A (and so, the anonymous class just above B that is created as soon as B includes A), will belong to B (and also to C, since it includes A).

To put it simply:

  1. When @@foo was only defined in B and in C (but not in A), then B had a class variable @@foo that was different than the class variable @@foo in C. This is because the class variables are only accessible to that class and to all descendants. But B and C are related through their ancestor A, and not through their descendants.
  2. As soon as @@foo was defined in A, that class variable became inherited by all descendants of A - that is, B and C. From here on out, the reference to @@foo in class B is really referencing the class variable that belongs to A. The original @@foo which was defined in B has been overwritten replaced (taken over by its ancestor). The same has happened to the @@foo in C. B and C 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, or C can all modify @@foo. For example:

class B
  p @@foo  # => 3
  @@foo = 1
end

module A
  p @@foo  # => 1
end
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top