Though you can gain a superficial knowledge of C++ in a couple of weeks, and become adept at it in a few years, an experienced C++ programmer would probably never claim to be a master of the language. Its complexities are mind-boggling. (I know people who have made career changes because of it.) More recent languages have eliminated a number of its tricky features and made it much easier to write good code. However, they have their own set of challenges. While they have taken a few steps forward on a number of issues, they have gone a few steps backward on some others. This can impact the extensibility of your code, break polymorphism, and make it harder for you to derive from your classes. Learning what these challenges are, and avoiding them or handling them judiciously, is imperative to effective development. In this chapter I will discuss the things you need to be aware of in the areas of inheritance and polymorphism so you can make best use of these important concepts.
In working with an inheritance hierarchy, how do you know which type an instance belongs to? Given a reference, you can use casting to convert it to the type you desire. However, the problem with casting is that if the conversion is not valid, it results in a runtime InvalidCastException
. This is ugly and you must avoid it at all costs. What alternatives do you have?
.NET languages allow you to determine the type of an object at runtime. This feature is called Runtime Type Identification (RTTI). In C# you use the keyword is
; in VB.NET you use TypeOf
...Is
. I will refer to these as RTTI operators.
What are the consequences of using RTTI operators extensively or arbitrarily? I will discuss the dark side of RTTI in a very simple example, shown in Example 6-1.
Example 6-1. Working with object hierarchy
//Animal.cs public class Animal { public void Eat() { Console.WriteLine("Animal eating"); } } //Dog.cs public class Dog : Animal { public void Bark() { Console.WriteLine("Dog barking"); } } //Cat.cs public class Cat : Animal { public void Meow() { Console.WriteLine("Cat Meowing"); } } //Trainer.cs public class Trainer { public void Train(Animal anAnimal) { anAnimal.Eat(); //Using casting Dog doggie = (Dog) anAnimal; doggie.Bark(); Cat aCat = (Cat) anAnimal; aCat.Meow(); } } //Test.cs class Test { [STAThread] static void Main(string[] args) { Dog spencer = new Dog(); Cat snow = new Cat(); Trainer jimmy = new Trainer(); jimmy.Train(spencer); jimmy.Train(snow); } }
'Animal.vb Public Class Animal Public Sub Eat() Console.WriteLine("Animal eating") End Sub End Class 'Dog.vb Public Class Dog Inherits Animal Public Sub Bark() Console.WriteLine("Dog barking") End Sub End Class 'Cat.vb Public Class Cat Inherits Animal Public Sub Meow() Console.WriteLine("Cat Meowing") End Sub End Class 'Trainer.vb Public Class Trainer Public Sub Train(ByVal anAnimal As Animal) anAnimal.Eat() 'Using Casting Dim doggie As Dog = CType(anAnimal, Dog) doggie.Bark() Dim aCat As Cat = CType(anAnimal, Cat) aCat.Meow() End Sub End Class 'Test.vb Module Module1 Sub Main() Dim spencer As New Dog Dim snow As New Cat Dim jimmy As New Trainer jimmy.Train(spencer) jimmy.Train(snow) End Sub End Module
In this example, the Trainer
wants to train an Animal
. In the process of training, she first feeds the animal by calling the Eat()
method. In the next activity, she wants the animal to express itself. In this example, the animal may be either a Dog
or a Cat
, so you cast it to these types and call the Bark()
and the Meow()
methods. But this code, while flawless in compilation, throws an InvalidCastException
at runtime, as shown in Figure 6-1.
The CLR does not like your casting a Dog
object to a Cat
. (It is only natural that dogs do not like to be treated as cats—do not try that at home.) The worst you can do at this point is to surround the casting code with try
/catch
statements, suppress the exception in the catch
and claim that you have taken care of the situation. This is undesirable for a couple of reasons. For one thing, using exceptions in situations like this is expensive. For another, you have not properly handled the condition where the given Animal
is not a type you expect. Casting is ugly.
Let’s consider the use of RTTI
. In the code in Example 6-2, I show only the changes to the Trainer
class (the only class I have changed).
Example 6-2. Using RTTI
✗ C# (RTTI)
//Trainer.cs public class Trainer { public void Train(Animal anAnimal) { anAnimal.Eat(); //Using RTTI if (anAnimal is Dog) { Dog doggie = (Dog) anAnimal; doggie.Bark(); } else if (anAnimal is Cat) { Cat aCat = (Cat) anAnimal; aCat.Meow(); } } }
✗ VB.NET (RTTI)
'Trainer.vb Public Class Trainer Public Sub Train(ByVal anAnimal As Animal) anAnimal.Eat() 'Using RTTI If TypeOf anAnimal Is Dog Then Dim doggie As Dog = CType(anAnimal, Dog) doggie.Bark() ElseIf TypeOf anAnimal Is Cat Then Dim aCat As Cat = CType(anAnimal, Cat) aCat.Meow() End If End Sub End Class
In the Train()
method you check to see if, at run time, the given reference points to an instance of Dog
. If so, then you perform the cast. Similarly, you check to see if the reference points to an object of Cat
and make the cast only if it is. You will not trigger an exception in this case. Figure 6-2 shows the output from the modified program.
In C#, Dog doggie = anAnimal as Dog; if (doggie != null) { doggie.Bark(); } is equivalent to if (anAnimal is Dog) { Dog doggie = (Dog) anAnimal; doggie.Bark(); }
Both the as
and is
operators represent the use of RTTI. However, the as
operator can only be used with reference types.
Is this better than using casting? Well, at least the exceptions go away. But what happens if you add another type of Animal
to your system in the future, say a Horse
? When an instance of Horse
is sent to the Train()
method, it invokes the Eat()
method, but not any of the other methods on Horse
, for example Neighs()
. If you ask the horse if it had a good time at the trainer, it will probably say, “The trainer fed me, then he asked if I was a dog and I said no. He then asked if I was a cat and I said no. Then he just walked away. He is not an equal opportunity trainer.”
The code that uses RTTI in this manner is not extensible. It fails the Open-Closed Principle (OCP), which states that a software module must be open for extension but closed for modification. That is, you should be able to accommodate changes in the requirements by adding small new modules of code, not by changing existing code (see Gotcha #23, "Copy Constructor hampers exensibility“).
While RTTI is better than casting, it is still bad. It is better to rely on polymorphism. You should abstract the methods of the derived class into the base class. In this example, the Bark()
of Dog
and the Meow()
of Cat
can be abstracted as, say, MakeNoise()
in Animal
. However, Animal
doesn’t know how to implement that method, so it’s marked as abstract
/MustOverride
. This alerts derived classes that they are responsible for implementing it. The code in Example 6-3 shows these changes.
Example 6-3. Relying on abstraction and polymorphism
✓ C# (RTTI)
//Animal.cs public abstract class Animal { public void Eat() { Console.WriteLine("Animal eating"); } public abstract void MakeNoise(); } //Dog.cs public class Dog : Animal { public void Bark() { Console.WriteLine("Dog barking"); } public override void MakeNoise() { Bark(); } } //Cat.cs public class Cat : Animal { public void Meow() { Console.WriteLine("Cat Meowing"); } public override void MakeNoise() { Meow(); } } //Trainer.cs public class Trainer { public void Train(Animal anAnimal) { anAnimal.Eat(); //Using Abstraction and Polymorphism anAnimal.MakeNoise(); } }
'Animal.vb Public MustInherit Class Animal Public Sub Eat() Console.WriteLine("Animal eating") End Sub Public MustOverride Sub MakeNoise() End Class 'Dog.vb Public Class Dog Inherits Animal Public Sub Bark() Console.WriteLine("Dog barking") End Sub Public Overrides Sub MakeNoise() Bark() End Sub End Class 'Cat.vb Public Class Cat Inherits Animal Public Sub Meow() Console.WriteLine("Cat Meowing") End Sub Public Overrides Sub MakeNoise() Meow() End Sub End Class 'Trainer.vb Public Class Trainer Public Sub Train(ByVal anAnimal As Animal) anAnimal.Eat() 'Using Abstraction and Polymorphism anAnimal.MakeNoise() End Sub End Class
Now the Trainer
class relies on the polymorphic behavior of the MakeNoise()
method. This code is far superior to using RTTI, as it’s more extensible. It makes it easier to add different domestic animals who might seek education from this Trainer
.
Not all uses of RTTI are bad, though. For example, consider the case where an Animal
has a Play()
method and a Dog
decides that it only likes playing with other Dog
s. This code looks like Example 6-4.
Example 6-4. OK use of RTTI
✓ C# (RTTI)
//Animal.cs public abstract class Animal { public void Eat() { Console.WriteLine("Animal eating"); } public abstract void MakeNoise(); public abstract bool Play(Animal other); } //Dog.cs public class Dog : Animal { public void Bark() { Console.WriteLine("Dog barking"); } public override void MakeNoise() { Bark(); } public override bool Play(Animal other) { if (other is Dog) return true; else return false; } }
'Animal.vb Public MustInherit Class Animal Public Sub Eat() Console.WriteLine("Animal eating") End Sub Public MustOverride Sub MakeNoise() Public MustOverride Function Play(ByVal other As Animal) As Boolean End Class 'Dog.vb Public Class Dog Inherits Animal Public Sub Bark() Console.WriteLine("Dog barking") End Sub Public Overrides Sub MakeNoise() Bark() End Sub Public Overrides Function Play(ByVal other As Animal) As Boolean If TypeOf other Is Dog Then Return True Else Return False End If End Function End Class
The use of RTTI in the Play()
method is benign. It checks to see if Other
refers to an instance of its own type. At this point, there is no extensibility issue if a Dog
never wants to play with any animals other than Dog
s. Of course, if the Dog
changes its mind, you’ll have to externalize the rule (maybe in a configuration file), or apply a solution like the Visitor pattern. (Refer to [Freeman04, Gamma95] for more details on patterns.) Those would eliminate the use of RTTI here as well.
Use RTTI sparingly. Do not use it if you are checking against multiple types. Use it only if you do not violate the Open-Closed Principle.
Gotcha #23, "Copy Constructor hampers exensibility,” Gotcha #43, "Using new/shadows causes “hideous hiding",” Gotcha #44, "Compilers are lenient toward forgotten override/overrides,” Gotcha #45, "Compilers lean toward hiding virtual methods,” Gotcha #46, "Exception handling can break polymorphism,” and Gotcha #47, "Signature mismatches can lead to method hiding.”
If a method is marked virtual
/overridable
, the base class tells the compiler not to bind to it statically at compile time, since a derived class may override that method. The method to be called is resolved at runtime. However, if a method is not marked virtual
/overridable
, then the compiler binds to it statically at compile time.
In C++, hiding occurs when a programmer writes a method in the derived class with the same name and signature as a method not marked virtual
in the base class. Hiding can also result from virtual methods that differ in signature between base and derived classes (see Gotcha #47, "Signature mismatches can lead to method hiding“). Hiding of non-virtual
methods in C++ is generally an accident and (hopefully) not the intent. Unfortunately, even though the managed compilers warn you against such mistakes, .NET gives you a legal way to violate the principles of good object-oriented programming—the new
/shadows
keyword. (The .NET approach to method hiding is like the laws in Las Vegas: “Oh yeah, that’s illegal in most parts of the world, but here that’s just fine and you are most welcome!”)
You may argue that hiding facilitates versioning—it allows a base class to introduce, in a later version, a method that is already present in the derived class. However, this facility leads to more trouble than it’s worth, as discussed below. I prefer my code to behave consistently in an object-oriented manner (or fail compilation) than to quietly misbehave.
You should expect the same method to be executed on an object no matter how you invoke it—whether through a direct reference to its type, or through a reference to its base type. This is the essence of polymorphism, which says that the actual method invoked is based on the type of the object and not the type of the reference. Hiding works against this. It makes the method that is invoked dependent on the type of the reference and not on the real type of the object. Consider Example 6-5.
Example 6-5. Hiding methods
//Base.cs namespace Hiding { public class Base { public virtual void Method1() { Console.WriteLine("Base.Method1 called"); } public virtual void Method2() { Console.WriteLine("Base.Method2 called"); } } } //Derived.cs namespace Hiding { public class Derived : Base { public override void Method1() { Console.WriteLine("Derived.Method1 called"); } public new void Method2() { Console.WriteLine("Derived.Method2 called"); } } } //Test.cs namespace Hiding { class Test { static void Main(string[] args) { Derived d = new Derived(); Base b = d; d.Method1(); d.Method2(); b.Method1(); b.Method2(); } } }
'Base.vb Public Class Base Public Overridable Sub Method1() Console.WriteLine("Base.Method1 called") End Sub Public Overridable Sub Method2() Console.WriteLine("Base.Method2 called") End Sub End Class 'Derived.vb Public Class Derived Inherits Base Public Overrides Sub method1() Console.WriteLine("Derived.Method1 called") End Sub Public Shadows Sub method2() Console.WriteLine("Derived.Method2 called") End Sub End Class 'Test.vb Module Test Sub Main() Dim d As New Derived Dim b As Base = d d.Method1() d.method2() b.Method1() b.Method2() End Sub End Module
In this example, Method2()
in Derived
hides Method2()
of Base
, while Method1()
in Derived
overrides Method1()
in Base
. In Test
, you have only one instance of Derived
, but two references to it. One reference named d
is of type Derived
, and the other reference named b
is of type Base
. Regardless of how you invoke Method1()
, the same method Method1()
in Derived
is called, as shown in the output in Figure 6-3. However, the call to Method2()
goes to Base.Method2()
if called using b
, and to Derived.Method2()
if called using d
. The method that is actually executed, in the latter case, depends on the type of the reference instead of the type of the object that it refers to; this is an example of hiding.
Hiding is very anti-object-oriented. After all, the reason you mark a method virtual
/overridable
is to allow derived classes to provide alternate implementations. If the base class uses virtual
/overridable
correctly, and the derived class uses override
/overrides
, a consistent method is invoked on that object without regard to how and where it is accessed. Hiding fundamentally breaks that tenet.
Gotcha #42, "Runtime Type Identification can hurt extensibility,” Gotcha #44, "Compilers are lenient toward forgotten override/overrides ,” Gotcha #45, "Compilers lean toward hiding virtual methods,” Gotcha #46, "Exception handling can break polymorphism,” and Gotcha #47, "Signature mismatches can lead to method hiding.”
Say a method in the base class is declared virtual
/overridable
, and you implement a method with the same name and signature in the derived class. What happens if you do not mark the method as override
/overrides
? When you compile the code and look at the output window in Visual Studio, you will not see any compilation error (see Gotcha #12, "Compiler warnings may not be benign“). However, a warning is generated. This warning tells you, very quietly, that the derived method has been assumed to hide the base class method (see Gotcha #43, "Using new/shadows causes “hideous hiding”“). This is a serious warning that should not have been hidden in the output window. In fact, this should not be considered a warning at all, in my opinion. The compiler should jump out of the computer, grab the programmer by the collar, give him a smack, and demand that he fix the code. (Maybe I’m going a bit too far, but you get the point). Consider Example 6-6.
Example 6-6. Accidental hiding
//Base.cs using System; namespace MarkOverride { public class Base { public virtual void Method1() { Console.WriteLine("Base.Method1 called"); } } } //Derived.cs using System; namespace MarkOverride { public class Derived : Base { public void Method1() { Console.WriteLine("Derived.Method1 called"); } } } //Test.cs using System; namespace MarkOverride { class Test { [STAThread] static void Main(string[] args) { Derived d = new Derived(); Base b = d; d.Method1(); b.Method1(); } } }
✗ VB.NET (RememberMarkOverride)
'Base.vb Public Class Base Public Overridable Sub Method1() Console.WriteLine("Base.Method1 called") End Sub End Class 'Derived.vb Public Class Derived Inherits Base Public Sub Method1() Console.WriteLine("Derived.Method1 called") End Sub End Class 'Test.vb Module Test Sub Main() Dim d As New Derived Dim b As Base = d d.Method1() b.Method1() End Sub End Module
In this example, Method1()
is declared virtual
/overridable
in the Base
class. However, in the Derived
class Method1()
has not been marked as override
/overrides
. This compiles OK, as you can see in Figure 6-4.
But this is misleading; a warning actually is printed, but it scrolls out of the output window. The message is:
warning CS0114: 'MarkOverride.Derived.Method1()' hides inherited member 'MarkOverride.Base.Method1()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
The output produced by the code is shown in Figure 6-5.
It would have been nice if the .NET compilers had erred on the side of caution. If a method is not marked override
/overrides
, why not assume that it overrides its base-class method, rather than assume that it hides it? Better still, shouldn’t this have appeared as a fatal compilation error? Unfortunately it doesn’t, so you need to pay attention to these warnings and treat them seriously. You can (and should) configure the compiler to treat warnings as errors. (See Gotcha #12, "Compiler warnings may not be benign.”)
If you are overriding a method, always remember to mark the method as override
/overrides
. Otherwise, the compiler just gives you a polite warning and assumes the method is intended to be new
/shadows
.
Gotcha #12, "Compiler warnings may not be benign,” Gotcha #42, "Runtime Type Identification can hurt extensibility,” Gotcha #43, "Using new/shadows causes “hideous hiding”,” Gotcha #45, "Compilers lean toward hiding virtual methods,” Gotcha #46, "Exception handling can break polymorphism,” and Gotcha #47, "Signature mismatches can lead to method hiding.”
Programmers coming from C++ are used to marking methods as virtual
in a derived class when overriding virtual
methods of the base class. However, this habit should not be carried over to .NET. Let’s see what happens if you mark a method in the derived class virtual
/overridable
when a virtual
/overridable
method with the same name and signature exists in the base class. Consider Example 6-7.
Example 6-7. Another accidental hiding
//Base.cs using System; namespace DerivedMethod { public class Base { public virtual void Method1() { Console.WriteLine("Base.Method1 called"); } } } //Derived.cs using System; namespace DerivedMethod { public class Derived : Base { public virtual void Method1() { Console.WriteLine("Derived.Method1 called"); } } } //Test.cs using System; namespace DerivedMethod { public class Test { [STAThread] static void Main(string[] args) { Derived d = new Derived(); Base b = d; d.Method1(); b.Method1(); } } }
'Base.vb Public Class Base Public Overridable Sub Method1() Console.WriteLine("Base.Method1 called") End Sub End Class 'Derived.vb Public Class Derived Inherits Base Public Overridable Sub Method1() Console.WriteLine("Derived.Method1 called") End Sub End Class 'Test.vb Public Class Test Shared Sub Main() Dim d As New Derived Dim b As Base = d d.Method1() b.Method1() End Sub End Class
Once again, as in Gotcha #44, "Compilers are lenient toward forgotten override/overrides ,” there is no compilation error. You only get a hidden warning message from the C# compiler:
warning CS0114: 'DerivedMethod.Derived.Method1()' hides inherited member 'DerivedMethod.Base.Method1()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
A similar message appears in the VB.NET version as well. The output from the program is shown in Figure 6-6.
Do not mark a method as virtual
/overridable
if your intent is to override a base method. Mark it as override
/overrides
.
Gotcha #42, "Runtime Type Identification can hurt extensibility,” Gotcha #43, "Using new/shadows causes “hideous hiding”,” Gotcha #44, "Compilers are lenient toward forgotten override/overrides,” Gotcha #46, "Exception handling can break polymorphism ,” and Gotcha #47, "Signature mismatches can lead to method hiding.”
Liskov’s Substitution Principle (LSP) [Martin03] is one of the cardinal tenets of inheritance and polymorphism. To quote Barbara Liskov:
Any derived class object must be substitutable wherever a base class object is used, without the need for the user to know the difference.
Overridden methods in derived classes must appear to behave the same as their base-class version. After all, methods express a contract that users of a class must be able to rely on. While the number of parameters, their types, and the method return type can be verified syntactically, there are more subtle details not expressed through syntax. One is the exceptions that a method throws. Unlike Java, the .NET languages have no syntax to declare this, so it is often expressed using documentation. Consider Example 6-8.
Example 6-8. Expressing method exceptions
//Base.cs using System; namespace ExceptionExample { /// <summary> /// A class to illustrate exception when overriding /// </summary> public class Base { /// <summary> /// Method1 does some thing on input given. /// </summary> /// <param name="val">Input to work on</param> /// <exception /// cref="ExceptionExample.InvalidInputException"> /// Thrown if parameter is less than 0. /// </exception> public virtual void Method1(int val) { if (val < 0) throw new InvalidInputException(); //... rest of the code goes here } } }
✓ VB.NET (ExceptionInDeriving)
'Base.vb ''' <summary> ''' A class to illustrate exception when overriding ''' </summary> Public Class Base ''' <summary> ''' Method1 does some thing on input given. ''' </summary> ''' <param name="val">Input to work on</param> ''' <exception ''' cref="ExceptionExample.InvalidInputException"> ''' Thrown if parameter is less than 0. ''' </exception> Public Overridable Sub Method1(ByVal val As Integer) If val < 0 Then Throw New InvalidInputException End If ' ... rest of the code goes here End Sub End Class
The method Method1()
of the Base
class throws an exception if the parameter’s value is less than zero. Looking at the NDoc-generated documentation, you see the details of Method1()
as shown in Figure 6-7. (Third-party tools can be used to generate XML documentation for VB.NET code. VB.NET in VS 2005 will support XML comments directly.)
The documentation specifies that Method1()
throws an InvalidInputException
if the value of its val
parameter is less than zero. If you are using an object of Base
, then you can consult the documentation for the behavior of the methods. Based on that information, you write code that uses a Base
object, as shown in Example 6-9. Notice that the Use()
method, relying on the documentation of the Base
class, handles the InvalidInputException
.
Example 6-9. An example using a method that throws an exception
//Test.cs
using System;
namespace ExceptionExample
{
class Test
{
private static void Use(Base baseObject, int theValue)
{
Console.WriteLine("Executing Use with {0}, {1}",
baseObject.GetType().Name, theValue);
try
{
baseObject.Method1(theValue);
}
catch(InvalidInputException e)
{
Console.WriteLine(
"{0} was thrown", e.GetType().FullName);
// Handle the exception here
}
}
//...
}
}
✓ VB.NET (ExceptionInDeriving)
'Test.vb
Class Test
Private Shared Sub Use(ByVal baseObject As Base, ByVal theValue As Integer)
Console.WriteLine("Executing Use with {0}, {1}", _
baseObject.GetType().Name, theValue)
Try
baseObject.Method1(theValue)
Catch e As InvalidInputException
Console.WriteLine( _
"{0} was thrown", e.GetType().FullName)
' Handle the exception here
End Try
End Sub
'...
End Class
One implication of Liskov’s Substitution Principle is that a derived class should not throw any exceptions that are not thrown by its base class in methods that it overrides. If it does so, then the base and derived classes have different behavior, and the user needs to know the difference. But suppose a programmer who has never heard of Liskov’s Substitution Principle derives a class from Base
, and in the course of his development decides he needs to throw a new type of exception. Example 6-10 shows the consequences.
Example 6-10. Improper overriding of a method that throws an exception
using System; namespace ExceptionExample { /// <summary> /// A Derived class that violates LSP. /// </summary> public class Derived : Base { /// <summary> /// Method1 does something with input /// </summary> /// <param name="val">val to work with</param> /// <exception cref="InvalidInputException"> /// thrown if parameter is 0 /// </exception> /// <exception cref="InputMustBeEvenException"> /// thrown if parameter is not even /// </exception> public override void Method1(int val) { if ((val % 2) != 0) { // Not an even number throw new InputMustBeEvenException(); } base.Method1(val); //Continue with rest of the code } } }
✗ VB.NET (ExceptionInDeriving )
''' <summary> ''' A Derived class that violates LSP. ''' </summary> Public Class Derived Inherits Base ''' <summary> ''' Method1 does something with input ''' </summary> ''' <param name="val">val to work with</param> ''' <exception cref="InvalidInputException"> ''' thrown if parameter is 0 ''' </exception> ''' <exception cref="InputMustBeEvenException"> ''' thrown if parameter is not even ''' </exception> Public Overrides Sub Method1(ByVal val As Integer) If Not val Mod 2 = 0 Then 'Not an even number Throw New InputMustBeEvenException End If MyBase.Method1(val) 'Continue with rest of the code End Sub End Class
In this example, Method1()
of the Derived
class violates LSP, because it throws an exception (InputMustBeEvenException
) that differs from Method1()
’s behavior in Base
. A method call through a reference to the base class must be able to target an object of any derived class without determining its type. Let’s consider the code (as part of the Test
class) in Example 6-11.
Example 6-11. Code that fails due to violation of LSP
✗ C# (ExceptionInDeriving)
//... [STAThread] static void Main(string[] args) { Base object1 = new Base(); Use(object1, -1); Use(object1, 3); Derived object2 = new Derived(); Use(object2, -1); //Use does not handle InputMustBeEvenException Use(object2, 3); }
✗ VB.NET (ExceptionInDeriving)
'... Shared Sub Main() Dim object1 As New Base Use(object1, -1) Use(object1, 3) Dim object2 As New Derived Use(object2, -1) 'Use does not handle InputMustBeEvenException Use(object2, 3) End Sub
In the Main()
method you create an instance of Base
and call the Use()
method with that object, first with a value of -1
and then with a value of 3
. You then create an object of Derived
and call the Use()
method with this new instance and the same values as before. The output is shown in Figure 6-8.
The program generates an unhandled exception when the Use()
method is called with the Derived
object as its first argument and -1
as its second, because Derived
throws an InputMustBeEvenException
. This is undesirable behavior and should be avoided.
If you do want to throw a new type of exception, how do you handle that? One possibility is for the InputMustBeEvenException
to inherit from InvalidInputException
. The output after this change is shown in Figure 6-9.
Note that in this case, the Use()
method was able to catch the exception. Since the InputMustBeEvenException
inherits from InvalidInputException
, it is substitutable. This is still dangerous to a great extent. Why? You are still breaking the contract. Method1()
in Base
has promised to throw the InvalidInputException
only when the parameter is less than zero. The derived class throws the exception (though of Derived
type) even when its parameter is greater than zero. This again violates LSP.
The overriding method in a derived class should not throw an exception in a way that violates Liskov’s Substitution Principle (LSP). An instance of the derived class must be substitutable wherever an instance of the base class is used.
Gotcha #42, "Runtime Type Identification can hurt extensibility,” Gotcha #43, "Using new/shadows causes “hideous hiding”,” Gotcha #44, "Compilers are lenient toward forgotten override/overrides,” Gotcha #45, "Compilers lean toward hiding virtual methods,” and Gotcha #47, "Signature mismatches can lead to method hiding .”
The signature of a method is its parameter list: the number, order, and type of parameters it takes. When deriving from a class, you need to pay special attention to the signatures of its methods. If you want to introduce a method with a different signature in the derived class, it is not as simple as just writing the newer method. Going this route, you may end up hiding the base-class methods [Cline99]. This is demonstrated in Example 6-12.
Example 6-12. Hiding methods due to signature mismatch
//Base.cs using System; namespace MethodSignature { public class Base { public virtual void Method1(double val) { Console.WriteLine("Base.Method1(double val) called"); } } } //Derived.cs using System; namespace MethodSignature { public class Derived : Base { public virtual void Method1(int val) { Console.WriteLine("Derived.Method1(int val) called"); } } } //Test.cs using System; namespace MethodSignature { class Test { [STAThread] static void Main(string[] args) { Derived d = new Derived(); Base b = d; // b and d refer to the same object now. b.Method1(3); d.Method1(3); } } }
'Base.cs Public Class Base Public Overridable Sub Method1(ByVal val As Double) Console.WriteLine("Base.Method1(double val) called") End Sub End Class 'Derived.cs Public Class Derived Inherits Base Public Overridable Sub Method1(ByVal val As Integer) Console.WriteLine("Derived.Method1(int val) called") End Sub End Class 'Test.vb Module Test Sub Main() Dim d As New Derived Dim b As Base = d ' b and d refer to the same object now. b.Method1(3) d.Method1(3) End Sub End Module
The VB.NET code generates a warning (not an error). The message is:
warning BC40003: sub 'Method1' shadows an overloadable member declared in the base class 'Base'.
If you want to overload the base method, the derived method must be declared Overloads
. However, adding the Overloads
keyword to Derived.Method1()
does not change the behavior; the output is still that shown in Figure 6-10.
The C# version does not even generate a warning.
In this example, a class Base
has a virtual
/overridable
method Method1()
that has one parameter of type double
. In the class Derived
, which inherits from Base
, there is also a Method1()
, but its one parameter is of type int
/Integer
. Test.Main()
creates an object of Derived
and calls its Method1()
using a reference of type Derived
and then a reference of type Base
. The output produced by this program is shown in Figure 6-10.
Even though you are dealing with one instance of Derived
, you end up calling two different methods. What went wrong? When you call Method1(3)
on the base reference, the compiler uses the base class’s signature of the method to convert 3
to 3.0
at compile time. (You can see this clearly if you look at the MSIL for Test.Main()
in ildasm.exe). However, when Method1(3)
is called on the Derived
reference, the value of 3
is sent as is. Both the calls are bound dynamically at runtime. Since the derived class has no overriding method that takes a double
as a parameter, the call using the Base
reference ends up in the Base
method itself.
If you really want the method to have a different method signature in the derived class, it is better to overload and override at the same time, as shown in Example 6-13.
Example 6-13. Overriding and overloading to change signature
//Derived.cs using System; namespace MethodSignature { public class Derived : Base { public override void Method1(double val) { Console.WriteLine( "Derived.Method1(double val) called"); } public virtual void Method1(int val) { Console.WriteLine("Derived.Method1(int val) called"); // You may call Method1((double) val) from here if // you want consistent behavior. } } }
'Derived.cs Public Class Derived Inherits Base Public Overloads Overrides Sub Method1(ByVal val As Double) Console.WriteLine( _ "Derived.Method1(double val) called") End Sub Public Overridable Overloads Sub Method1(ByVal val As Integer) Console.WriteLine("Derived.Method1(int val) called") ' You may call Method1((double) val) from here if ' you want consistent behavior. End Sub End Class
As you can see, in this modified version of the Derived
class, you override and overload Method1()
. The output is shown in Figure 6-11.
Now both the calls end up in Derived
, though in two different methods. In the Derived
class, you can take care of providing consistent and appropriate behavior for these two calls.
If you want to change the signature of a method in a derived class, then override and overload at the same time. This assures that all calls to a virtual
/overridable
method whose signature the derived class is changing consistently execute in that class, regardless of the type of reference used. Otherwise, you may end up hiding the method.
Gotcha #12, "Compiler warnings may not be benign" and Gotcha #13, "Ordering of catch processing isn’t consist across languages.”
3.147.238.1