GOTCHA 46 Exception handling can break polymorphism


GOTCHA #46 Exception handling can break polymorphism

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

C# (ExceptionInDeriving)

 //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.)

Figure 6-7. Documentation showing details of method exception


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

C# (ExceptionInDeriving)

 //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

C# (ExceptionInDeriving)

 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.

Figure 6-8. Output from Example 6-11


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.

Figure 6-9. Output from Example 6-11 if InputMustBeEvenException derives from InvalidInputException


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.

IN A NUTSHELL

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.

SEE ALSO

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."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

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