skip to content

In Ruby, how does a @@class variable differ from a class-level @instance variable when Shape has subclasses such as Circle and Square?

level: middleimportance: must knowfreq 70%

answer

  1. one variable for the whole tree
  2. class object's own @ivar
  3. subclass sees nil, not the value
  4. subclass write changes the parent
  5. per-class counters need @ivars

basics

~20 s

A @@class variable is one storage slot shared by Shape and every subclass, so a write from Circle changes Shape's value. A class-level @ivar belongs to one class object only; Circle does not inherit it and reads nil.

solid answer

~40 s

`@@count` is a **class variable**: it is created in the class where it is first assigned and shared by that class, all its subclasses and all their instances, so `Circle` writing `@@count` changes the value `Shape` sees. A **class-level instance variable** is an ordinary `@count` set in the class body or in a `def self.` method; it belongs to that one class object. Subclasses do not inherit it: `Circle.count` reads Circle's own `@count`, which is `nil` until set. Instance methods can read `@@count` directly but not the class-level `@count`: inside them `@count` is the object's own variable. So for "how many Circles vs Squares" use class-level `@count` per class; for "how many shapes in total" a single shared value is what `@@` gives.

code

ruby · 10 lines
ruby
class Shape
  @sides = 0
  def self.sides = @sides
end

class Square < Shape
end

Shape.sides   # => 0
Square.sides  # => nil: Square never set its own @sides

go deeper

for a junior

Say that @@ is shared by the class and its subclasses while an @ivar set in the class body belongs to that one class, and that subclasses read nil for it.

for a middle

Explain both in terms of the owning object, trace a per-class counter through self.class, and contrast NameError for unset @@ with nil for unset @ivars.

for a senior

Spot per-class counters or settings built on @@ that silently merge across subclasses, and replace them with class-level ivars behind def self. readers.

for a principal

Treat both as process-wide mutable state: decide which values may live on classes at all and which belong in explicit configuration objects passed around.

## Two kinds of class-level state Ruby has two ways to keep data at the level of a class rather than one object: - **Class variables**, written `@@name`. They belong to a class **and its whole subclass tree**. - **Class-level instance variables**, written `@name` but set where `self` is the class: in the class body or inside `def self.` methods. They belong to **one class object**. The second kind works because a class is itself an object, and every object can have instance variables. Nothing special happens; the object just happens to be `Shape`. ## Counting shapes both ways ```ruby class Shape @@total = 0 def self.count = @count || 0 def self.total = @@total def self.track! @count = count + 1 @@total += 1 end def initialize self.class.track! end end class Circle < Shape; end class Square < Shape; end Circle.new; Circle.new; Square.new Circle.count # => 2 Square.count # => 1 Shape.count # => 0 Shape.total # => 3 Circle.total # => 3 ``` What happened: 1. `Circle.new` calls `self.class.track!`, and `self.class` is `Circle`, so `track!` runs with `self` = `Circle`. 2. `@count` there is **Circle's** instance variable. Square's and Shape's are different variables. 3. `@@total` is looked up through the class tree, finds the one defined in `Shape`, and every class increments that single slot. ## Side by side | | `@@total` class variable | class-level `@count` | |---|---|---| | Owned by | the class that first assigns it | one class object | | Seen by subclasses | yes, the same slot | no, each class has its own | | Seen by instance methods | yes, as `@@total` | no; `@count` there is the object's | | Unset read | `NameError`: uninitialized class variable | `nil` | | Subclass write | changes the parent's value too | changes only the subclass | Two rows deserve attention: - An **unset** class variable raises `NameError`, while an unset instance variable of any object, including a class, reads `nil`. That is why `self.count` uses `@count || 0`. - A **subclass write** to `@@total` does not create a Circle-only copy when Shape already has `@@total`; it updates Shape's. If no ancestor has the variable yet, the assignment creates it in the class that runs it. ## The trap people fall into A common mistake is to reach for `@@count` to count instances "per class": ```ruby class Shape @@count = 0 def initialize @@count += 1 end def self.count = @@count end ``` Now `Circle.count`, `Square.count` and `Shape.count` all return the same total, because there is only one `@@count`. The per-class version needs class-level `@count`, as in the first example. The reverse mistake is expecting a class-level `@setting` to reach subclasses. `Circle` sees `nil` unless it sets its own or a reader falls back to `superclass`. ## Choosing between them 1. Ask **who should see a change**. If a write from `Circle` must be visible to `Shape` and `Square`, the value is tree-wide and `@@` matches that; otherwise use a class-level `@ivar`. 2. Ask **what a subclass should read by default**. A class-level `@ivar` gives `nil` in a subclass, so decide whether the reader falls back to `superclass` or the subclass must set its own value. 3. Expose either kind through `def self.` readers rather than touching the variable from many places; changing the storage later then means editing one method. Most per-class counters, registries and settings end up as class-level `@ivars`, which is why many teams treat `@@` as the exception that needs a reason. ## Why interviewers ask it It is the standard way to test whether a candidate understands that classes are objects. Someone who explains both variables in terms of **which object owns the slot** (the whole tree versus one class object) can predict every row in the table without memorising it.

  • How would you give Circle a default taken from Shape's class-level @ivar?
    Make the reader fall back up the tree, for example `def self.sides = @sides || superclass.sides` with a base case in `Shape`, or copy the parent's value into the subclass when it is first read. The value stays per class, and a subclass can still override it by setting its own `@sides`.
  • What does an instance method of Shape see when it reads @count and @@total?
    `@count` is the object's own instance variable, unrelated to the class-level one, so it is `nil` unless the object set it. `@@total` is the shared class variable, reachable from instance methods of Shape and every subclass. To read the class-level count from an instance, call `self.class.count`.

A @@ variable is the one whiteboard in the family kitchen: anyone in the family who writes on it changes what everyone reads. A class-level @ivar is each person's own notebook: Circle has one, Square has one, and a new sibling starts with an empty notebook.

saying these in an interview costs you the question

  • Each subclass gets its own copy of a @@ variable when it assigns it.
  • A class-level @count set in Shape is inherited by Circle with the same value.
  • Inside an instance method, @count reads the class-level @count.
  • Reading an unset @@ variable returns nil, like an unset @ivar.