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.
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.
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
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.
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 .
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
As expected, this will show the following results:
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
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:
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
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.
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).
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
Once again, you see a surprise when you look at the ugly cat:
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
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.
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
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.
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 !
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.
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
Many developers, even some quite experienced ones, will predict the following output:
This Man appears to be a Man This Feline appears to be a Feline This Cat appears to be a Cat
Some developers, usually those who are looking more carefully, will predict the following output:
This Man appears to be an Animal This Feline appears to be an Animal This Cat appears to be an Animal
So both groups of developers are surprised when the following output appears:
This Man appears to be a Man This Feline appears to be an Animal This Cat appears to be an Animal
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.
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.
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
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:
The actual result is not so intuitive:
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:
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.