Objects, Inheritance and Polymorphism
Object Semantics
Suppose we have a simple class Foo:
class Foo {
private int theValue;
public Foo(int v) {theValue = v;}
public int getValue() {return theValue;}
public void setValue(int v) {theValue = v;}
}
Foo x = new Foo(3);
Foo y = new Foo(5);
x.setValue(2); // x is now 2
x = y;
// x is now 5
x.setValue(3); // x and y are
now 3. Why does y change?
It is important to
understand why y changes at the end. The local variables x and y do not
represent two different locations for the Foo objects; instead, they
represent pointers to the objects. After the line x=y, x and y
point to the same object.
Polymorphism
Here is a fundamental example about classes and so-called inheritance,
also in the file poly.cs. The Child class "extends" the class Base.
However, a Child object permanently retains its identity, even when it is
assigned to a Base variable (as in b2 = new Child(4,5)). When the get()
method is called, it is the Child get method, not the Base get method.
using System;
using System.IO;
class Demo {
static void Main(string[] args) {
Base b1 = new Base(3);
Base b2 = new Child(4,5);
Console.WriteLine("b1.get() = {0}, b2.get = {1}", b1.get(), b2.get());
}
}
class Base {
private int x;
public virtual int get() {return x;}
public Base(int X) {x = X;}
}
class Child : Base {
private int y;
public override int get() {return base.get()+y;}
public Child (int X, int Y) : base(X) {y=Y;}
}
b1.get() returns 3; there is not much else it could return. However,
b2.get() returns 9. An observer who reasoned that b2 was of type Base might
try to invoke Base.get(), which would return 4.
The "polymorphism" behavior depends on get being declared virtual
in class Base, and then override in class Child. If we
leave out the override, then the call to b2.get() uses
"static" typing: b2 has type Base, so Base.get is called, and the result is
4, not 9.
ToString
Console.WriteLine is polymorphic: when you give it a non-string type, it
calls the object's ToString() method to convert it to a string. We saw
this in the ratio.cs demo.
Ratio.ToString can be called explicitly whenever needed, as r.ToString().
However, note that ToString() works more generally, using inheritance.
The master parent class Object defines ToString(); any subclass can override that definition, as is being
done here. In WriteLine(), when an Object needs to be printed then
ToString() is called implicitly; the rules of inheritance ensure that the
most specific version of ToString
is the one to be invoked.
Shapes
One classic application of this is with drawing shapes. There is a base
class Shape, and then child classes for each specific shape: Triangle,
Rectangle, Circle, etc. The base class has a draw() method, but it does
nothing (in fact, it is usually labeled abstract,
meaning it must be supplied by a subclass). If we have a list
shapeList of Shape objects (that is, a picture!) we can draw it as
follows:
foreach (Shape s in shapeList)
s.draw();
We do not have to check each shape s for what kind of
shape it is.
Zuul
The Game of Zuul came from a Java implementation in Objects First with Java
by David Barnes and Michael Kölling. Polymorphism is used extensively to
create different puzzle-room classes that behave differently from the
default Room class.
In the Game class, the main game loop is as follows:
while (! finished) {
Command command = parser.getCommand();
finished = processCommand(command);
}
The processCommand() function understands some universal commands such as
"quit", "help", "inventory" (listing the things you have with you) and
"look" (listing the things visible in the current room, although the room is
able to influence the visibility). But everything else is processed by the
room itself, via the Room.respond() method. A subclass of Room can in effect
redefine respond(), and thus create new behavior.
Overview of basic classes by file
Game.cs: contains the main game loop, above
Parser.cs, Command.cs, commandWord.cs: defines the basic mechanism for
reading commands
Item.cs and Light.cs: Items are things you can carry around with you, like
swords and flashlights. Some Items have the special property that they can
be "lighted"; some rooms (eg DarkRoom) won't show you whats' going on unless
you are carrying a light.
Room.cs: Rooms have a description and a list of exits (north, south, east,
west, up, down, though others are possible). Rooms also have the following
virtual methods:
- getShortDescription()
- getExitString()
- getLongDescription() (a combination of the above)
- getExit(): for going to a new room
- respond(Command c, List<Item> inventory): how a Room responds to
a Command. The inventory is needed for TAKE and DROP commands, but also
to check if you are carrying a light or a key or a sword.
Each room contains a list of Items found in the room; Items are portable
and can be TAKEn or DROPped.
RoomMaker.cs: creates the map of rooms. Technically this is called a directed
graph. (This isn't "just" a directed graph, or digraph; the
different links have names.)
Then there are the special rooms:
DarkRoom: if you don't have a light, only the way out is shown. No Items in
the room are visible. (And you cannot TAKE them if you guess their name)
DustyRoom: if you SWEEP, you find a WAND
LockedRoom: One exit is locked, and you can only go that way if you have a
key in your inventory. No "unlock" action is needed.
SerpentRoom: contains a giant but harmless serpent. He does block your way
back, though.
ValveRoom: You can't go DOWN unless you first TURN VALVE
ZooRoom: contains a sad zookeeper looking for a very small serpent
Downcasting
The player carries around a list of Items. How do we check this list to see
if one of the Items is a Light, and, if so, is actually Lighted? There is no
method in the Item class for asking if it is a Light, let alone the light's
status. Polymorphism, in other words, is no help to us. Polymorphism applies
only to methods that are part of the base class (and then redefined by the
derived class), not to methods (like Light.is_lighted()) that apply to the
The process of checking an object belonging to a base class (Item) to see if
it is in fact a member of a derived class, and, if so, viewing it as such,
is called downcasting.
The Light class has two static methods that do downcasting; here is
getLight() that returns the first lighted Light object on a
List<Item>:
public static Light getLight(List<Item> inv) {
int i = 0;
foreach (Item itm in inv) {
if (itm is Light) {
Light lt = itm as Light; // note (cast)
if (lt.isOn()) return lt;
}
}
return null; // get here if not found
}
Note the "if (itm is Light)" which is a test for whether
an Item is a Light, and "Light lt = itm as Light", which is the actual
conversion. If itm is not a Light, then lt becomes null; this
conversion is thus safe (even though we've already checked here that itm is
a Light, the compiler treats the two statements as independent).
Shapesworld
Suppose we have the Shape class from earlier, and we want to go through
shapeList to find each Rectangle and modify it by rounding its corners with
the Rectangle-specific method setRoundCorners(). We can do the following:
foreach (Shape s in
ShapeList) {
Rectangle r = s as Rectangle;
if (r!= null)
r.setRoundCorners();
}
If all we wanted to do was draw the rectangles, we could use this; we don't
have to downcast as we're not actually calling any method that is not
present for the Shape class:
foreach (Shape s in
ShapeList) {
if (s is Rectangle) s.draw();
}
Commands and double dispatch
I want "turn valve" to work only in the Valveroom. However, it is then
important that "turn" be at least recognized everywhere, or else someone
might erroneously conclude that "turn" isn't a game command at all. In other
rooms, the response is "Hmm.. I don't seem to know how to turn here!" which
is a clue that "turn" might work somewhere else. How do I do this?
In the Room class, there is a catchall: if the verb word is legitimate (as
determined by the CommandWords class), then the Room.response() is "Hmm ...
I don't seem to know how to " + commandWord + "here!".
This means that each new verb must be entered into the CommandWords class.
It would also be possible to add to CommandWords a default response for each
verb to be used in rooms that didn't "understand" that verb. In this case we
might have a polymorphic verb.response() method; each verb would have its
own class and would have its own response().
This is double dispatch: we want response() to depend on
both the room and the verb,
polymorphically. There are languages that allow this directly, but C# (and
most OO languages) are a little more constrained. If we took that route, we
would first try room.response(verb) and then, if that "failed", would try
verb.response() (probably built into the default Room.response().