|
|
|
Sometimes, inheritance isn't as simple as cribbing off of original class's code. There are many situations where a specific class needs the same methods as the generic class, but different code. For instance, if you have a Cube class that inherits from a Square class, your GetSize() method would need to work differently. The GetSize() method in Square would only do the calculation based on _height and _width, whereas the GetSize() method in Cube also needs to factor in _depth. You can't change Square's method to include _depth, because _depth doesn't exist in Square. However, it would also appear that you can't write a new version of the GetSize() method in Cube, since the method name would conflict with the original one in Square... or would it? There's a special keyword, "virtual", that we can use with methods and properties. "virtual" means that inheriting classes have permission to write their own version of the original method. All they need to do is include the word "override" in the new method's signature: // Original method, as it would appear in Square public virtual int GetSize() { return _width * _height; } // New method, as it would appear in Cube public override int GetSize() { return _width * _height * _depth; } ...and just like that, Cube has written its own version of GetSize() that overrides the one in Square! If we made another class that inherited from Cube, we could override that one, too. The important thing is that "virtual" appears where the method is originally defined, and "override" appears everywhere the method is redefined. Now, suppose that we had an Animal class with a Speak() method, and a Dog class that inherited from Animal, overwriting Speak(). If we were to pass a Dog object into the following method... static void DoAnimalActions(Animal animal) { animal.Speak(); } ...which version of Speak() do you think would run? Well, remember from the previous lesson that even though the parameter is of type Animal, C# remembers the object's original datatype, which is Dog. So when we call the animal object's Speak() method, your program will actually run Dog's overridden version of Speak(). This is tremendously useful, because it allows us to perform different actions for different Animal types, without even having to check what type of Animal it is! As long as we're calling a virtual method, we can just leave the animal object the way it is, and know that C# will call the override that actually belongs to its true datatype. Incidentally, you don't have to completely rewrite the overriding method's logic. If you're just adding an extra step to the original virtual method, you can call the base class's method from your inherited class. Consider our Square/Cube GetSize() example: public override int GetSize() { // Call the code in the base class's GetSize() // method, which multiplies width and height int squareSize = base.GetSize(); // Add any of our own code for this class return squareSize * _depth; } The benefits of this are two-fold. Firstly, you don't have to re-write all of the code that's in the base class's method, if you want to re-use it. You can make a call to the base class's methods wherever you want, from within your inherited class (provided, of course, that they are public or protected). Secondly, it means that we don't have to remember how all of the logic in the base class works when writing the override. We can just make a call to base at the start of our override, and follow-up with whatever specific code our inherited class will need. Of course, you don't have to call the original method at all, if it's not going to be helpful. This is particularly true if the inherited class requires a totally different set of logic, or if calling the base class's method is going to interfere with the function of the inherited class's method. This is also a good time to mention abstract classes (I'll explain why it's a good time in just a minute). In all of these examples, we're really just using the base class "Animal" as a sort of "blueprint" for the more specific classes, like Dog and Cat. We never actually create an object of generic type "Animal", because it just wouldn't make sense. There's no such thing as a "generic" Animal; every animal has to be something! And if we didn't have a specific class for an animal we wanted to use, we would write one. In cases like this, where there's an implied characteristic in our program's architecture (namely, we're never going to create instances of plain old "Animal"), we can use the "abstract" keyword to make it explicit, and enforce this idea. Observe: abstract class Animal { // the rest of the class goes here } By adding the "abstract" keyword to our class definition, we make it impossible for the program to create instances of Animal. This operation becomes forbidden: Animal a = new Animal(); // no longer permitted! The benefit of this, as stated above, is that it takes an implicit assumption, and turns it into an explicitly enforced rule. If it doesn't make sense for your program to create instances of a base class, then forbid it from doing so! This is important aspect of OOP: enforce the nature of your objects whenever possible. Don't count on assumptions and common knowledge to get the job done! Write your classes so that they can only be used the way you want them to be. In this manner, you can keep the program as flexible as you need it, and drastically reduce the possibility of buggy code at the same time. So, returning to our original lesson on overriding... the reason that this is a good time to mention abstract classes is that they enable us to write abstract methods. An abstract method looks like this: abstract class Animal { public abstract void Speak(); } Whoa! It doesn't have any code at all; there's literally nothing to it but the signature! As you can imagine, it would cause C# a great deal of stress if it tried to execute a method without any implementation at all. This doesn't matter in the abstract Animal class; we're not allowed to create instances of Animal anyway, so no one is ever going to call the Speak() method on an Animal object. However, if someone were to create a non-abstract class that inherited from Animal, that class must override Speak(). There is no choice in this matter; C# will not permit you to define a non-abstract inheriting class if it doesn't provide overrides for all the base class's abstract methods. In other words, virtual methods permit inheriting classes to override them, whereas abstract methods demand that inheriting classes override them. Why would we want this? Well, just like abstract classes, abstract methods are about enforcing the nature of your classes. If we made the Animal class' Speak() method virtual, exactly what code would go in there? How can we know what sound an animal will make, unless we know which animal it is? Because there is no reasonable default behavior for Speak(), we want to force the programmer (you) to write an override for every specific animal. Making Speak() abstract guarantees that every class inheriting from Animal will have its own appropriate version of the method. It's impossible to forget, because the program won't compile, otherwise. Remember, too, that even though there's no code in the abstract method, it's still part of the base class's definition. So our method that accepts a generic Animal object and calls Speak() will still work, because any object we pass in is going to be an inherited class that has a legitimate override for the abstract Speak() method. Of course, because the virtual, override and abstract keywords all depend on the way individual objects behave, we cannot apply them to static methods. Nonetheless, you will find overriding to be a useful tool, almost any time you need to use inheritance. By the way, the formal term for all this inheritance/overriding stuff is "polymorphism". It allows us to use one thing as many different things. Now, review the code sample, and proceed to the next lesson (and yes, I put console output statements in the classes... that was just for clarity)! using System; namespace B_Overriding { class Class1 { static readonly string ln = Environment.NewLine; [STAThread] static void Main(string[] args) { Dog dog = new Dog(); Cat cat = new Cat(); Pig pig = new Pig(); DoAnimalActions(dog); DoAnimalActions(cat); DoAnimalActions(pig); PromptForExit(); } static void DoAnimalActions(Animal animal) { animal.Eat(); animal.Speak(); Console.Write(ln); } static void PromptForExit() { Console.Write(ln + "Program complete! Hit enter to exit..."); Console.ReadLine(); } } // Abstract class: // We cannot make objects from this class. abstract class Animal { // Virtual method: // If an inheriting class doesn't override it, the code that's // defined in this class will be used. public virtual void Eat() { Console.WriteLine("Ate food."); } // Abstract method: // An inheriting class MUST override it with its own code. // Only permitted in abstract classes public abstract void Speak(); } class Dog: Animal { public override void Eat() { // We can do this to execute the code from the lower-level class // But we don't have to, of course base.Eat(); // We can also provide our own code Console.WriteLine("Barfed all over the floor."); } public override void Speak() { Console.WriteLine("Bark!! Bark!!"); } } class Cat: Animal { // Because we didn't override Eat(), a Cat object's Eat() // method will just call Animal's Eat() method. public override void Speak() { Console.WriteLine("Meow."); } } class Pig: Animal { public override void Eat() { // We won't call base.Eat() this time, just so that this // version of Eat() looks totally different! Console.WriteLine("The pig ate all of the food."); } public override void Speak() { Console.WriteLine("Oink!"); } } } |
|