1

I was playing around with class variables, and I knew that I could override a class variable in a subclass but I didn't realize where I call a class method matters. For example;

class Vehicle
  @@wheels = 4

  def self.wheels
    @@wheels
  end
end

puts Vehicle.wheels #4
puts Vehicle.wheels #4
puts Vehicle.wheels #4

class Motorcycle < Vehicle
  @@wheels = 2
end

The puts statement outputs 4, that all makes sense to me because I figured that you @@wheels is not overridden until an object is instantiated from the Motorcycle class. So then I did this:

class Vehicle
  @@wheels = 4

  def self.wheels
    @@wheels
  end
end

class Motorcycle < Vehicle
  @@wheels = 2
end

puts Vehicle.wheels #2
puts Vehicle.wheels #2
puts Vehicle.wheels #2

Now once I move those three lines to after the class Motorcycle they output 2... even though a Motorcycle object hasn't been instantiated. This confuses me. In detail what is happening here? Is it because class variables are loaded when we try to access it? How is this possible?

  • _"until an object is instantiated from the Motorcycle class"_ – can you clarify this? You never create an instance of `Motorcycle` in the above code snippets. – Stefan Jun 08 '21 at 09:30

2 Answers2

4

First of all, instantiation has nothing to do with it; this is a class variable, not an instance variable. Your code never instantiates anything; nor, as written, does it need to. We're just talking about some classes. But classes are first-class objects in Ruby, so that's fine.

Second, order does matter, because in Ruby all statements are executable. When you say

class Motorcycle < Vehicle
  @@wheels = 2
end

...all of that code executes right now, at the moment you say it — the moment when it is encountered as Ruby walks down the page obeying instructions. That code is not a mere template to be obeyed at some future time; it says: "create a Motorcycle class and set its class variable wheels to 2, now."

Third, you're not "overriding" anything. In the class Motorcycle, @@wheels is the very same class variable @@wheels that you defined for Vehicle earlier. It is not some other variable that "overrides" it. This is a single value that works like a kind of namespaced global, reachable from the class, any subclasses, and any instances thereof.

For more information, see (e.g.):

If you want something you can override, what you're looking for is probably more like an instance variable in a class context, often called a "class instance variable" — e.g. like this:

class Vehicle
  @wheels = 4
  class << self
    attr_reader :wheels
  end
end

class Motorcycle < Vehicle
  @wheels = 2
end

puts Vehicle.wheels # 4
puts Motorcycle.wheels # 2
matt
  • 515,959
  • 87
  • 875
  • 1,141
0

All occurances of @@wheels refer to the exactly same variable. First you set it to 4, then you set it to 2. Why does it surprise you, that it has the value 2? Use @wheels instead (as matt) suggested, if you want to have a separate variable for both.

Having said this (and this is the only reason I'm writing this answer, because matt has already covered everything else in his answer), your usage of this "variable" suggests that is is supposed to be an invariable property of the respective class. That is, every motorcycle is supposed to have 2 wheels. In this case, it would perhaps make more sense to make it a constant of its class:

class Motorcycle < Vehicle
  WHEELS = 2
end

class Unicycle < Vehicle
  WHEELS = 1
end

and use it as

puts Motorcycle::WHEELS

If you have a variable v, where you only know that it is some subclasss of Vehicle, you can write

puts v.class::WHEELS

Whether you also define WHEELS in the class Vehicle itself, its a design question. If you intend to treat Vehicle as abstract class, it does not make sense to define a fixed number of wheels in it. For orthogonality, you could catch this case by doing a

class Vehicle
  WHEELS = nil
end

if you want to, or just don't define it and rely on the fact that your code will raise an exception, if you try to access the wheels of something which is "just a plain vehicle".

If you do want to instantiate a Vehicle, of course it may make sense to set the constants there to, i.e., 4.

However, in this case I would consider allowing individual instances having a different number of wheels. The Reliant Robin was a car with 3 wheels, and trucks have sometimes 6 or 8 wheels. For maximum generality, I would therefore make the number of wheels an instance variable, and of course you can provide a default value:

class Vehicle
  attr_reader :wheels
  def initialize(wheels=4)
    @wheels=wheels
  end
end
class Motorcycle < Vehicle
  attr_reader :wheels
  def initialize(wheels=2)
    super(wheels)
  end
end

m = Motorcycle.new
puts m.wheels
user1934428
  • 19,864
  • 7
  • 42
  • 87