Errors and Exception Handling

 
Chapter 4 - Advanced C# Topics
bySimon Robinsonet al.
Wrox Press 2002
  

No matter how good your coding is, your programs will always have to be able to handle possible errors. For example, in the middle of some complex processing your code discovers that it doesn't have permission to read a file, or while it is sending network requests the network goes down. In such exceptional situations, it is not enough for a method to simply return an appropriate error code there might be 15 or 20 nested method calls, so what you really need the program to do is jump back up through all those 15 or 20 calls in order to exit the task completely and sort out the mess. C# has very good facilities to handle this kind of situation, through the mechanism known as exception handling .

Error handling facilities in VB are very restricted, and is essentially limited to the On Error GoTo statement. If you are coming from a VB background, you will find C# exceptions open up a whole new world of error handling in your programs. On the other hand, Java and C++ developers will be familiar with the principle of exceptions since these languages also handle errors in a similar way to C#. Developers using C++ are sometimes wary of exceptions because of possible C++ performance implications, but this is not the case in C#. Using exceptions in C# code does not in general adversely affect performance in any way.

Exception Classes

In C#, an exception is an object created (or thrown ) when a particular exceptional error condition occurs. This object contains information that should help track down the problem. Although we can create our own exception classes (and we will be doing so later), .NET provides us with many predefined exception classes too.

Base Class Exception Classes

In this section, we will do a quick survey of some of the exceptions that are available in the base classes. There are a large number of exception classes that Microsoft has defined, and it is not possible to provide anything like an exhaustive list here. This class hierarchy diagram shows a few of them, however, in order to give a flavor of the general pattern:

click to expand

All the classes in this diagram are in the System namespace, apart from IOException and the classes derived from IOException . These are in the namespace System.IO , which deals with reading and writing data to files. In general, there is no specific namespace for exceptions; exception classes should be placed in whatever namespace is appropriate to the classes that they could be generated by hence IO related exceptions are in the System.IO namespace, and you will find exception classes in quite a few of the base class namespaces.

The generic exception class, System.Exception is derived from System.Object , as we would expect for a .NET class. In general, you should not throw generic System.Exception objects from your code, because they give no idea of the specifics of the error condition.

There are two important classes in the hierarchy that are derived from System.Exception .

  • System.SystemException This is for exceptions that are usually thrown by the .NET runtime, or which are considered of a very generic nature and may be thrown by almost any application. As examples, StackOverflowException will be thrown by the .NET runtime if it detects the stack is full. On the other hand, you might choose to throw ArgumentException , or its subclasses in your own code, if you detect that a method has been called with inappropriate arguments. Subclasses of System.SystemException include ones that represent both fatal and non fatal errors.

  • System.ApplicationException This class is important, because it is the intended base for any class of exception defined by third parties (in other words, organizations other than Microsoft). Hence, if you define any exceptions covering error conditions unique to your application, you should derive these directly or indirectly from System.ApplicationException .

We are not going to discuss all of the other exception classes shown in the diagram, since, for the most part, their purposes should be clear from their names , but here is a small selection:

  • As just mentioned, StackOverflowException occurs if the area of memory allocated to the stack becomes completely full. A stack overflow can occur if a method continuously calls itself recursively. This is generally a fatal error, since it prevents your application from doing anything apart from terminating (in which case it is unlikely that even the finally block will execute). There is usually little point in your attempting to handle errors like this yourself.

  • We will cover streams in EndOfStreamException is an attempt to read past the end of a file.

  • An OverflowException is what will happen if for example you attempt to cast an int containing a value of 40 to a uint in a checked context.

The class hierarchy for exceptions is somewhat unusual in that most of these classes do not add any functionality to their respective base classes. However, in the case of exception handling, the usual reason for adding inherited classes is simply to indicate more specific error conditions, and there is often no need to override methods or add any new ones (although it is not uncommon to add extra properties that carry extra information about the error condition). For example, you might have a base ArgumentException class intended for method calls where inappropriate values are passed in, and an ArgumentNullException class derived from this, which is intended to be specifically for when a null argument is passed.

Catching Exceptions

Given that we have these predefined base class exception objects available to us, how do we make use of them in our code to trap error conditions? In order to deal with possible error conditions in C# code, you will normally divide the relevant part of your program into blocks of three different types:

  • try blocks contain code that forms part of the normal operation of your program, but which might encounter some serious error conditions

  • catch blocks contain the code that deals with the various error conditions

  • finally blocks contain the code that cleans up any resources or takes any other action that you will normally want done at the end of a try or catch block. It is important to understand that the finally block is executed whether or not any exception is thrown. Since the aim is that the finally block contains cleanup code that should always be executed, the compiler will flag an error if you place a return statement inside a finally block.

So how do these blocks fit together to trap error conditions? Here's how:

  1. The execution flow enters a try block.

  2. If no errors occur, execution proceeds normally through the try block, and when the end of the try block is reached, the flow of execution jumps to the finally block (Step 5). However, if an error occurs within the try block, execution jumps to a catch block ( next step).

  3. The error condition is handled in the catch block.

  4. At the end of the catch block, execution automatically transfers to the finally block.

  5. The finally block is executed.

The C# syntax used to bring all this about looks roughly like this:

   try     {     // code for normal execution     }     catch     {     // error handling     }     finally     {     // clean up     }   

Actually, there are a few variations on this theme:

  • You can omit the finally block.

  • You can also supply as many catch blocks as you want to handle different types of error.

  • You can omit the catch blocks altogether, in which case the syntax serves not to identify exceptions, but as a way of guaranteeing that code in the finally block will be executed when execution leaves the try block. This is useful if there are several exit points in the try block.

So far so good, but this leaves the question: if the code is running in the try block, how does it know to switch to the catch block if an error has occurred? If an error is detected , the code does something that is known as throwing an exception . In other words, it instantiates an exception object class and throws it:

   throw new OverflowException();   

Here we have instantiated an exception object of the OverflowException class. As soon as the computer encounters a throw statement inside a try block, it immediately looks for the catch block associated with that try block. If there is more than one catch block associated with the try block, it identifies the correct catch block by checking which exception class the catch block is associated with. For example, when the OverflowException object is thrown, execution will jump to the following catch block:

   catch (OverflowException e)     {   

In other words, the computer looks for the catch block that indicates a matching exception class instance of the same class (or of a base class).

With this extra information, we can expand out the try block we have just demonstrated. Let's assume, for the sake of argument, that there are two possible serious errors that can occur in it: an overflow and an array out of bounds. We will assume that our code contains two Boolean variables , Overflow and OutOfBounds , which indicate whether these conditions exist. We have already seen that a predefined exception class exists to indicate overflow ( OverflowException ); similarly, an IndexOutOfRangeException class exists to handle an array out-of-bounds.

Now our try block looks like this:

   try     {     // code for normal execution     if (Overflow == true)     throw new OverflowException();     // more processing         if (OutOfBounds == true)     throw new IndexOutOfRangeException();     // otherwise continue normal execution     }     catch (OverflowException e)     {     // error handling for the overflow error condition     }     catch (IndexOutOfRangeException e)     {     // error handling for the index out of range error condition     }     finally     {     // clean up     }   

So far, this might not look like we have achieved much that you can't do with VB's On Error GoTo , beyond the fact that we have more cleanly separated the different parts of the code. In fact, we have here a far more powerful and flexible mechanism for error handling.

This is because we can have throw statements that are nested in several method calls inside the try block, but the same try block continues to apply even as execution flow enters these other methods. If the computer encounters a throw statement, it immediately goes back up through all the method calls on the stack, looking for the end of the containing try block and the start of the appropriate catch block. As it does so, all the local variables in the intermediate method calls will correctly go out of scope. This makes the try...catch architecture beautifully suited to the situation we described at the beginning of this section, where the error occurs inside a method call that is nested inside 15 or 20 method calls, and processing has to stop immediately.

As you can probably gather from the above discussion, try blocks can play a very significant part in controlling the flow of execution of your code. However, it is important to understand that exceptions are intended for exceptional conditions, hence their name . It would be considered very bad programming style to, for example, use exceptions as a way of controlling when to exit a do...while loop.

Implementing Multiple catch Blocks

The easiest way to see how try...catch...finally blocks work in practice is with a couple of examples. Our first example is called SimpleExceptions . It repeatedly asks the user to type in a number and displays it. However, for the sake of this example, we will imagine that the number needs to be between 0 and 5, or the program won't be able to process the number properly. Therefore we will throw an exception if the user types in something anything outside this range.

The program continues to ask for more numbers to be processed until the user simply hits the Enter key without entering anything.

You should note that this code does not provide a good example of when to use exception handling. As we have already indicated, the idea of exceptions is that they are provided for exceptional circumstances. Users are always typing in silly things, so this situation doesn't really count. Normally, your program will handle incorrect user input by performing an instant check and asking the user to retype the input if there is a problem. However, generating exceptional situations is difficult in a small sample that you can read through in a few minutes! So, we will tolerate this bad practice for now in order to demonstrate how exceptions would work. The examples that follow will present more realistic situations.

The code for SimpleExceptions looks like this:

   using System;     namespace Wrox.ProCSharp.AdvancedCSharp     {     public class MainEntryPoint     {     public static void Main()     {     string userInput;     while (true)     {     try     {     Console.Write("Input a number between 0 and 5 " +     "(or just hit return to exit)> ");     userInput = Console.ReadLine();     if (userInput == "")     break;     int index = Convert.ToInt32(userInput);     if (index < 0  index > 5)     throw new IndexOutOfRangeException(     "You typed in " + userInput);     Console.WriteLine("Your number was " + index);     }     catch (IndexOutOfRangeException e)     {     Console.WriteLine("Exception: " +     "Number should be between 0 and 5. " + e.Message);     }     catch (Exception e)     {     Console.WriteLine(     "An exception was thrown. Message was: " + e.Message);     }     catch     {     Console.WriteLine("Some other exception has occurred");     }     finally     {     Console.WriteLine("Thank you");     }     }     }     }     }   

The core of this code is a while loop, which continually uses Console.ReadLine() to ask for user input. ReadLine() returns a string, so the first thing we do is convert it to an int using the System.Convert.ToInt32() method. The System.Convert class contains various useful methods to perform data conversions, and provides an alternative to the int.Parse() method. In general, System.Convert contains methods to perform various type conversions. Recall that the C# compiler resolves int to instances of the System.Int32 base class.

It is also worth pointing out that the parameter passed to the catch block is scoped to that catch block which is why we are able to use the same parameter name, e , in successive catch blocks in the above code.

In the above code, we also check for an empty string, since this is our condition for exiting the while loop. Notice how the break statement actually breaks right out of the enclosing try block as well as the while loop this is valid. Of course, as execution breaks out of the try block, the Console.WriteLine() statement in the finally block gets executed. Although we just display a greeting here, more commonly, you will be doing tasks like closing file handles and calling the Dispose() method of various objects in order to perform any cleaning up. Once the computer leaves the finally block, it simply carries on execution at the next statement that it would have executed, had the finally block not been present. In this case, we iterate back to the start of the while loop, and enter the try block again (unless the finally was entered as a result of executing the break statement in the while loop, in which case we simply exit the while loop).

Next, we check for our exception condition:

 if (index < 0  index > 5)    throw new IndexOutOfRangeException("You typed in " + userInput); 

When throwing an exception, we need to choose what type of exception to throw. Although the class System.Exception is available, it is really intended as a base class and it is considered bad programming practice actually to throw an instance of this class as an exception, because it conveys no information about the nature of the error condition. Instead, Microsoft has defined many other exception classes that are derived from System.Exception . Each of these matches a particular type of exception condition, and you are free to define your own ones too. The idea is that you give as much information as possible about the particular exception condition by throwing an instance of a class that matches the particular error condition. In this, case we have picked System.IndexOutOfRangeException as the best choice in the circumstances. IndexOutOfRangeException has several constructor overloads. The one we have chosen takes a string, which describes the error. Alternatively, we might choose to derive our own custom Exception object that describes the error condition in the context of our application.

Suppose the user then types in a number that is not between 0 and 5. This will be picked up by the if statement and an IndexOutOfRangeException object will be instantiated and thrown. At this point the computer will immediately exit the try block and hunt for a catch block that handles IndexOutOfRangeException . The first catch block it comes to is this:

 catch (IndexOutOfRangeException e) {    Console.WriteLine("Exception: Number should be between 0 and 5." + e.Message); } 

Since this catch block takes a parameter of the appropriate class, this will be passed the exception instance and executed. In this case, we display an error message and the Exception.Message property (which corresponds to the string we passed to IndexOutOfRange 's constructor). After executing this catch block, control switches to the finally block, just as if no exception had occurred.

Notice that we have also provided another catch block:

 catch (Exception e) {    Console.WriteLine("An exception was thrown. Message was: " + e.Message); } 

This catch block would also be capable of handling an IndexOutOfRangeException if it weren't for the fact that such exceptions will already have been caught by the previous catch block a reference to a base class can also refer to any instances of classes derived from it, and all exceptions are derived from System.Exception . So why doesn't this catch block get executed? The answer is that the computer executes only the first suitable catch block it finds. So why is this second catch block here? Well, it is not only our code that is covered by the try block; inside the block, we actually make three separate calls to methods in the System namespace ( Console.ReadLine() , Console.Write() , and Convert.ToInt32() ), and any of these methods might throw an exception.

If we type in something that's not a number say a or hello , then the Convert.ToInt32() method will throw an exception of the class System.FormatException , to indicate that the string passed into ToInt32() is not in a format that can be converted to an int . When this happens, the computer will trace back through the method calls, looking for a handler that can handle this exception. Our first catch block (the one that takes an IndexOutOfRangeException ) won't do. The computer then looks at the second catch block. This one will do because FormatException is derived from Exception , so a FormatException instance can be passed in as a parameter here.

The structure of our example is actually fairly typical of a situation with multiple catch blocks. We start off with catch blocks that are designed to trap very specific error conditions. Then, we finish with more general blocks that will cover any errors for which we have not written specific error handlers. Indeed, the order of the catch blocks is important. If we had written the above two blocks in the opposite order, the code would not have compiled, because the second catch block is unreachable (the Exception catch block would catch all exceptions).

However, we have a third catch block in our code too:

 catch {    Console.WriteLine("Some other exception has occurred"); } 

This is the most general catch block of all it doesn't take any parameter. The reason this catch block is here is to catch exceptions thrown by other code that isn't written in C#, or isn't even managed code at all. You see, it is a requirement of the C# language that only instances of classes that are derived from System.Exception can be thrown as exceptions, but other languages might not have this restriction C++ for example allows any variable whatsoever to be thrown as an exception. If your code calls into libraries or assemblies that have been written in other languages, then it may find an exception has been thrown that is not derived from System.Exception , although in many cases, the .NET PInvoke mechanism will trap these exceptions and convert them into .NET Exception objects. However, there is not that much that this catch block can do, because we have no idea what class the exception might represent.

For our particular example, there is probably not much point adding this catch-all catch handler. Doing this is useful if you are directly calling into some other libraries that are not .NET-aware and which might throw exceptions. However, we have included it in our example to illustrate the principle.

Now that we have analyzed the code for the example, we can run it. The following screenshot illustrates what happens with different inputs, and demonstrates both the IndexOutOfRangeException and the FormatException being thrown:

  SimpleExceptions  Input a number between 0 and 5 (or just hit return to exit)> 4 Your number was 4 Thank you Input a number between 0 and 5 (or just hit return to exit)> 0 Your number was 0 Thank you Input a number between 0 and 5 (or just hit return to exit)> 10 Exception: Number should be between 0 and 5. You typed in 10 Thank you Input a number between 0 and 5 (or just hit return to exit)> hello An exception was thrown. Message was: Input string was not in a correct format. Thank you Input a number between 0 and 5 (or just hit return to exit)> Thank you 

Catching Exceptions from Other Code

In our previous example, we have demonstrated the handling of two exceptions. One of them, IndexOutOfRangeException , was thrown by our own code. The other, FormatException , was thrown from inside one of the base classes. It is actually very common for code in a library to throw an exception if it detects that some problem has occurred, or if one of the methods has been called inappropriately by being passed the wrong parameters. However, library code rarely attempts to catch exceptions; this is regarded as the responsibility of the client code.

Often, you will find that exceptions get thrown from the base class libraries while you are debugging. The process of debugging to some extent involves determining why exceptions have been thrown and removing the causes. Your aim should be to ensure that by the time the code is actually shipped, exceptions really do only occur in very exceptional circumstances, and if possible, are handled in some appropriate way in your code.

System.Exception Properties

In our example, we have only illustrated the use of one property, Message , of the exception object. However, a number of other properties are available in System.Exception :

Property

Description

HelpLink

A link to a help file that provides more information about the exception.

Message

Text that describes the error condition.

Source

The name of the application or object that caused the exception.

StackTrace

Details of the method calls on the stack (to help track down the method that threw the exception).

TargetSite

A .NET reflection object that describes the method that threw the exception

InnerException

If this exception was thrown inside a catch block, it contains the exception object that sent the code into that catch block.

Of these properties, StackTrace and TargetSite are supplied automatically by the .NET runtime if a stack trace is available. Source will always be filled in by the .NET runtime as the name of the assembly in which the exception was raised (though you might wish to modify the property in your code to give more specific information), while Message , HelpLink , and InnerException must be filled in by the code that threw the exception, by setting these properties immediately before throwing the exception. So, for example, the code to throw an exception might look something like this:

   if (ErrorCondition == true)     {     Exception myException = new ClassmyException("Help!!!!");     myException.Source = "My Application Name";     myException.HelpLink = "MyHelpFile.txt";     throw myException;     }   

Here, ClassMyException is the name of the particular exception class you are throwing. Note that it is usual practice for the names of all exception classes to end with Exception .

What Happens if an Exception isn't Handled?

Sometimes an exception might be thrown, but there might not be a catch block in your code that is able to handle that kind of exception. Our SimpleExceptions example can serve to illustrate this. Suppose, for example, we omitted the FormatException and catch-all catch blocks, and only supplied the block that traps an IndexOutOfRangeException . In that event, what would happen if a FormatException got thrown?

The answer is that the .NET runtime would catch it. Later in this section we will see how it is possible to nest try blocks, and in fact, there is already a nested try block behind the scenes in the sample. The .NET runtime has effectively placed our entire program inside another huge try block it does this for every .NET program. This try block has a catch handler that can catch any type of exception. If an exception occurs that your code doesn't handle, then the execution flow will simply pass right out of your program and get trapped by this catch block in the .NET runtime. However, the results probably won't be what you wanted. It means execution of your code will be promptly terminated and the user will get presented with a dialog box that complains that your code hasn't handled the exception, as well as any details about the exception the .NET runtime was able to retrieve. At least the exception will have been caught though! This is what actually happened in Chapter 3 in the Vector example when our program threw an exception.

In general, if you are writing an executable, you should try to catch as many exceptions as you reasonably can, and handle them in a sensible way. If you are writing a library, it is normally best not to handle exceptions (unless a particular exception represents something wrong in your code which you can handle), but to assume instead that the calling code will handle them. However, you may nevertheless want to catch any Microsoft defined exceptions, so that you can throw your own exception objects that give more specific information to the client code.

Nested try Blocks

One nice feature of exceptions is that it is possible to nest try blocks inside each other, like this:

   try     {     // Point A     try     {     // Point B     }     catch     {     // Point C     }     finally     {     // clean up     }     // Point D     }     catch     {     // error handling     }     finally     {     // clean up     }   

Although each try block is only accompanied by one catch block in the example above, we could string several catch blocks together too. Let's now take a closer look at how nested try blocks work.

If an exception is thrown inside the outer try block but outside the inner try block (points A and D), then the situation is no different to any of the scenarios we have seen before: either the exception is caught by the outer catch block and the outer finally bock is executed, or the finally block is executed and the .NET runtime handles the exception.

If an exception is thrown in the inner try block (point B ) , and there is a suitable inner catch block to handle the exception, then again we are in familiar territory: the exception is handled there, and the inner finally block is executed before execution resumes inside the outer try block (at point D).

Now suppose an exception occurs in the inner try block but there isn't a suitable inner catch block to handle it. This time, the inner finally block is executed as usual, but then the .NET runtime will have no choice but to leave the entire inner try block in order to search for a suitable exception handler. The next obvious place to look is in the outer catch block. If the system finds one here, then that handler will be executed and then the outer finally block. If there is no suitable handler here, then the search for one will go on. In this case it means the outer finally block will be executed, and then, since there are no more catch blocks, control will transfer to the .NET runtime. Note that at no point is the code beyond point D in the outer try block executed.

An even more interesting thing happens if an exception is thrown at point C. If the program is at point C then it must be already processing an exception that was thrown at point B. It is in fact quite legitimate to throw another exception from inside a catch block. In this case, the exception is treated as if it had been thrown by the outer try block, so flow of execution will immediately leave the inner catch block, and execute the inner finally block, before the system searches the outer catch block for a handler. Similarly, if an exception is thrown in the inner finally block, control will immediately transfer to the best appropriate handler, with the search starting at the outer catch block.

Important 

It is perfectly legitimate to throw exceptions from catch and finally blocks.

Although we have shown the situation with just two try blocks, the same principles hold no matter how many try blocks you nest inside each other. At each stage, the .NET runtime will smoothly transfer control up through the try blocks, looking for an appropriate handler. At each stage, as control leaves a catch block, any cleanup code in the corresponding finally block will be executed, but no code outside any finally block will be run until the correct catch handler has been found and run.

We have now shown how having nested try blocks can work. The obvious next question is why would you want to do that? There are two reasons:

  • To modify the type of exception thrown

  • To enable different types of exception to be handled in different places in your code

Modifying the Type of Exception

Modifying the type of the exception can be useful when the original exception thrown does not adequately describe the problem. What typically happens is that something possibly the .NET runtime throws a fairly low-level exception that says something like an overflow occurred ( OverflowException ) or an argument passed to a method was incorrect (a class derived from ArgumentException ). However, because of the context in which the exception occurred, you will know that this reveals some other underlying problem (for example, an overflow can only have happened at that point in your code because a file you have just read contained incorrect data). In that case, the most appropriate thing that your handler for the first exception can do is throw another exception that more accurately describes the problem, so that another catch block further along can deal with it more appropriately. In this case, it can also forward the original exception through a property implemented by System.Exception called InnerException . InnerException simply contains a reference to any other related exception that was thrown in case the ultimate handler routine will need this extra information.

Of course there is also the situation where an exception occurs inside a catch block. For example, you might normally read in some configuration file that contains detailed instructions for handling the error, and it might turn out that this file is not there.

Handling Different Exceptions in Different Places

The second reason for having nested try blocks is so that different types of exception can be handled at different locations in your code. A good example of this is if you have a loop where various exception conditions can occur. Some of these might be serious enough that you need to abandon the entire loop, while others might be less serious and simply require that you abandon that iteration and move on to the next iteration around the loop. You could achieve this by having one try block inside the loop, which handles the less serious error conditions, and an outer try block outside the loop, which handles the more serious error conditions. We will see how this works in the exceptions example that we are going to unveil next.

User-Defined Exception Classes

We are now ready to look at a second example that illustrates exceptions. This example, called MortimerColdCall , will contain two nested try blocks, and also illustrates the practice of defining our own custom exception classes, and throwing another exception from inside a try block.

For this example, we are going to return to the Mortimer Phones mobile phone company that we used in Appendix A). We are going to assume that Mortimer Phones want some more customers. Its sales team is going to ring up a list of people to invite them to become customers, or to use sales jargon, they are going to 'cold-call' some people. To this end we have a text file available that contains the names of the people to be cold called. The file should be in a well-defined format in which the first line contains the number of people in the file and each subsequent line contains the name of the next person. In other words a correctly formatted file of names might look like this:

 4 Avon from 'Blake's 7' Zbigniew Harlequin Simon Robinson Christian Peak 

Since this is only an example, we are not really going to cold-call these people! Our version of cold-calling will be to display the name of the person on the screen (perhaps for the sales guy to read). That's why we have only put names, and not phone numbers in the file as well.

Our program will ask the user for the name of the file, and will then simply read it in and display the names of people.

That sounds like a simple task, but even here there are a couple of things that can go wrong and require us to abandon the entire procedure:

  • The user might type in the name of a file that doesn't exist. This will be caught as a FileNotFound exception.

  • The file might not be in the correct format. There are two possible problems here. Firstly, the first line of the file might not be an integer. Secondly, there might not be as many names in the file as the first line of the file indicates. In both cases, we want to trap this as a custom exception that we have written specially for this purpose, ColdCallFileFormatException .

There is also something else that could go wrong which won't cause us to abandon the entire process, but will mean we need to abandon that person and move on to the next person in the file (and hence this will need to be trapped by an inner try block). Some people are spies working for rival land-line telephone companies, and obviously, we wouldn't want to let these people know what we are up to by accidentally phoning one of them. Our research has indicated that we can identify who the land-line spies are because their names begin with Z. Such people should have been screened out when the data file was first prepared, but just in case any have slipped through, we will need to check each name in the file, and throw a LandLineSpyFoundException if we detect a land-line spy. This, of course, is another custom exception object.

Finally, we will implement this sample by coding up a class, ColdCallFileReader , which maintains the connection to the cold-call file and retrieves data from it. We will code up this class in a very safe way, which means its methods will all throw exceptions if they are called inappropriately, for example, if a method requiring a file read is called before the file has been opened. For this purpose, we will code up another exception class, UnexpectedException .

Catching the User-Defined Exceptions

Let's start with the Main() method of the MortimerColdCall sample, which catches our user-defined exceptions. Notice that we will need to call up file-handling classes in the System.IO namespace as well as the System namespace.

   using System;     using System.IO;     namespace Wrox.ProCSharp.AdvancedCSharp     {     class MainEntryPoint     {     static void Main()     {     string fileName;     Console.Write("Please type in the name of the file " +     "containing the names of the people to be cold-called > ");     fileName = Console.ReadLine();     ColdCallFileReader peopleToRing = new ColdCallFileReader();     try     {     peopleToRing.Open(fileName);     for (int i=0 ; i<peopleToRing.NPeopleToRing; i++)     {     peopleToRing.ProcessNextPerson();     }     Console.WriteLine("All callees processed correctly");     }     catch(FileNotFoundException e)     {     Console.WriteLine("The file {0} does not exist", fileName);     }     catch(ColdCallFileFormatException e)     {     Console.WriteLine(     "The file {0} appears to have been corrupted", fileName);     Console.WriteLine("Details of problem are: {0}", e.Message);     if (e.InnerException != null)     Console.WriteLine(     "Inner exception was: {0}", e.InnerException.Message);     }     catch(Exception e)     {     Console.WriteLine("Exception occurred:\n" + e.Message);     }     finally     {     peopleToRing.Dispose();     }     Console.ReadLine();     }     }   

This code is basically little more than a loop to process people from the file. We start off by asking the user for the name of the file. Then we instantiate an object of a class called ColdCallFileReader that we will define later. This is the class that handles the file reading. Notice that we do this outside the initial try block that's because the variables that we instantiate here need to be available in the subsequent catch and finally blocks, and if we declared them inside the try block they'd go out of scope at the closing curly brace of the try block.

In the try block we open the file ( ColdCallFileReader.Open() method), and loop over all the people in it. The ColdCallFileReader.ProcessNextPerson() method reads in and displays the name of the next person in the file, while the ColdCallFileReader.NPeopleToRing property tells us how many people should be in the file (obtained by reading the first line of the file).

There are three catch blocks, one for each of FileNotFoundException and ColdCallFileFormatException , and a third catch block to trap any other .NET exceptions.

In the case of a FileNotFoundException , we display a message to that effect. Notice that in this catch block, we don't actually use the exception instance at all. The reason is that I decided to use this catch block to illustrate the user-friendliness of our application. Exception objects generally contain technical information that is useful for developers, but not the sort of stuff you want to show to your end users. So in this case, we create a simpler message of our own.

For the ColdCallFileFormatException handler, we have done the opposite, and illustrated how to give fuller technical information, including details of the inner exception, if one is present.

Finally, if we catch any other generic exceptions, we display a user-friendly message, instead of letting any such exceptions fall through to the .NET runtime. Note that we have chosen not to handle any other exceptions not derived from System.Exception , since we are not calling directly into non-.NET code.

The finally block is there to clean up resources. In this case, this means closing any open file performed by the ColdCallFileReader.Dispose() method.

Throwing the User-Defined Exceptions

Now let's have a look at the definition of the class that handles the file reading, and ( potentially ) throws our user-defined exceptions: ColdCallFileReader . Since this class maintains an external file connection, we will need to make sure it gets disposed of correctly in accordance with the principles we laid down for the disposing of objects in Chapter 3. Hence we derive this class from IDisposable .

First, we declare some variables:

   class ColdCallFileReader :IDisposable     {     FileStream fs;     StreamReader sr;     uint nPeopleToRing;     bool isDisposed = false;     bool isOpen = false;   

FileStream and StreamReader , both in the System.IO namespace, are the base classes that we will use to read the file. FileStream allows us to connect to the file in the first place, while StreamReader is specially geared up to reading text files, and implements a method, StreamReader() , which reads a line of text from a file. We will look at StreamReader more closely in Chapter 12 when we discuss file handling in depth.

The isDisposed field indicates whether the Dispose() method has been called. We have chosen to implement ColdCallFileReader so that once Dispose() has been called, it is not permitted to reopen connections and reuse the object. isOpen is also used for error checking in this case, checking whether the StreamReader actually connects to an open file.

The process of opening the file and reading in that first line the one that tells us how many people are in the file is handled by the Open() method:

   public void Open(string fileName)     {     if (isDisposed)     throw new ObjectDisposedException("peopleToRing");     fs = new FileStream(fileName, FileMode.Open);     sr = new StreamReader(fs);     try     {     string firstLine = sr.ReadLine();     nPeopleToRing = uint.Parse(firstLine);     isOpen = true;     }     catch (FormatException e)     {     throw new ColdCallFileFormatException(     "First line isn\'t an integer", e);     }     }   

The first thing we do in this method (as with all other ColdCallFileReader methods) is check whether the client code has inappropriately called it after the object has been disposed of, and throw a predefined ObjectDisposedException object if that has occurred. The Open() method checks the isDisposed field to see whether Dispose() has already been called. Since calling Dispose() implies the caller has now finished with this object, we regard it as an error to attempt to open a new file connection if Dispose() has been called.

Next, the method contains the first of two inner try blocks. The purpose of this one is to catch any errors resulting from the first line of the file not containing an integer. If that problem arises, the .NET runtime will throw a FormatException , which we trap and convert to a more meaningful exception that indicates there is actually a problem with the format of the cold-call file. Note that System.FormatException is there to indicate format problems with basic data types, not with files, and so is not a particularly useful exception to pass back to the calling routine in this case. The new exception thrown will be trapped by the outermost try block. Since there is no cleanup needed here, there is no need for a finally block.

If everything is fine, we set the isOpen field to true to indicate that there is now a valid file connection from which data can be read.

The ProcessNextPerson() method also contains an inner try block:

   public void ProcessNextPerson()     {     if (isDisposed)     throw new ObjectDisposedException("peopleToRing");     if (!isOpen)     throw new UnexpectedException(     "Attempt to access cold call file that is not open");     try     {     string name;     name = sr.ReadLine();     if (name == null)     throw new ColdCallFileFormatException("Not enough names");     if (name[0] == 'Z')     {     throw new LandLineSpyFoundException(name);     }     Console.WriteLine(name);     }     catch(LandLineSpyFoundException e)     {     Console.WriteLine(e.Message);     }     finally     {     }     }   

There are two possible problems with the file here ( assuming there actually is an open file connection the ProcessNextPerson() method checks this first). First, we might read in the next name and discover that it is a land-line spy. If that condition occurs, the exception is trapped by the first of the catch blocks in this method. Since that exception has been caught here, inside the loop, it means that execution can subsequently continue in the Main() method of the program, and the subsequent names in the file will continue to be processed.

A problem may also occur if we try to read the next name and discover that we have already reached the end of the file. The way that the StreamReader 's ReadLine() method works, is if it has got past the end of the file, it doesn't throw an exception, but simply returns null . So if we find a null string, we know that the format of the file was incorrect because the number in the first line of the file indicated a larger number of names than were actually present in the file. If that happens, we throw a ColdCallFileFormatException , which will be caught by the outer exception handler (which will cause execution to terminate).

Once again, we don't need a finally block here since there is no cleanup to do, but this time we have put an empty one in, just to show that you can do so, if you want.

We have nearly finished the example. We have just two more members of ColdCallFileReader to look at: the NPeopleToRing property, which returns the number of people supposed to be in the file, and the Dispose() method, which closes an open file. Notice that the Dispose() method just returns if it has already been called this is the recommended way of implementing it. It also checks that there actually is a file stream to close before closing it. This example is here to illustrate defensive coding techniques, so that's what we are doing!

   public uint NPeopleToRing     {     get     {     if (isDisposed)     throw new ObjectDisposedException("peopleToRing");     if (!isOpen)     throw new UnexpectedException(     "Attempt to access cold call file that is not open");     return nPeopleToRing;     }     }     public void Dispose()     {     if (isDisposed)     return;     isDisposed = true;     isOpen = false;     if (fs != null)     {     fs.Close();     fs = null;     }     }   

Defining the Exception Classes

Finally, we need to define the three exceptions of our own. Defining our own exception is quite easy, since there are rarely any extra methods to add. It is just a case of implementing a constructor to ensure that the base class constructor is called correctly. Here is the full implementation of LandLineSpyFoundException :

   class LandLineSpyFoundException : ApplicationException     {     public LandLineSpyFoundException(string spyName)     :   base("LandLine spy found, with name " + spyName)     {     }     public LandLineSpyFoundException(     string spyName, Exception innerException)     :   base(     "LandLine spy found with name " + spyName, innerException)     {     }     }   

Notice we've derived it from ApplicationException , as you would expect for a custom exception. In fact, if we'd been going about this even more formally , we would probably have put in an intermediate class, something like ColdCallFileException , derived from ApplicationException , and derived both of our exception classes from this class, just to make sure that the handling code has that extra fine degree of control over which exception handlers handle which exception, but to keep the example simple, we won't do that.

We have done one bit of processing in LandLineSpyFoundException . We have assumed the 'message' passed into its constructor is just the name of the spy found, and so we turn this string into a more meaningful error message. We have also provided two constructors, one that simply takes a message, and one that also takes an inner exception as a parameter. When defining your own exception classes, it is best to include as a minimum, at least these two constructors (although we won't actually be using the second LandLineSpyFoundException constructor in this example).

Now for the ColdCallFileFormatException . This follows the same principles as the previous exception, except that we don't do any processing on the message:

   class ColdCallFileFormatException : ApplicationException     {     public ColdCallFileFormatException(string message)     :   base(message)     {     }     public ColdCallFileFormatException(     string message, Exception innerException)     :   base(message, innerException)     {     }     }   

And finally, UnexpectedException , which looks much the same as ColdCallFileFormatException :

   class UnexpectedException : ApplicationException     {     public UnexpectedException(string message)     :   base(message)     {     }     public UnexpectedException(string message, Exception innerException)     :   base(message, innerException)     {     }     }   

Now we are ready to test the program. First, we try the people.txt file whose contents we displayed earlier. This has four names (which matches the number given in the first line of the file) including one spy. Then, we will try the following people2.txt file, which has an obvious formatting error:

 49 Avon from 'Blake's 7' Zbigniew Harlequin Simon Robinson Christian Peak 

Finally, we will try the example but specify the name of a file that does not exist, people3.txt , say. Running the program three times for the three filenames gives these results:

  MortimerColdCall  Please type in the name of the file containing the names of the people to be cold-called > people.txt Avon from 'Blake's 7' LandLine spy found, with name Zbigniew Harlequin Simon Robinson Christian Peak All callees processed correctly MortimerColdCall Please type in the name of the file containing the names of the people to be cold-called > people2.txt Avon from 'Blake's 7' LandLine spy found, with name Zbigniew Harlequin Simon Robinson Christian Peak The file people2.txt appears to have been corrupted Details of the problem are: Not enough names MortimerColdCall Please type in the name of the file containing the names of the people to be cold-called > people3.txt The file people3.txt does not exist 
  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

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