Understanding Inheritance Issues


One of the objectives of the VB .NET compiler team was to avoid a situation where adding a new method to a base class would result in an unexpected change of behavior within the inheritance tree. This would be a definite route to some nasty bugs and is classified academically as the "fragile base class" problem.

Shadowing by Accident

VB .NET's way of resolving an overloaded method (i.e., searching through the entire inheritance chain) was demonstrated in the previous sections and has an interesting side effect. Consider what should happen if you define a new base class method that happens to have the same name as a previously defined derived method. The derived method has, of course, no inheritance qualifier specified, as there was not previously a base method to inherit.

In the perfect world, current code should carry on using the derived method, even if normal method resolution would suggest that the base method is more appropriate. Likewise, new code should always invoke the base method, even if polymorphism suggests that calling the base method should result in the derived method being invoked.

If the compiler defaulted the derived method to Overloads , the normal overload method resolution might result in current code calling the new base method instead of the derived method. Likewise, if the derived method had defaulted to Overrides , the rules of polymorphism might result in new code calling the derived method when the base method was expected.

So the VB .NET team decided that where the author's intent was not specified, the default inheritance qualifier for a member should be Shadows , which hides any base class methods with the same name and results in unchanged behavior in the situation discussed previously. This decision avoids this type of fragile base class problem very well, but it needs to be treated carefully . If a base class author defines a method as Overridable , but a derived class author forgets to use the Overrides keyword, the derived method will instead default to Shadows , which is unlikely to be what the derived class author intended when he or she wrote the base class shown in Listing 2-7. The only saving grace is that the compiler does issue a warning about a potential mistake.

Listing 2-7. Don't Forget to Override
start example
 Option Strict On Class Base     Overridable Sub DoSomething         'The method definition goes here     End Sub End Class Class Derived : Inherits Base     Sub DoSomething         'This method will actually shadow its base method rather than Override it,         'because the developer forgot to add the Overrides keyword     End Sub End Class 
end example
 

Making Shadows the default is more conservative, and probably safer, than using Overrides . In this way, changes to third-party libraries will not break your existing code or change its behavior. However, the result can be surprising, so perhaps it would have been better for the IDE to insert the Shadows keyword automatically in these circumstances so that the actual behavior would be obvious.

More Shadowing Issues

You should also realize that using the Shadows keyword could seriously degrade the maintainability of your code. For instance, you might create a Cat class that subclasses an Animal class; here the assumption is that Cat is an Animal and therefore behaves like an Animal . If you then use the Shadows keyword within the Cat class, you can alter the behavior of your Cat class so that it no longer behaves like an Animal . If you want the maintenance programmer who supports your code to go psychotic and come after you with a loaded AK-47, this is probably one of the better ways of doing it.

Listing 2-8 shows an example of a normal Cat inheritance hierarchy, without using the Shadows keyword. You can see that the objNormalCat object is declared and instantiated as a Cat , the objLameCat object is declared and instantiated as a LameCat , and finally, the objUglyCat object is declared as a Cat but instantiated as a LameCat .

Listing 2-8. Normal Inheritance Hierarchy
start example
 Option Strict On Module CatTester     Sub Main()         'NormalCat         Dim objNormalCat As New Cat()         With objNormalCat             Console.WriteLine("NormalCat is a " +.GetType.Name)             Console.WriteLine("It has " +.Legs.ToString + " legs and " _                                              +.Feet.ToString + " feet")             Console.WriteLine()         End With         'LameCat         Dim objLameCat As New LameCat()         With objLameCat             Console.WriteLine("LameCat is a " +.GetType.Name)             Console.WriteLine("It has " +.Legs.ToString + " legs and " _                                               +.Feet.ToString + " feet")             Console.WriteLine()         End With         'UglyCat         Dim objUglyCat As Cat         objUglyCat = New LameCat()         With objUglyCat             Console.WriteLine("UglyCat is a " +.GetType.Name)             Console.WriteLine("It has " +.Legs.ToString + " legs and " _                                              +.Feet.ToString + " feet")             Console.WriteLine()         End With         Console.ReadLine()     End Sub End Module Class Cat     Overridable Function Feet() As Int16         Return 4     End Function     Overridable Function Legs() As Int16         Return Me.Feet     End Function End Class Class LameCat : Inherits Cat  Overrides Function Feet() As Int16  Return 3     End Function     Overrides Function Legs() As Int16         Return Me.Feet     End Function End Class 
end example
 

As expected, this will show the following results:

start sidebar
 NormalCat is a Cat It has 4 legs and 4 feet LameCat is a LameCat It has 3 legs and 3 feet UglyCat is a LameCat It has 3 legs and 3 feet 
end sidebar
 

Now change the Overrides modifier of the LameCat.Feet member (the line marked in bold) to Shadows , and suddenly you are looking at a quite unexpected mutant and nonsensical cat:

start sidebar
 NormalCat is a Cat It has 4 legs and 4 feet LameCat is a LameCat It has 3 legs and 3 feet UglyCat is a LameCat It has 3 legs and 4 feet 
end sidebar
 

The Shadows keyword (which is the default) makes a huge difference in this code. The slightly unusual situation here exists because you are cheating the compiler by declaring an object as a Cat but actually instantiating it as a Lame-Cat . This is allowed because inheritance rules dictate that a subclass can always be substituted for its superclass. Wherever you use a Cat , you can also use a LameCat .

The result of using Shadows is that LameCat.Feet is accessible when viewed from within the class, for instance via the LameCat.Legs member, but hidden completely from the inheritance chain. This mixing of paradigms can be very confusing for maintenance programmers. Whenever you see the Shadows keyword, be on the lookout for unintended side effects.

Understanding Equality

If you shift the Shadows keyword back to Overrides , you can investigate another nasty side effect of declaring an object as a superclass but instantiating it as a subclass. In an attempt at political correctness, you want a lame cat to be the equal of any other cat. In order to do this, you need to overload the Equals member of the LameCat class to return true whenever a lame cat is compared to a normal cat. You're going to ignore the Equals member of the Cat class, as you're only concerned here with lame cats. This looks like it should be relatively trivial (see the lines marked in bold in Listing 2-9).

Listing 2-9. Lame Cat Equal to Any Other Cat?
start example
 Option Strict On Module CatTester     Sub Main()         'NormalCat         Dim objNormalCat As New Cat()         With objNormalCat             Console.WriteLine("NormalCat is a " +.GetType.Name)             Console.WriteLine("It has " +.Legs.ToString + " legs and " _                                              +.Feet.ToString + " feet")             Console.WriteLine()         End With         'LameCat         Dim objLameCat As New LameCat()         With objLameCat             Console.WriteLine("LameCat is a " +.GetType.Name)             Console.WriteLine("It has " +.Legs.ToString + " legs and " _                                              +.Feet.ToString + " feet")             Console.WriteLine("Equal to a cat? " +.Equals(New Cat()).ToString)             Console.WriteLine()         End With         'UglyCat         Dim objUglyCat As Cat         objUglyCat = New LameCat()         With objUglyCat             Console.WriteLine("UglyCat is a " +.GetType.Name)             Console.WriteLine("It has " +.Legs.ToString + " legs and " _                                              +.Feet.ToString + " feet")             Console.WriteLine("Equal to a cat? " +.Equals(New Cat()).ToString)             Console.WriteLine()         End With         Console.ReadLine()     End Sub End Module Class Cat     Overridable Function Feet() As Int16         Return 4     End Function     Overridable Function Legs() As Int16         Return Me.Feet     End Function End Class Class LameCat : Inherits Cat     Overrides Function Feet() As Int16         Return 3     End Function     Overrides Function Legs() As Int16         Return Me.Feet     End Function  "Add an overload only for cat comparison purposes     Overloads Function Equals(ByVal AnyCat As Cat) As Boolean         Return True     End Function  End Class 
end example
 

Once again, you see a surprise when you look at the ugly cat:

start sidebar
 NormalCat is a Cat It has 4 legs and 4 feet LameCat is a LameCat It has 3 legs and 3 feet Equal to a cat? True UglyCat is a LameCat It has 3 legs and 3 feet Equal to a cat? False 
end sidebar
 

The explanation for this behavior is subtle. The Equals member of LameCat does not actually overload the Equals member for Cat , because Cats inherit from Object , whose Equals member takes an Object parameter. Note that this will seem to work correctly in most circumstances, such as in the LameCat case shown in the preceding code. The problem is dangerous because it is only likely to appear under circumstances that will probably never be tested by the original developers of the Cat or LameCat classes.

Ironically, you can work around this problem by not checking your types so carefully. You can override the default Equals by using an Object parameter, and then use runtime type checking to identify whether any equality exists. So the new Equals member of LameCat might look as shown in Listing 2-10.

Listing 2-10. Corrected LameCat.Equals Member
start example
 Overloads Overrides Function Equals(ByVal Obj As Object) As Boolean     If Object.ReferenceEquals(Obj.GetType, New Cat().GetType) Then         Return True     Else         Return MyBase.Equals(Obj)     End If End Function 
end example
 

This runtime type checking is obviously more error-prone than compile-time checking, and it might also have a performance overhead.

One lesson that can be learned from this confusion is that when you're adding or changing an inherited method, it isn't sufficient just to test that specific method. You also need to retest every related method in the inheritance tree to ensure that your modifications didn't cause any unwanted side effects. This is the result of linking classes together within a model operating on implementation inheritance.

Better Equality

If you implement your own version of Equals for your reference and value types, you must ensure that you keep to the four major principles of equality:

  • Reflexivity: a.Equals(a) must always return true .

  • Symmetry: a.Equals(b) must return the same as b.Equals(a).

  • Transitivity: If a.Equals(b) is true and a.Equals(c) is true , then b.Equals(c) must also be true .

  • Consistency: a.Equals(b) must always return the same value until either a or b has been changed.

If you fail to keep to one or more of these principles, your code is likely to encounter horrible bugs that are difficult to reproduce. You have been warned !

Inheritance and Method Visibility

The previous examples showed that predicting which method will be called in some inheritance situations can be challenging and sometimes surprising. When you add method visibility into the mix, things can become even more confusing. The code in Listing 2-11 instantiates a Man , a Feline , and a Cat , all from within an Animal class. It then attempts to call the ClassName member belonging to each of the three objects and prints the results.

Listing 2-11. Using Protection with Inheritance
start example
 Option Strict On Class Animal     Public Shared Sub Main()         Dim objMan As New Man(), objFeline As New Feline(), objCat As New Cat()         Console.WriteLine(objMan.ClassName("This Man"))         Console.WriteLine(objFeline.ClassName("This Feline"))         Console.WriteLine(objCat.ClassName("This Cat"))         Console.ReadLine()     End Sub     Protected Overridable Function ClassName(ByVal CallingType As String) As String         Return CallingType + " appears to be an Animal"     End Function End Class Class Man : Inherits Animal     Protected Overrides Function ClassName(ByVal CallingType As String) As String         Return CallingType + " appears to be a Man"     End Function End Class Class Feline : Inherits Animal     Protected Overridable Shadows Function _         ClassName(ByVal CallingType As String) As String        Return CallingType + " appears to be a Feline"     End Function End Class Class Cat : Inherits Feline     Protected Overrides Function ClassName(ByVal CallingType As String) As String         Return CallingType + " appears to be a Cat"     End Function End Class 
end example
 

Many developers, even some quite experienced ones, will predict the following output:

start sidebar
 This Man appears to be a Man This Feline appears to be a Feline This Cat appears to be a Cat 
end sidebar
 

Some developers, usually those who are looking more carefully, will predict the following output:

start sidebar
 This Man appears to be an Animal This Feline appears to be an Animal This Cat appears to be an Animal 
end sidebar
 

So both groups of developers are surprised when the following output appears:

start sidebar
 This Man appears to be a Man This Feline appears to be an Animal This Cat appears to be an Animal 
end sidebar
 

This looks peculiar. Each method is being invoked directly from the correct type of variable, and at first glance it's hard to see what's going wrong. The key lies in the Protected keyword and the way in which it interacts with the Overrides and Shadows keywords.

A protected member without a Friend qualifier can only be accessed from within its own class or a derived class. Clients of the base class or the derived class cannot access it. So in each of the previous tests, the member within the Animal class is the only method that is ever invoked directly. The ClassName member within the Man class is simply an Override , so it is visible from within the Animal.ClassName member and can be called. However, the combination of the Protected and Shadows qualifiers on the ClassName member within the Feline class blocks access from the base class to that member and any associated member above it in the inheritance chain.

If all of the members had been qualified with the Friend keyword as well as the Protected keyword, the prediction given by the first group of developers would have been correct. The combination of the Protected and Friend keywords allows complete member accessibility within an assembly, as well as accessibility from derived classes in other assemblies. Therefore, the behavior of the code would have been more intuitive to most developers.

You will be looking at the benefits and dangers of implementation versus interface inheritance in more detail as part of a later chapter. For the moment, make a mental note that when you use implementation inheritance, you should use the Overridable , Shadows , and Overloads keywords with great care. Because they can cause confusion and inconsistent behavior depending on the type of variable being referenced, they have the potential to catch developers by surprise, and surprises often lead to bugs.

Navigating the Inheritance Tree

Sometimes even perfectly straightforward code can conceal a surprise. For instance, attempting to locate your position in an inheritance chain looks like exactly the task that MyClass is designed for. MyClass is similar to the familiar Me keyword, except that MyClass identifies the class member to call at compile time (sometimes known as static binding ). The keyword Me , on the other hand, identifies the class member to invoke at runtime in any situation where the method being called is declared as Overridable (sometimes known as dynamic binding ). So MyClass looks like a useful keyword to have when writing the code shown in Listing 2-12.

Listing 2-12. Where Am I?
start example
 Option Strict On Class Test     Shared Sub Main()         Dim objDerived As New Derived()         Console.WriteLine(objDerived.ClassName())         Console.WriteLine(objDerived.BaseName())         Console.ReadLine()     End Sub End Class Class Base     Public Overridable Function ClassName() As String         Return MyClass.ToString     End Function End Class Class Derived : Inherits Base     Public Overrides Function ClassName() As String         Return MyClass.ToString     End Function    Public Function BaseName() As String         Return MyBase.ClassName()     End Function End Class 
end example
 

Many developers, knowing that the use of MyClass results in a static (i.e., compile time) decision about which method to invoke and therefore is not subject to polymorphism and runtime decisions, expect this code to print the following:

start sidebar
 ProgramExample.Derived ProgramExample.Base 
end sidebar
 

The actual result is not so intuitive:

start sidebar
 ProgramExample.Derived ProgramExample.Derived 
end sidebar
 

The key to understanding this particular result is to look at what ToString is doing internally. If you substitute ToString with GetType.FullName , you will see exactly the same result. It appears as though ToString is calling Get-Type.FullName under the hood. GetType is using the type of the declared variable ”and the type of the variable is of course always Derived , even when the call to MyBase is performed. In fact, the documentation states specifically that Type always returns the derived class runtime type. You can verify this by instantiating a new variable of type Base and then calling its ClassName member. This time you will see the Base class type appearing in the result.

Instead of using GetType.FullName in the base class method, you could try using GetType.BaseType.FullName . This would return the desired result in this specific case, but it is still not useful in the general case where you want to know the type of the current class instance regardless of where it is in the inheritance chain. To achieve this, you can perform a little trick. Try adding the following function to the Test class:

 Public Shared Function ImmediateClassName() As String               Dim objStackFrame As New Diagnostics.StackFrame(1)               Return objStackFrame.GetMethod.DeclaringType.FullName           End Function 

Then replace each MyClass.ToString with a call to Test.ImmediateClassName and run the program again. This time you will see the desired result:

start sidebar
 ProgramExample.Derived ProgramExample.Base 
end sidebar
 

This works by getting access to VB .NET's method call stack. The parameter of 1 means look at the stack frame one above the current stack frame ”namely, the frame that invoked the ImmediateClassName method. The GetMethod function then returns the method within which the specified stack frame is executing. Finally, the DeclaringType property returns the actual type that declared the method, as opposed to the base or derived type.




Comprehensive VB .NET Debugging
Comprehensive VB .NET Debugging
ISBN: 1590590503
EAN: 2147483647
Year: 2003
Pages: 160
Authors: Mark Pearce

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net