C# Exceptions

C# allows a programmer almost all of the functionality provided by the CLR. To demonstrate exception handling in a C# environment, look at the next example that generates exceptions of various types. Because the code is rather long, the listing has been split into small sections. The full source for Listings 15.415.8 is in the BasicExceptions directory. Listing 15.4 shows an example of how C# code can be used to catch a DivideByZeroException.

Listing 15.4. Catching a Divide-By-Zero Exception
// Try a divide by zero
try
{
    int x = 0;
    x = 1/x;
}
catch(DivideByZeroException e)
{
    // Dump interesting exception information
    Console.WriteLine (e.ToString());
}

When the exception is caught, the output looks like this:

System.DivideByZeroException: Attempted to divide by zero.
 at Exceptions.BasicExceptions.Main(String[] args) in basicexceptions.cs:line 32

When an exception is converted to a string, the type of exception is first (System.DivideByZeroException), followed by a descriptive message, followed by a stack trace. In this case, not much stack exists because the code for this sample is all contained in Main. For cases in which a more complicated call-stack exists, this information can be invaluable in debugging. Listing 15.5 illustrates catching another runtime type of exception: a null reference exception.

Listing 15.5. Catching a Null Reference Exception
// Try an access violation
try
{
    Object o = null;
    Console.WriteLine(o.ToString());
}
catch(NullReferenceException e)
{
    Console.WriteLine (e.ToString());
}

When this part of the code executes, the output looks like this:

System.NullReferenceException: Value null was found where an instance of an object was
 required.
   at Exceptions.BasicExceptions.Main(String[] args) in basicexceptions.cs: line 46

Although the CLR and the compiler are pretty smart when it comes to avoiding and detecting errors, sometimes the CLR has no choice but to throw an exception. When the runtime encounters an object that is null and it tries to call a member function from a null instance, it throws an exception. This is not what happens under unmanaged code. Take for instance a class:

class SpecialClass
{
public:
    void Output()
    {
        std::cout << "Calling SpecialClass::Output()" << std::endl;
    }
} ;

What would you expect to happen with this code?

__try
{
    SpecialClass *p = 0;
    p->Output();
}
__except( EvalException(GetExceptionCode(), GetExceptionInformation()) )
{
    std::cout << "Caught exception" << std::endl;
}

Unfortunately, the Output() member function is called on the null instance with no exception thrown at all.

Most of the time you spend writing exception code will probably be spent either developing custom exception classes or catching and interpreting exceptions thrown by the .NET Framework. Listing 15.6 illustrates the definition and usage of a custom exception class.

Listing 15.6. Catching a Custom Exception
// Custom exception class derived from ApplicationException
class CustomException : ApplicationException
{
    public CustomException(String msg) : base(msg)
    {
    }
}
. . .
// Try custom exception
try
{
    throw new CustomException("I am throwing a custom exception");
}
catch(CustomException e)
{
    Console.WriteLine (e.ToString());
}

The first part of this listing illustrates the definition of a custom exception class derived appropriately from ApplicationException. Notice that no new functionality is added after creating this new exception class. In fact, as has already been noted, most all of the exception classes in the .NET Framework are defined much like this one. New functionality does not need to be added as a new type. The type is caught or filtered out. It is by type that a routine can decide whether it is in the best context to handle the exception.

The next section of code in Listing 15.6 illustrates throwing and catching this custom exception. Remember that all of the samples presented in this section are merely showing the concept of how to use exceptions. In the real world, this throw would be many levels deep in the call stack or this whole set of try/catch blocks could be nested in another set of try/catch blocks. The output for this section of code looks like this:

Exceptions.CustomException: I am throwing a custom exception
 at Exceptions.BasicExceptions.Main(String[] args) in basicexceptions.cs:line 59

Notice that it is possible to pass rich error information back to the user with exceptions. This is where the custom message is passed and the construction of the object is displayed. The type of exception is also shown. Much information is available in a custom exception with little work on the part of the programmer. However, this extra information does not come free. Constructing an instance of an exception class is expensive, as you will see later in this chapter. Exceptions are meant to handle exceptional conditions; using them for anything else will jeopardize the performance of your application.

Listing 15.7 illustrates an exception thrown by the framework. An InvalidCastException has been chosen to represent all of the other types of exceptions that can be thrown by the framework. This is probably not the best example, but it is one of the easiest to illustrate.

Listing 15.7. Catching an Object-Based Exception
// Try an object-based exception (InvalidCast)
try
{
    int n = 5;
    Object o = n;
    string s = (string)o;
}
catch(InvalidCastException e)
{
    Console.WriteLine (e.ToString());
}

When the integer n is boxed into an object, the instance of the object class knows its type. If an attempt is made to unbox the original value via a cast to a type that is different, an exception is thrown. The output after catching this exception is shown next:

System.InvalidCastException: Exception of type System.InvalidCastException was thrown.
   at Exceptions.BasicExceptions.Main(String[] args) in basicexceptions.cs: line 74

This could have been an exception thrown as a result of a File.Open called for a file that could not be found, or a result of trying to shrink an ArrayList, or any number of cases that cause the framework classes to throw an exception. This is the way that errors are communicated and handled within the .NET Framework. If in doubt, put a set of try/catch blocks around your code. Doing so will make your code more readable and the application more stable and manageable.

One concept that has not been covered so far is how a programmer can gracefully handle the case when the exception is not thrown. This is the reason for the finally clause in the try/catch blocks. Perhaps you open a file and read from it. If an error exists, you want to report the error and close the file. If no error is present, then you still want to be able to close the file; otherwise, you have a resource leak. Listing 15.8 shows an example of opening a file, reading the contents, and closing the file. If an error exists, it will be reported and the file will be closed. The code within the finally block will be executed no matter what happens.

Listing 15.8. Using finally
StreamReader streamReader = null;
try
{
    FileStream stream = File.OpenRead(@"....BasicExceptions.cs");
    streamReader = new StreamReader(stream, Encoding.ASCII);
    streamReader.BaseStream.Seek(0, SeekOrigin.Begin);
    string s = streamReader.ReadToEnd();
    // Console.WriteLine(s);
}
catch(Exception e)
{
    // Dump interesting exception information
    Console.WriteLine ("There has been an error:
{0} ", e.ToString());
}
finally
{
    Console.WriteLine ("Closing the file");
    if(streamReader != null)
        streamReader.Close();
}

If an exception is present, the catch block does not execute. If many catch blocks exist and an exception is not thrown, none of the catch blocks are executed. When you need to have a guarantee that a given code block will be executed, regardless of whether an exception is thrown, then finally is the way to go. In fact, try/finally is a perfect idiom and valid syntax when you want to ignore an error and execute a section of code upon exiting the try block, no matter how that exit occurs. Even though the finally block is guaranteed to execute, it has not caught an exception. If you just have try/finally and an exception is thrown while in the try block, your finally block will be executed, but the exception that is thrown has not been handled. If it is not handled, it could end up as an unhandled exception.

It is recommended that a custom exception should derive from ApplicationException. In addition, it is useful for your application to partition the errors into several sets of exceptions. This gives you a “divide and conquer” approach to debugging what went wrong when something does go wrong. Listing 15.9 shows a contrived example showing how you can set up try/catch to handle many errors at once.

Listing 15.9. Filtering Exceptions
class Custom1Exception : ApplicationException
{
    public Custom1Exception(String msg) : base(msg)
    {
    }
}
class Custom2Exception : ApplicationException
{
    public Custom2Exception(String msg) : base(msg)
    {
    }
}
class Custom3Exception : ApplicationException
{
    public Custom3Exception(String msg) : base(msg)
    {
    }
}
class Custom4Exception : ApplicationException
{
    public Custom4Exception(String msg) : base(msg)
    {
    }
}
. . .
Console.WriteLine("
Filtering exceptions
");
try
{
    Random r = new Random();
    switch(r.Next(4) + 1)
    {
        case 1:
            throw new Custom1Exception("I am throwing a Custom1Exception");
        case 2:
            throw new Custom2Exception("I am throwing a Custom2Exception");
        case 3:
            throw new Custom3Exception("I am throwing a Custom3Exception");
        case 4:
            throw new Custom4Exception("I am throwing a Custom4Exception");
    }
}
catch(Custom1Exception e)
{
    Console.WriteLine ("There has been a Custom1Exception error.
{0} ", e.ToString());
}
catch(Custom2Exception e)
{
    Console.WriteLine ("There has been a Custom2Exception error.
{0} ", e.ToString());
}
catch(Custom3Exception e)
{
    Console.WriteLine ("There has been a Custom3Exception error.
{0} ", e.ToString());
}
catch(Custom4Exception e)
{
    Console.WriteLine ("There has been a Custom4Exception error.
{0} ", e.ToString());
}

Notice that in this example, you don't need to specify a local variable for the instance of the exception that is caught. You only need to be interested in the type of exception that is thrown; here, you don't need any of the additional information that is contained in the exception class.

In a real-world application, you will want to build several different custom exception classes so that when an exception is thrown, you can quickly narrow down the cause. You might want to consider deriving the custom exceptions from SocketException, IOException, or ArithmeticException, or any other of the predefined exception classes that match the domain in which an error could occur. For example, you could derive a set of exception classes that derive from SocketException and are thrown by your application when your application accesses a socket. Again, portioning your error domain into a manageable set goes a long way toward helping you to maintain and debug your application.

If you have a relatively complex algorithm, you might find it useful to nest exception handling. This is handled nicely within C#. Listing 15.10 shows an example of a nested set of handlers.

Listing 15.10. Nested try/catch Blocks
Console.WriteLine("
Nested exceptions
");
try
{
    // Try a divide by zero
    try
    {
        int x = 0;
        x = 1/x;
    }
    catch(DivideByZeroException e)
    {
        // Dump interesting exception information
        Console.WriteLine (e.ToString());
        throw;
    }
}
catch(DivideByZeroException e)
{
    // Dump interesting exception information
    Console.WriteLine (e.ToString());
}

The listings so far have shown what happens when an exception is handled explicitly. Listing 15.11 shows how you can protect your code from unhandled exceptions with a last-ditch unhandled exception handler. If the exception reaches the top of the stack and it still has not found an appropriate handler, it is an unhandled exception. You can protect your code from crashing by installing an unhandled exception handler, as shown in Listing 15.11.

Listing 15.11. Unhandled Exceptions
Thread.GetDomain().UnhandledException +=
    new UnhandledExceptionEventHandler(UnhandledExceptionHandler);
. . .
// Try to divide by zero and see where it gets you.
int y = 0;
y = 1 / y;
. . .
public static void UnhandledExceptionHandler(object sender,
                                            UnhandledExceptionEventArgs args)
{
    Console.WriteLine("Unhandled Exception!");
    Console.WriteLine("Sender: {0} ", sender.ToString());
    Console.WriteLine("Type: {0} ", args.ExceptionObject.GetType().ToString());
    Console.WriteLine("IsTerminating: {0} ", args.IsTerminating);
}

The first section of this code shows how to register to be notified when an unhandled exception is thrown. The UnhandledException member of AppDomain is an Event. Remember Events? When an unhandled exception is generated and an Event handler has been registered, the AppDomain agrees to call each of the Event handler routines before the AppDomain shuts down. The Event handler has no means to recover; in fact, the IsTerminating member will indicate that the CLR is shutting down (the AppDomain is unloading). Any code after the unhandled exception is generated (here, after y = 1/y;) will not be executed. The output of this section of code looks like this:

Unhandled Exception!
Sender: Name: BasicExceptions.exe
There are no context policies.
Type: System.DivideByZeroException
IsTerminating: True

After this Event handler has been called, the AppDomain is well on in its winding down phase and cannot be stopped or reversed from the Event handler. The Event handler is just a means of notification.

This main function has been sprinkled with many try/catch blocks. You might have wondered about the overhead involved in setting up all of the try/catch blocks. Throwing an exception requires some overhead; however, unlike VC++, the code is relatively unaffected by exceptions that are never thrown. Other than the incremental cost of more code, virtually no overhead is involved with many try/catch blocks.

Note

It is possible that you are running in this code, and when an unhandled exception occurs, you get a dialog that prompts you for a debugger to use to help find the error. For these examples, just select No. This feature is called Just-In-Time Debugging and might be useful for a variety of problems; in this context, however, it can be ignored.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.1.158