The previous article, C# OOP: Queues and Stacks with Inheritance, introduced inheritance and showed that it can help you to reuse methods and variables within one class in another class that derives from it. As I explained in that article, when we say "Queue is a List", we mean it in the sense of "Dog is a mammal".
In practice, inheritance relationships can be a little more elaborate than that. Let's take a classic OOP example - shapes:
So we have a Shape, that is the parent to everything else. It defines a GetArea() method which is expected to be defined in each subclass. The area of a square is calculated based on its length, and that of a circle is calculated based on its radius, each area calculation is specific to that particular shape.
Also, we define a rectangle as a special case of a square. That might sound counterintuitive, but in OOP it makes sense. This is because a rectangle requires more specific attributes than a square (i.e. the width). If we did it the other way round, Square would inherit a width attribute that it doesn't really need.
Anyway, as you can see, it doesn't make any sense for you to have an instance of Shape. You can have an instance of Square, or Circle, or Rectangle, and calculate their area. But how would you calculate the area of a Shape? You can't, because a Shape does not exist.
Let me give you a different example: fruit. You have probably seen apples, oranges, bananas, and many other fruit. But have you ever seen something that is a generic fruit? Things like fruit and shapes are called abstract classes: they give you a general idea of what you can do with them, but they can only be subclassed. They cannot be instantiated.
Let's see a fruity example in action. Start a new SharpDevelop C# console application, and start off with the following class:
abstract class Fruit
{
public String name;
public abstract void Eat();
}
Our Fruit has a name, which may be used directly by subclasses. It also declares a method called Eat(), which is abstract. An abstract method is intended to be implemented by subclasses (much like GetArea() in the Shape example), and does not contain any implementation. Abstract methods must be defined in abstract classes, such as Fruit. When a class is abstract, it cannot be instantiated, so trying something like this:
Fruit fruit = new Fruit();
...would cause a compile-time error:
Now, let's add some subclasses to spice things up:
class Apple : Fruit
{
public Apple(String name)
{
this.name = name;
}
public override void Eat()
{
Console.WriteLine("You eat the {0}. Crunch crunch.", this.name);
}
public void Throw(String target)
{
Console.WriteLine("You throw the {0} at the {1}", this.name, target);
}
}
class Orange : Fruit
{
public Orange(String name)
{
this.name = name;
}
public override void Eat()
{
Console.WriteLine("You eat the {0}. Squish squish.", this.name);
}
public void Squeeze()
{
Console.WriteLine("You squeeze the juicy orange into a cup.");
}
}
Each subclass conveniently has a constructor that sets the name variable inherited from the parent (Fruit). Each subclass also implements the Eat() method in its own way. The subclasses must provide an implementation for abstract methods they inherit (a compile-time error occurs if you don't). Finally, the Orange and Apple class each implement a method specific to them: Apple has a Throw() method, and Orange has a Squeeze() method. Note: If you're thinking that oranges can also be thrown, Aladdin would disagree:
You can now create instances of Apple or orange and use them as you like:
Apple apple = new Apple("green apple");
apple.Eat();
apple.Throw("guard");
This gives you the following output:
An Apple is always a Fruit. Although you can't instantiate Fruit directly, you can use an Apple as a Fruit:
Fruit appleFruit = new Apple("green apple");
Since Eat() is originally declared in Fruit, you can call it on a Fruit variable without issues:
appleFruit.Eat();
You can't, however, call Throw() from a Fruit because Throw() is declared in Apple, not in Fruit. Doing this:
apple.Throw("guard");
...results in a compile-time error.
The ability to treat different types of Fruit (Apple, Orange) as if they were Fruit allows you to work with them (using the methods and variables provided by Fruit) without needing to know what subclass they are underneath:
Fruit appleFruit = new Apple("green apple");
Fruit redAppleFruit = new Apple("red apple");
Fruit orangeFruit = new Orange("fresh orange");
List<Fruit> myFruit = new List<Fruit>();
myFruit.Add(appleFruit);
myFruit.Add(redAppleFruit);
myFruit.Add(orangeFruit);
foreach (Fruit fruit in myFruit)
fruit.Eat();
The resulting output is:
This approach is called Polymorphism, and as you can see, it has nothing to do with turning orcs into critters:
A practical example where I've used this is when making games using XNA. You can have many different DrawableGameComponents, each of which may implement its own Draw() method. You can then loop through all your game images (similar to what we did with Fruit) and call Draw() on each of them to draw them on the screen.
Fantastic. In this article we learned about abstract methods, abstract classes, and how to use them polymorphically. An abstract method is a method declaration, without the implementation, that must be implemented in subclasses. Abstract methods are declared in abstract classes, which cannot be instantiated. However, variables of an abstract class type may be used by assigning a subclass. Polymorphism is when we use members of the parent class so that we don't need to distinguish between different subclasses - instead we use the common functionality provided by the parent class.
We've covered quite a bit of ground in OOP, but there is still a lot more to learn: interfaces, overriding, overloading, and, most importantly, encapsulation. Future articles will touch upon these topics as well.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.