A few other interesting things are going on in this program. Right at the top of the Thing class you will see this:
@@num_things = 0
The two @
characters at the start of this variable name, @@num_things
, define this to be a class variable. The variables we’ve used inside classes up to now have been instance variables, preceded by a single @
, like @name
. Whereas each new object (or instance) of a class assigns its own values to its own instance variables, all objects derived from a specific class share the same class variables. I have assigned 0 to the @@num_things
variable to ensure that it has a meaningful value at the outset.
Here, the @@num_things
class variable is used to keep a running total of the number of Thing objects in the game. It does this simply by incrementing the class variable (by adding 1 to it: += 1
) in its initialize
method every time a new object is created:
@@num_things += 1
If you look later in the code, you will see that I have created a Map class to contain an array of rooms. This includes a version of the to_s
method that prints information on each room in the array. Don’t worry about the implementation of the Map class right now; we’ll be looking at arrays and their methods in Chapter 4.
class Map def initialize( someRooms ) @rooms = someRooms end def to_s @rooms.each { |a_room| puts(a_room) } end end
Scroll to the code at the bottom of the file, and run the program to see how I have created and initialized all the objects and used the class variable, @@num_things
, to keep a tally of all the Thing objects that have been created.
Class Variables and Instance Variables
This diagram shows a Thing class (the rectangle) that contains a class variable, @@num_things
, and an instance variable, @name
. The three oval shapes represent “Thing objects”—that is, instances of the Thing class. When one of these objects assigns a value to its instance variable, @name
, that value affects only the @name
variable in the object itself. So here, each object has a different value for @name
. But when an object assigns a value to the class variable, @@num_things
, that value “lives inside” the Thing class and is shared by all instances of that class. Here @@num_things
equals 3, and that is true for all the Thing objects.
Digging Deeper
Every class you create will descend from one or more other classes. Here I explain the fundamentals of the Ruby class hierarchy.
Superclasses
To understand how the super
keyword works, take a look at the sample program super.rb. This contains five related classes. The Thing class is the ancestor of all the others, and from Thing descends Thing2, from Thing2 descends Thing3, from Thing3 descends Thing4, and from Thing4 descends Thing5.
super.rb
class Thing def initialize( aName, aDescription ) @name = aName @description = aDescription puts("Thing.initialize: #{self.inspect}\n\n") end def aMethod( aNewName ) @name = aNewName puts("Thing.aMethod: #{self.inspect}\n\n") end end class Thing2 < Thing def initialize( aName, aDescription ) super @fulldescription = "This is #{@name}, which is #{@description}" puts("Thing2.initialize: #{self.inspect}\n\n") end def aMethod( aNewName, aNewDescription ) super( aNewName ) puts("Thing2.aMethod: #{self.inspect}\n\n") end end class Thing3 < Thing2 def initialize( aName, aDescription, aValue ) super( aName, aDescription ) @value = aValue puts("Thing3.initialize: #{self.inspect}\n\n") end def aMethod( aNewName, aNewDescription, aNewValue ) super( aNewName, aNewDescription ) @value = aNewValue puts("Thing3.aMethod: #{self.inspect}\n\n") end end class Thing4 < Thing3 def aMethod puts("Thing4.aMethod: #{self.inspect}\n\n") end end class Thing5 < Thing4 end
Let’s take a closer look at the first three classes in this hierarchy: The Thing class has two instance variables, @name
and @description
. Thing2 also defines @fulldescription
(a string that contains @name
and @description
); Thing3 adds yet another variable, @value
.
These three classes each contain an initialize
method that sets the values of the variables when a new object is created; they also each have a method named, rather inventively, aMethod
, which changes the value of one or more variables. The descendant classes, Thing2 and Thing3, both use the super
keyword in their methods.
At the bottom of this code unit I’ve written a “main” loop that executes when you run the program. Don’t worry about the syntax of this; you’ll be learning about loops in Chapter 5. I’ve added this loop so that you can easily run the different bits of code contained in the methods, test1
to test5
. You can run the program in a command window and enter a number, 1 to 5, when prompted, or Q to quit. When you run it for the first time, type 1
at the prompt and press the enter key. This will run the test1
method containing these two lines of code:
t = Thing.new( "A Thing", "a lovely thing full of thinginess" ) t.aMethod( "A New Thing" )
The first line here creates and initializes a Thing object, and the second line calls its aMethod
method. Because the Thing class doesn’t descend from anything special, nothing very new or interesting happens here. In fact, as with all Ruby classes, Thing descends from the Object class, which is the ancestor of all other classes (with the sole exception of the BasicObject class in Ruby 1.9, as explained later in this chapter). The output uses the inspect
method to display the internal structure of the object when the Thing.initialize
and Thing.aMethod
methods are called. This is the result:
Thing.initialize: #<Thing:0x28e0290 @name="A Thing", @description="a lovely thing full of thinginess"> Thing.aMethod: #<Thing:0x28e0290 @name="A New Thing", @description="a lovely thing full of thinginess">
The inspect
method can be used with all objects and is an invaluable debugging aid. Here, it shows a hexadecimal number, which identifies this specific object followed by the string values of the @name
and @description
variables.
Now enter 2
at the prompt to run test2
, which contains the following code:
t2 = Thing2.new( "A Thing2", "a Thing2 thing of great beauty" ) t2.aMethod( "A New Thing2", "a new Thing2 description" )
This creates a Thing2 object, t2, and calls t2.aMethod
. Look carefully at the output. You will see that even though t2 is a Thing2 object, it is the Thing class’s initialize
method that is called first. And only then is the Thing2 class’s initialize
called.
Thing.initialize: #<Thing2:0x2a410a0 @name="A Thing2", @description="a Thing2 thing of great beauty"> Thing2.initialize: #<Thing2:0x2a410a0 @name="A Thing2", @description="a Thing2 thing of great beauty", @fulldescription="This is A Thing2, which is a Thing2 thing of great beauty">
To understand why this is so, look at the code of the Thing2 class’s initialize
method:
def initialize( aName, aDescription ) super @fulldescription = "This is #{@name}, which is #{@description}" puts("Thing2.initialize: #{self.inspect}\n\n") end
This uses the super
keyword to call the initialize
method of Thing2’s ancestor, or superclass. The superclass of Thing2 is Thing, as you can see from its declaration:
class Thing2 < Thing
In Ruby, when the super
keyword is used on its own (that is, without any arguments), it passes all the arguments from the current method (here Thing2.initialize
) to a method with the same name in its superclass (here Thing.initialize
). Alternatively, you can explicitly specify a list of arguments following super
. So, in this case, the following code would have the same effect:
super( aName, aDescription )
Although it is permissible to use the super
keyword all on its own, it is often preferable to explicitly specify the list of arguments to be passed to the superclass, for the sake of clarity. If you want to pass only a limited number of the arguments sent to the current method, an explicit argument list is necessary. Thing2’s aMethod
, for example, passes only the aName
argument to the initialize
method of its superclass, Thing1:
super( aNewName )
This explains why the @description
variable is not changed when Thing2.aMethod
is called.
Now if you look at Thing3, you will see that this adds one more variable, @value
. In its implementation of initialize
, it passes the two arguments, aName
and aDescription
, to its superclass, Thing2. In its turn, as you’ve already seen, Thing2’s initialize
method passes these same arguments to the initialize
method of its superclass, Thing.
With the program running, enter 3
at the prompt to view the output. The following code will execute:
t3 = Thing3.new("A Thing3", "a Thing3 full of Thing and Thing2iness",500) t3.aMethod( "A New Thing3", "and a new Thing3 description",1000)
Note how the flow of execution goes right up the hierarchy so that code in the initialize
and aMethod
methods of Thing execute before code in the matching methods of Thing2 and Thing3.
It is not obligatory to override a superclass’s methods as I have done in the examples so far. This is required only when you want to add some new behavior. Thing4 omits the initialize
method but implements the aMethod
method.
Enter 4
at the prompt to execute the following code:
t4 = Thing4.new( "A Thing4", "the nicest Thing4 you will ever see", 10 ) t4.aMethod
When you run it, notice that the first available initialize
method is called when a Thing4 object is created. This happens to be Thing3.initialize
, which, once again, also calls the initialize
methods of its ancestor classes, Thing2 and Thing. However, the aMethod
method implemented by Thing4 contains no call to its superclasses, so this executes right away, and the code in any other aMethod
methods in the ancestor classes is ignored:
def aMethod puts("Thing4.aMethod: #{self.inspect}\n\n") end
Finally, Thing5 inherits from Thing4 and doesn’t introduce any new data or methods. Enter 5
at the prompt to execute the following:
t5 = Thing5.new( "A Thing5", "a very simple Thing5", 40 ) t5.aMethod
This time, you will see that the call to new
causes Ruby to backtrack through the class hierarchy until it finds the first initialize
method. This happens to belong to Thing3 (which also calls the initialize
methods of Thing2 and Thing). The first implementation of aMethod
, however, occurs in Thing4, and there are no calls to super
, so that’s where the trail ends.
The Root of All Classes
As I mentioned earlier, all our Ruby classes will ultimately descend from the Object class. You may think of Object as the “root” or “base” class of the Ruby hierarchy. In Ruby 1.8 this is literally true—there are no classes from which Object itself descends. In Ruby 1.9, however, Object is derived from a new class called BasicObject. This new class was created to provide programmers with a very lightweight class—one that supplies only the bare minimum of methods for creating objects, testing equality, and manipulating special methods called singletons. (I’ll talk more about singletons in Chapter 7.)
The Ruby 1.9 Object class inherits the methods from BasicObject and adds a number of new methods of its own. BasicObject does not exist in Ruby 1.8, and the Object class supplies all the methods provided by the combination of BasicObject and Object in Ruby 1.9. Since all normal Ruby classes—both Ruby 1.8 and Ruby 1.9—descend from Object, you may generally think of Object as being the “root” of all other classes. Just bear in mind that in Ruby 1.9, the ultimate ancestor of all classes is BasicObject.
The root class itself has no superclass, and any attempt to locate its superclass will return nil
. You can see this for yourself by running superclasses.rb. This calls the superclass
method to climb up the class hierarchy from the Three class to the Object or BasicObject class. At each turn through the loop, the variable x
is assigned the class of x
’s immediate parent until x
equals nil
. Here class
and superclass
are methods that return references to Ruby classes rather than to objects created from those classes. The begin..until
block is one of Ruby’s looping constructs, which you’ll look at in more detail in Chapter 5.
superclasses.rb
class One end class Two < One end class Three < Two end # Create ob as instance of class Three # and display the class name ob = Three.new x = ob.class puts( x ) # now climb back through the hierarchy to # display all ancestor classes of ob begin x = x.superclass puts(x.inspect) end until x == nil
The previous code displays the following output:
Three Two One Object BasicObject # Ruby 1.9 only! nil
Constants Inside Classes
There may be times when you need to access constants (identifiers beginning with a capital letter, which are used to store nonchanging values) declared inside a class. Let’s assume you have this class:
classconsts.rb
class X A = 10 class Y end end
To access the constant A
, you would need to use the special scope resolution operator ::
like this:
X::A
Class names are constants, so this same operator gives you access to classes inside other classes. This makes it possible to create objects from “nested” classes such as class Y
inside class X
:
ob = X::Y.new
Partial Classes
In Ruby it is not obligatory to define a class all in one place. If you want, you can define a single class in separate parts of your program. When a class descends from a specific superclass, each subsequent partial (or open) class definition may optionally repeat the superclass in its definition using the <
operator.
Here I create one class, A, and another that descends from it, B:
partial_classes.rb
class A def a puts( "a" ) end end class B < A def ba1 puts( "ba1" ) end end class A def b puts( "b" ) end end class B < A def ba2 puts( "ba2" ) end end
Now, if I create a B object, all the methods of both A and B are available to it:
ob = B.new ob.a ob.b ob.ba1 ob.ba2
You can also use partial class definitions to add features to Ruby’s standard classes such as Array:
class Array def gribbit puts( "gribbit" ) end end
This adds the gribbit
method to the Array class so that the following code can now be executed:
[1,2,3].gribbit