|
|
|
Up to this point, you have mainly seen objects used as an organizational tool. Class can do far more than organize code, however, and it's all thanks to a special ability that classes have, called "inheritance". Say that you've got several "animal" classes: Dog, Cat, Whale and Elephant. Chances are, these classes will have a lot of similar members. Each class will likely have members for storing height, weight, preferred food, habitat and so on. And why do these classes have so much in common? Because although each class represents a separate species, they are all animals. And since all animals have a height, weight, diet and habitat, each of your classes has this data, too. It's not that they're identical, mind you: the Cat class probably doesn't have a property called LengthOfTrunk, and the Elephant class might be lacking a HairballCount. But the classes all have a few basic things in common, and it's that common ground that inheritance is interested in. Let's start by writing a generic "Animal" class that has all of the things we just mentioned... class Animal { protected double _height; protected double _weight; protected string _diet; protected string _habitat; public double Height { get{ return _height; } set{ _height = value; } } public double Weight { get{ return _weight; } set{ _weight = value; } } public string Diet { get{ return _diet; } set{ _diet = value; } } public string Habitat { get{ return _habitat; } set{ _habitat = value; } } public Animal() { } } Why "protected" and not "private"? It all has to do with this inheritance thing we're about to use. Now, let's make our Cat class... class Cat: Animal { protected int _hairBallCount; public int HairBallCount { get{ return _hairBallCount; } set{ _hairBallCount = value; } } public Cat() { } } It might look like the Cat class only contains the HairBallCount property, but guess what? Because of that little ": Animal" thing, we've just told C# that Cat inherits from Animal. More to the point, Cat is an Animal, and therefore, Cat gets to use all the code in the Animal class, because Cats are Animals, too! So the Cat class also has all the properties and members that the Animal class has. Had we defined any methods in the Animal class, Cat would get those, too. Long live the Cat! Common, we're talking about infinite code reuse, here. Don't tell me that isn't cool. So, what's the deal with "protected"? Well, as you already know, "public" means that anyone, anywhere, has access to it. "private" means that it's only accessible within that class. It follows, then, that "protected" falls somewhere in between. What it basically means is that something is accessible only within that class, or within any class that inherits from it. So, since Cat inherits from Animal, it has access to all of the protected members and methods in Animal. Class1, and any other class that isn't an Animal, does not. Also, there's an important distinction here between having something, and having access to something. If we had made Animal's members private instead of protected, Cat would not have access to them from within its code. Even so, every instance of Cat would still have those members. It's just that Cat wouldn't have internal access to them, which leaves the code in Animal responsible for managing them. It gets even better. Inheritance not only saves us from writing a lot of identical code in the actual classes, it also makes coding easier on the outside, as well! Consider the following method: static void PrintAnimal(Animal animal) { Console.WriteLine("Animal Statistics:"); Console.WriteLine(" Type = " + animal.GetType().Name); Console.WriteLine(" Height = " + animal.Height); Console.WriteLine(" Weight = " + animal.Weight); Console.WriteLine(" Diet = " + animal.Diet); Console.WriteLine(" Habitat = " + animal.Habitat); } As you can plainly see, this method accepts an Animal object as a parameter, and prints its data to the console. But guess what? Because Cat inherits from Animal, we can pass in a Cat as a parameter, too! We can also pass in Dogs, Whales, Elephants and any other class that inherits from Animal. And because they all have the same data and methods that Animal does, they can all be used in the same way Animal can, as well. Do you remember back when we first learned about ArrayLists, how everything was stored as an Object? That's the reason that you can store anything in an ArrayList - because all classes and datatypes implicitly inherit from Object! Because Object is the lowest common denominator for any C# datatype, ArrayLists are able to store any C# datatype. And in case you haven't noticed, all the classes you've created so far already have ToString() methods, even though you never wrote one for them. That's because everything inherits from Object, which is where the ToString() method is defined! Of course, the one downside to the PrintAnimal() method is that it rules out using properties and methods that are specific to a particular inherited class. The Animal class does not have a LengthOfTrunk property, and so we can't code the PrintAnimal class to display it, even though the method might process the occasional Elephant. Does that mean that it's impossible to display and work with specific data from the inherited elements? Run the above method, and see what gets output as the Animal's type. You might be surprised to find that, even though the method's parameter is cleared marked as Animal, the object's type will be Cat, or Dog, or whatever the original object actually was before it was passed in. The reason for this is that, even though you're able to pass in Cats and Dogs into a method that accepts Animals, the method never actually converts your Cat object to an Animal object. C# is willing to pretend that it's an Animal object, but it still remembers all of the specific data it contained as a Cat. It's simply a matter of reminding the program of the object's original datatype, through casting: static void PrintAnimal(Animal animal) { Console.WriteLine("Animal Statistics:"); Console.WriteLine(" Type = " + animal.GetType().Name); Console.WriteLine(" Height = " + animal.Height); Console.WriteLine(" Weight = " + animal.Weight); Console.WriteLine(" Diet = " + animal.Diet); Console.WriteLine(" Habitat = " + animal.Habitat); if( animal is Cat ) { Cat cat = (Cat)animal; // Make a Cat version of "animal" Console.WriteLine(" HairBallCount = " + cat.HairballCount); } } We use the "is" keyword to test if an object is a certain type. If the test passes, then we know we are not just dealing with a generic Animal, we're actually dealing with a Cat that is masquerading as a generic Animal. It is therefore safe to cast the Animal to a Cat object, and access any specific data that it has. If we only need to access one piece of Cat-specific data, we can actually skip the Cat object declaration, and just cast "animal" for the one line that we actually need it to function as a Cat: Console.WriteLine(" HairBallCount = " + ((Cat)animal).HairballCount); Note that the entire casting operation is enclosed by brackets, to ensure that the casting will be complete before ".HairballCount" is evaluated. As soon as this operation finishes executing, "animal" will be of type "Animal" again. Of course, in a really well-designed program, you shouldn't have to make decisions based on an object's inherited type at all! It still manages to come up sometimes with object creation (for example, creating inherited objects based off of the contents of an Xml document), but otherwise, the concepts that we'll learn in this unit should virtually eliminate the need to do so. It is important to note that inheritance isn't a one-time trick; it works more like a "family tree". It's perfectly fine to have multiple layers of inheritance. You can have a Toyota and Honda class, which both inherit from a Car class, which inherits from a MotorizedVehicle class, which inherits from a Transportation class, and so on. There's no limit to the number of "specific" classes that can inherit from a "base" class, and no limit to the number of "layers" that you can have in your "tree". As long there's a good reason for having each separate layer, go for it! Now, like any other feature, inheritance should only be used where it makes sense, and not just for the sake of having it. For example, many of those layers of inheritance in the previous paragraph would be pointless if you're only writing a program to sell used cars. A "Vehicle" class, from which a "Car", "Truck", and "Van" class inherit, would be plenty. The issue of Toyota/Honda would probably be represented by a Manufacturer property, which would be defined in the Vehicle class. Don't go to the trouble of adding extra classes and layers of inheritance, when a few well-placed properties will suffice. For the record, I find that I rarely need more than two or three layers of inheritence; I use a base class as the lowest level, specific classes as the middle level, and highly specialized classes as the third (and less frequently used) level. Of course, how you actually decide to categorize and structure your objects is up to you; that is the primary challenge of OOP. If you were writing the a program for a zoo, you probably don't want to make a different class for every possible animal on the planet! A more sensible approach would be to create a class for every category of animal... FlyingAnimal, WalkingAnimal and SwimmingAnimal, for example. Or, maybe you want to break the classes down into Herbivore, Carnivore and Omnivore. Or, maybe you want to use Mammal, Insect, Lizard and Bird. Break it down in the way that makes the most sense, and is the most helpful in your specific situation. And don't tell professional programmers that you only use inheritence to get free code reuse, or they'll slap you. Just as a real-life example, I recently wrote a 2-D platformer game in C#. I defined a "base" class, which contained generic information that every object on the screen would need (coordinates and size, for example). I then created classes for Items, Platforms and Enemies, which inherited from that base class. However, the specific details about each Item and Enemy were handled through the properties in the Item and Enemy classes, and not by having a separate class for every possible Item and Enemy in the game. Inheritance is useful for defining categories of objects, but the details should usually be handled by members and properties. Having said all that, we must also take a brief look at two different OOP relationships, inheritance and aggregation. Inheritance is the relationship you've just learned about now. A Cat is an Animal, a Truck is a Vehicle, and so on. Inheritance suggests that an object is just a specific category of a more generic object. Aggregation, on the other hand, is what you get when you put other objects within a class. A SolarSystem object might contain Asteroids, Planets and Stars. Aggregation suggests that an object contains, or has, several objects within itself. Once again, there's a time and place for both features. My platformer had a World class that contained all of those Items, Platforms and Enemies, but it wasn't related to them through inheritance. As you can now (hopefully) see, objects are far more than just a convenient way to group information; they're even more than a safety feature that can protect sensitive internal data. Objects allow us to define anything we can think of as a datatype in programming terms, and even code relationships between different things. We can create a world with code, and even decide what that world will be composed of. We're not just in the business of writing programs, anymore. Now, we can write our own building blocks, as well. Consider the following code sample, which applies what we've learned to a set of Tools... using System; namespace A_Inheritance { class Class1 { static readonly string ln = Environment.NewLine; [STAThread] static void Main(string[] args) { // The generic "tool" Tool tool = new Tool(); tool.Manufacturer = "Hypothetical Tool Company, Inc."; tool.Price = 10.0; // A specific tool: has all of Tool's stuff, plus anything unique to itself ScrewDriver screwDriver = new ScrewDriver(); screwDriver.Manufacturer = "Gary's Screwdrivers"; screwDriver.Price = 15.0; screwDriver.HeadType = "Phillips"; // Another specific tool DrillPress drillPress = new DrillPress(); drillPress.Manufacturer = "Ye Olde Power Tool Company"; drillPress.Price = 180.0; // Print them all PrintTool(tool); PrintTool(screwDriver); PrintTool(drillPress); PromptForExit(); } static void PrintTool(Tool tool) { Console.WriteLine("Tool Statistics:"); Console.WriteLine(" Type = " + tool.GetType().Name); Console.WriteLine(" Manufacturer = " + tool.Manufacturer); Console.WriteLine(" Price = $" + tool.Price); Console.WriteLine(" Powered = " + tool.Powered + ln); } static void PromptForExit() { Console.Write(ln + "Program complete! Hit enter to exit..."); Console.ReadLine(); } } class Tool { // Protected members are accessible by any inheriting class. // Private members cannot be accessed by an inheriting class, // even though the inheriting class will still have them. protected string _manufacturer; protected double _price; protected bool _powered; public string Manufacturer { get{ return _manufacturer; } set{ _manufacturer = value; } } public double Price { get{ return _price; } set{ _price = value; } } public bool Powered { get{ return _powered; } } public Tool() { _manufacturer = ""; _price = 0.0; _powered = false; } public string Use() { return "Tool is in use."; } } class ScrewDriver: Tool { // Add a member that is specific to this type of tool protected string _headType; public string HeadType { get{ return _headType; } set{ _headType = value; } } public ScrewDriver() { HeadType = ""; } } class DrillPress: Tool { public DrillPress() { // Access the base class's protected member _powered = true; } } } |
|