4.2. Exception Handling

 < Day Day Up > 

One of the most important aspects of managing an object is to ensure that its behavior and interaction with the system does not result in a program terminating in error. This means that an application must deal gracefully with any runtime errors that occur, whether they originate from faulty application code, the Framework Class Library, or hardware faults.

.NET provides developers with a technique called structured exception handling (SEH) to deal with error conditions. The basic idea is that when an exception occurs, an exception object is created and passed along with program control to a specially designated section of code. In .NET terms, the exception object is thrown from one section of code to another section that catches it.

Compared to error handling techniques that rely on error codes and setting bit values, SEH offers significant advantages:

  • The exception is passed to the application as an object whose properties include a description of the exception, the assembly that threw the exception, and a stack trace that shows the sequence of calls leading to the exception.

  • If an exception is thrown and an application does not catch it, the Common Language Runtime (CLR) terminates the application. This forces the developer to take error handling seriously.

  • The exception handling and detection code does not have to be located where the errors occur. This means, for example, that exception handling code could be placed in a special class devoted to that purpose.

  • Exceptions are used exclusively and consistently at both the application and system level. All methods in the .NET Framework throw exceptions when an error occurs.

Before looking at the actual mechanics of implementing exception handling, let's examine the exception itself. As previously mentioned, an exception is a class instance. All .NET exceptions derive from the System.Exception class. So, an understanding of this class is essential to working with exceptions.

System.Exception Class

As shown in Figure 4-3, System.Exception is the base class for two generic subclasses SystemException and ApplicationException from which all exception objects directly inherit. .NET Framework exceptions (such as IOException and ArithmeticException) derive directly from IOException, whereas custom application exceptions should inherit from ApplicationException. The sole purpose of these classes is to categorize exceptions, because they do not add any properties or methods to the base System.Exception class.

Figure 4-3. .NET exception classes hierarchy


The System.Exception class contains relatively few members. Table 4-1 summarizes the members discussed in this section.

Table 4-1. System.Exception Class Properties

Property

Type

Description

HelpLink

string

Contains a URL that points to help documentation.

InnerException

Exception

Is set to null unless the exception occurs while a previous exception is being handled. A GetBaseException method can be used to list a chain of previous inner exceptions.

Message

string

The text describing the exception.

Source

string

Name of the assembly that generated the exception.

StackTrace

string

Contains the sequence of method names and signatures that were called prior to the exception. It is invaluable for debugging.

TargetSite

MethodBase

Provides details about the method that threw the exception. The property is an object of type MethodBase. It returns the name of the method in which the exception occurred. It also has a DeclaringType property that returns the name of the class containing the method.

HResult

Int32

This is a protected property used when interoperating with COM code. When an exception is thrown to a COM client, this value is converted to an HRESULT in the COM world of unmanaged code.


Writing Code to Handle Exceptions

C# uses a TRy/catch/finally construct to implement exception handling (see Figure 4-4). When an exception occurs, the system searches for a catch block that can handle the current type of exception. It begins its search in the current method, working down the list of catch blocks sequentially. If none is found, it then searches the catch blocks in the calling method associated with the relevant try block. If the search yields no matching catch block, an unhandled exception occurs. As discussed later, the application is responsible for defining a policy to deal with this. Let's look at the details of using these three blocks.

Figure 4-4. Code blocks used for exception handling


The try Block

The code inside the try block is referred to as a guarded region because it has associated catch or finally blocks to handle possible exceptions or cleanup duties. Each try block must have at least one accompanying catch or finally block.

The catch Block

A catch block consists of the keyword catch followed by an expression in parentheses called the exception filter that indicates the type of exception to which it responds. Following this is the code body that implements the response.

The exception filter identifies the exception it handles and also serves as a parameter when an exception is thrown to it. Consider the following statement:

 catch (DivideByZeroException ex)  {  ... } 

The filter will be invoked if a System.DivideByZeroException occurs. The variable ex references the exception and provides access to its properties, such as ex.Message and ex.StackTrace.

When using multiple catch blocks, ordering is important. They should be listed hierarchically, beginning with the most specific exceptions and ending with the more general ones. In fact, the compiler generates an error if you do not order them correctly.

 catch (DivideByZeroException ex)  {  ... } catch (IndexOutOfRangeException ex) { ... } catch (Exception ex)  {  ... } 

This codes first looks for specific exceptions such as a division by zero or an index out of range. The final exception filter, Exception, catches any exception derived from System.Exception. When an exception is caught, the code in the block is executed and all other catch blocks are skipped. Control then flows to the finally block if one exists.

Note that the catch block may include a throw statement to pass the exception further up the call stack to the previous caller. The throw statement has an optional parameter placed in parentheses that can be used to identify the type of exception being thrown. If tHRow is used without a parameter, it throws the exception caught by the current block. You typically throw an exception when the calling method is better suited to handle it.

The finally Block

Regarded as the "cleanup" block, the finally block is executed whether or not an exception occurs and is a convenient place to perform any cleanup operations such as closing files or database connections. This block must be included if there are no catch blocks; otherwise, it is optional.

Example: Handling Common SystemException Exceptions

Listing 4-2 illustrates the use of the TRy/catch/finally blocks in dealing with an exception generated by the CLR.

Listing 4-2. Handling Exceptions Generated by the CLR
 using System; // Class to illustrate results of division by zero public class TestExcep {    public static int Calc(int j)    {       return (100 / j);    } } class MyApp {    public static void Main()    {       TestExcep exTest = new TestExcep();       try       {          // Create divide by zero in called method          int dZero = TestExcep.Calc(0);          // This statement is not executed          Console.WriteLine("Result: {0}",dZero);       }       catch(DivideByZeroException ex)       {         Console.WriteLine("{0}\n{1}\n", ex.Message, ex.Source);          Console.WriteLine(ex.TargetSite.ToString());          Console.WriteLine(ex.StackTrace);       }       catch (Exception ex)       {          Console.WriteLine("General "+ex.Message);       }       finally       {          Console.WriteLine("Cleanup occurs here.");       }    } } 

In this example, TestExcep.Calc tHRows a division by zero exception when MyApp calls it with a zero value. Because Calc has no code to handle the exception, the exception is thrown back automatically to MyApp at the point where Calc was called. From there, control passes to the block provided to handle DivideByZeroException exceptions. For demonstration purposes, the statements in the catch block display the following information provided by the exception object:

Property

Value Printed

ex.Message

Attempted to divide by zero

ex.Source

zeroexcept (assembly name)

ex.TargetSite

Void Main()

ex.StackTrace

at MyApp.Main()


Core Recommendation

StackTrace displays only those methods on the call stack to the level where the exception is first handled not where it occurs. Although you may be tempted to catch exceptions at the point where they occur in order to view the full call stack, this is discouraged. It may improve diagnostics; however, it takes time and space to throw an exception and its entire call stack. Usually, the lower on a call stack that an exception occurs, the more likely it is that the conditions causing it can be avoided by improved coding logic.


How to Create a Custom Exception Class

Custom exception classes are useful when you need to describe errors in terms of the class that issues the error. For example, you may want the exception to describe the specific behavior causing the error or to indicate a problem with a parameter that does not meet some required criteria. In general, first look to the most specific system exception available; if that is inadequate, consider creating your own.

In Listing 4-3, a method throws a custom exception if the object it receives does not implement the two required interfaces. The exception, NoDescException, returns a message describing the error and the name of the object causing the failure.

Listing 4-3. Building a Custom Exception Class
 // Custom Exception Class [Serializable] public class NoDescException : ApplicationException {  // Three constructors should be implemented    public NoDescException(){}    public NoDescException(string message):base(message){}    public NoDescException(string message, Exception innerEx)          :base(message, innerEx){ } } // Interfaces that shape objects are to implement public interface IShapeFunction    { double GetArea(); } public interface IShapeDescription    { string ShowMe();} // Circle and Rectangle classes are defined class Circle : IShapeFunction {    private double radius;    public Circle (double rad)    {       radius= rad;    }    // Methods to implement both interfaces    public double GetArea()    {  return (3.14*radius*radius);  } } class Rectangle : IShapeFunction, IShapeDescription {    private int width, height;    public Rectangle(int w, int h)    {       width= w;       height=h;    }    // Methods to implement both interfaces    public double GetArea()    {  return (height*width);  }    public string ShowMe()    {  return("rectangle");    } } public class ObjAreas {    public static void ShowAreas(object ObjShape)    {       // Check to see if interfaces are implemented       if (!(ObjShape is IShapeDescription &&           ObjShape is IShapeFunction) )       {          // Throw custom exception          string objName = ObjShape.ToString();          throw new NoDescException             ("Interface not implemented for "+objName);       }       // Continue processing since interfaces exist       IShapeFunction myShape   = (IShapeFunction)ObjShape;       IShapeDescription myDesc = (IShapeDescription) ObjShape;       string desc = myDesc.ShowMe();       Console.WriteLine(desc+" Area= "+                         myShape.GetArea().ToString());    } } 

To view the custom exception in action, let's create two shape objects and pass them via calls to the static ObjAreas.ShowAreas method.

 Circle myCircle = new Circle(4.0); Rectangle myRect = new Rectangle(5,2); try {    ObjAreas.ShowAreas(myRect);    ObjAreas.ShowAreas(myCircle); } catch (NoDescException ex) {    Console.WriteLine(ex.Message); } 

The ShowAreas method checks to ensure the object it has received implements the two interfaces. If not, it throws an instance of NoDescException and control passes to the calling code. In this example, the Circle object implements only one interface, resulting in an exception.

Pay particular attention to the design of NoDescException. It is a useful model that illustrates the rules to be followed in implementing a custom exception type:

  • The class should be derived from ApplicationException.

  • By convention, the exception name should end in Exception. The Exception base class defines three public constructors that should be included:

    1. A parameterless constructor to serve as the default.

    2. A constructor with one string parameter usually the message.

    3. A constructor with a string parameter and an Exception object parameter that is used when an exception occurs while a previous exception is being handled.

  • Use the base initializer to call the base class to take care of the actual object creation. If you choose to add fields or properties, add a new constructor to initialize these values.

  • The Serializable attribute specifies that the exception can be serialized, which means it can be represented as XML for purposes of storing it or transmitting it. Typically, you can ignore this attribute, because it's only required if an exception object is being thrown from code in one application domain to another. Application domains are discussed Chapter 15, "Code Refinement, Security, and Deployment"; for now, think of them as logical partitions that .NET uses to isolate code segments.

Unhandled Exceptions

Unhandled exceptions occur when the CLR is unable to find a catch filter to handle the exception. The default result is that the CLR will handle it with its own methods. Although this provides a warning to a user or developer, it is not a recommended way to deal with it. The solution is in the problem: Take advantage of .NET's unhandled exception event handlers to funnel all of the exceptions to your own custom exception handling class.

The custom class provides a convenient way to establish a policy for dealing with unhandled exceptions. The code can be implemented to recognize whether it is dealing with a debug or release version, and respond accordingly. For example, in debug version, your main concern is to start the debugger; in a release version, you should log the error and provide a meaningful screen that allows the user to end the program.

Unfortunately, there is no single approach that applies to all C# programming needs. Your actual solution depends on whether you are working with a Console, Windows Form, Web Forms, or Web Services application. In this section, we will look at how to implement a Windows Forms solution, which is conceptually the same as for a Console application. Web Forms and Web Services are addressed in the Web Applications chapters, Chapters 16 18.

Unhandled Exceptions in a Windows Forms Application

Event handling was discussed in the previous chapter along with the important role of delegates. We can now use those techniques to register our own callback method that processes any unhandled exceptions thrown in the Windows application.

When an exception occurs in Windows, the application's OnThreadException method is ultimately called. It displays a dialog box describing the unhandled exception. You can override this by creating and registering your own method that matches the signature of the System.Threading.ThreadExceptionEventHandler delegate. Listing 4-4 shows one way this can be implemented.

MyUnhandledMethod is defined to handle the exception and must be registered to receive the callback. The following code registers the method for the THReadException event using the THReadExceptionEventHandler delegate and runs the application:

 static void Main() {    Application.ThreadException += new       ThreadExceptionEventHandler(UnForgiven.MyUnhandledMethod);    Application.Run(new Form1()); } 

The implementation code in the method is straightforward. The most interesting aspect is the use of preprocessor directives (#if DEBUG) to execute code based on whether the application is in release or debug mode.

Listing 4-4. Unhandled Exceptions in a Windows Form
 // Class to receive callback to process unhandled exceptions using System.Diagnostics; using System.Windows.Forms; using System.Threading; public class UnForgiven {    // Class signature matches ThreadExceptionEventHandler    public static void MyUnhandledMethod       (object sender, ThreadExceptionEventArgs e)    { #if DEBUG    // Statements for debug mode    // Display trace and start the Debugger    MessageBox.Show("Debug: "+e.ToString()); #else    // Statements for release mode    // Provide output to user and log errors    MessageBox.Show("Release: "+e.ToString()); #endif    } } 

For a Console application, the same approach is used except that the delegate and event names are different. You would register the method with the following:

 Thread.GetDomain().UnhandledException += new    UnhandledExceptionEventHandler(       UnForgiven.MyUnhandledMethodAp); 

Also, the method's EventArgs parameter changes to UnhandledExceptionEventArgs.

Exception Handling Guidelines

The use of exception handling is the key to implementing code that performs in a stable manner when unexpected conditions occur. But successful exception handling requires more than surrounding code with try and catch blocks. Thought must be given to which exceptions should be caught and what to do with a caught exception. Included here are some general rules to consider. For a more extensive list, consult online "best practices" information.

Only catch exceptions when you have a specific need to do the following:

  • Perform a recovery

  • Perform a cleanup, particularly to release resources

  • Log information

  • Provide extra debug information

Use exceptions to handle unexpected events, not as a way to implement logic.

Distinguish between error handling and exception handling. Incorrectly formatted credit card numbers or incorrect passwords, for example, are all occurrences that should be anticipated and handled by the logic of your code. An unavailable database table or hardware failure requires an exception.

Don't catch or throw base exception types.

The base exception type System.Exception is a catch-all exception that can be used to catch any exception this is not specifically identified and caught. Much like "fools gold," its intuitive appeal is deceptive. The objective of exception handling is to identify specific exceptions and handle with code appropriate for them; catching each exception prevents the unexpected ones from being identified. As a rule, catch specific exceptions and allow unidentified ones to propagate up the call stack.

Make liberal use of finally blocks.

The purpose of a finally block is to perform any housekeeping chores before a method is exited. It is implemented as part of both try/catch/finally and try/ finally constructs. Because code should be more liberal in its use of throws than catches, an application should contain more TRy/finally constructs than TRy/ catch constructs.

The inclusion of thorough, intelligent exception handling code in an application is one of the hallmarks of well-written code. Although you'll find code segments in this book where exception handling is ignored or used sparingly, understand that this is done only in the interest of simplifying code examples. In real-world programming, there are no excuses for leaving it out.

     < Day Day Up > 


    Core C# and  .NET
    Core C# and .NET
    ISBN: 131472275
    EAN: N/A
    Year: 2005
    Pages: 219

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