C#, like
many object-oriented languages, handles errors and abnormal
conditions with exceptions
. An exception is an
object that encapsulates information about an unusual program
occurrence.
It is important to distinguish between bugs, errors, and exceptions. A bug is a programmer mistake that should be fixed before the code is shipped. Exceptions are not a protection against bugs. Although a bug might cause an exception to be thrown, you should not rely on exceptions to handle your bugs. Rather, you should fix the bug.
An error is caused by user action. For example, the user might enter a number where a letter is expected. Once again, an error might cause an exception, but you can prevent that by catching errors with validation code. Whenever possible, errors should be anticipated and prevented.
Even if you remove all bugs and anticipate all user errors, you will still run into predictable but unpreventable problems, such as running out of memory or attempting to open a file that no longer exists. You cannot prevent exceptions, but you can handle them so that they do not bring down your program.
When your program encounters an exceptional circumstance, such as running out of memory, it throws (or “raises”) an exception. When an exception is thrown, execution of the current function halts and the stack is unwound until an appropriate exception handler is found.
This means that if the currently running function does not handle the exception, the current function will terminate and the calling function will get a chance to handle the exception. If none of the calling functions handles it, the exception will ultimately be handled by the CLR, which will abruptly terminate your program.
An exception handler is a block of code designed
to handle the exception you’ve thrown. Exception handlers are
implemented as catch
statements. Ideally, if the
exception is caught and handled, the program can fix the problem and
continue. Even if your program can’t continue, by catching the
exception you have an opportunity to print a meaningful error message
and terminate gracefully.
If there is code in your function that must run regardless of whether
an exception is encountered (e.g., to release resources you’ve
allocated), you can place that code in a
finally
block, where it is certain to run, even
in the presence of exceptions.
In C#, you can throw only objects of
type System.Exception
, or objects derived from
that type. The CLR System
namespace includes a
number of exception types that can be used by your program. These
exception types include ArgumentNullException
,
InValidCastException
, and
OverflowException
, as well as many others.
To
signal an
abnormal condition in a C# class, you throw an exception. To do this,
you use the keyword throw
. This line of code
creates a new instance of System.Exception
and
then throws it:
throw new System.Exception( );
Throwing an exception immediately halts execution while the CLR
searches for an exception handler. If an exception handler cannot be
found in the current method, the runtime unwinds
the stack, popping up through the calling methods until a handler is
found. If the runtime returns all the way through Main( )
without finding a handler, it terminates the program.
Example 11-1 illustrates.
Example 11-1. Throwing an exception
namespace Programming_CSharp
{
using System;
public class Test
{
public static void Main( )
{
Console.WriteLine("Enter Main...");
Test t = new Test( );
t.Func1( );
Console.WriteLine("Exit Main...");
}
public void Func1( )
{
Console.WriteLine("Enter Func1...");
Func2( );
Console.WriteLine("Exit Func1...");
}
public void Func2( )
{
Console.WriteLine("Enter Func2...");
throw new System.Exception( );
Console.WriteLine("Exit Func2...");
}
}
}
Output:
Enter Main...
Enter Func1...
Enter Func2...
Exception occurred: System.Exception: An exception of type
System.Exception was thrown.
at Programming_CSharp.Test.Func2( )
in ...exceptions01.cs:line 26
at Programming_CSharp.Test.Func1( )
in ...exceptions01.cs:line 20
at Programming_CSharp.Test.Main( )
in ...exceptions01.cs:line 12
This simple example writes to the console as it enters and exits each
method. Main( )
creates an instance of type
Test
and call Func1( )
. After
printing out the Enter
Func1
message, Func1( )
immediately calls
Func2( )
. Func2( )
prints out
the first message and throws an object of type
System.Exception
.
Execution immediately stops, and the CLR looks to see if there is a
handler in Func2( )
. There is not, and so the
runtime unwinds the stack (never printing the exit
statement) to Func1( )
. Again, there is no
handler, and the runtime unwinds the stack back to Main( )
. With no exception handler
there, the default handler is called, which prints the error message.
In C#, an
exception handler is called a
catch
block
and is created with the catch
keyword.
In Example 11-2, the throw
statement is executed within a try
block, and a
catch
block is used to announce that the error has
been handled.
Example 11-2. Catching an exception
namespace Programming_CSharp { using System; public class Test { public static void Main( ) { Console.WriteLine("Enter Main..."); Test t = new Test( ); t.Func1( ); Console.WriteLine("Exit Main..."); } public void Func1( ) { Console.WriteLine("Enter Func1..."); Func2( ); Console.WriteLine("Exit Func1..."); } public void Func2( ) { Console.WriteLine("Enter Func2..."); try{
Console.WriteLine("Entering try block...");
throw new System.Exception( );
Console.WriteLine("Exiting try block...");
}
catch{
Console.WriteLine(
"Exception caught and handled.");
}
Console.WriteLine("Exit Func2..."); } } } Output: Enter Main... Enter Func1... Enter Func2... Entering try block... Exception caught and handled. Exit Func2... Exit Func1... Exit Main...
Example 11-2 is identical to Example 11-1 except that now the program includes a
try
/catch
block. You would
typically put the try
block around a potentially
“dangerous” statement, such as accessing a file,
allocating memory, and so forth.
Following the try
statement is a generic
catch
statement. The
catch
statement in Example 11-2 is
generic because you haven’t specified what kind of exceptions
to catch. In this case the statement will catch any exceptions that
are thrown. Using catch
statements to catch
specific types of exceptions is discussed later in this chapter.
In Example 11-2, the
catch
statement simply reports that the
exception has been caught and handled. In a real-world example, you
might take corrective action to fix the problem that caused an
exception to be thrown. For example, if the user is trying to open a
read-only file, you might invoke a method that allows the user to
change the attributes of the file. If the program has run out of
memory, you might give the user an opportunity to close other
applications. If all else fails, the catch
block
can print an error message so that the user knows what went wrong.
Examine the output of Example 11-2 carefully. You see the code enter
Main( )
, Func1( )
,
Func2( )
, and the
try
block. You never see it exit the
try
block, though it does exit Func2( )
, Func1( )
, and Main( )
. What happened?
When the exception is thrown, execution halts immediately and is
handed to the catch
block. It
never returns to the original code path. It
never gets to the line that prints the exit
statement for the try
block. The
catch
block handles the error, and then execution
falls through to the code following catch
.
Without catch
the call
stack unwinds, but with
catch
it does not unwind as a result of the
exception. The exception is now handled; there are no more problems;
and the program continues. This becomes a bit clearer if you move the
try
/catch
blocks up to
Func1( )
, as shown in Example 11-3.
Example 11-3. Catch in a calling function
namespace Programming_CSharp { using System; public class Test { public static void Main( ) { Console.WriteLine("Enter Main..."); Test t = new Test( ); t.Func1( ); Console.WriteLine("Exit Main..."); } public void Func1( ) { Console.WriteLine("Enter Func1...");try
{
Console.WriteLine("Entering try block...");
Func2( );
Console.WriteLine("Exiting try block...");
}
catch
{
Console.WriteLine(
"Exception caught and handled.");
}
Console.WriteLine("Exit Func1..."); } public void Func2( ) { Console.WriteLine("Enter Func2...");throw new System.Exception( );
Console.WriteLine("Exit Func2..."); } } } Output: Enter Main... Enter Func1... Entering try block... Enter Func2... Exception caught and handled. Exit Func1... Exit Main...
This time the exception is not handled in Func2( )
; it is handled in Func1( )
. When
Func2( )
is called, it prints the
Enter
statement and then throws an exception.
Execution halts and the runtime looks for a handler, but there
isn’t one. The stack unwinds, and the runtime finds a handler
in Func1( )
. The catch
statement is called, and execution resumes immediately following the
catch
statement, printing the
Exit
statement for Func1( )
and
then for Main( )
.
Make sure you are comfortable with why the Exiting Try Block
statement and the Exit Func2
statement are not printed. This is a classic case where putting the
code into a debugger and then stepping through it can make things
very clear.
So far, you’ve been working only with
generic catch
statements. You can create dedicated
catch
statements that handle only some exceptions
and not others, based on the type of exception thrown. Example 11-4 illustrates how to specify which exception
you’d like to handle.
Example 11-4. Specifying the exception to catch
namespace Programming_CSharp { using System; public class Test { public static void Main( ) { Test t = new Test( ); t.TestFunc( ); } // try to divide two numbers // handle possible exceptions public void TestFunc( ) { try { double a = 5; double b = 0; Console.WriteLine ("{0} / {1} = {2}", a, b, DoDivide(a,b)); } // most derived exception type firstcatch (System.DivideByZeroException)
{ Console.WriteLine( "DivideByZeroException caught!"); }catch (System.ArithmeticException)
{ Console.WriteLine( "ArithmeticException caught!"); } // generic exception type lastcatch
{ Console.WriteLine( "Unknown exception caught"); } } // do the division if legal public double DoDivide(double a, double b) { if (b == 0) throw new System.DivideByZeroException( ); if (a == 0) throw new System.ArithmeticException( ); return a/b; } } } Output: DivideByZeroException caught!
In this example, the DoDivide( )
method will not
let you divide zero by another number, nor will it let you divide a
number by zero. It throws an instance of
DivideByZeroException
if you try to divide by
zero. If you try to divide zero by another number, there is no
appropriate exception: dividing zero by another number is a legal
mathematical operation and shouldn’t throw an exception at all.
For the sake of this example, assume you don’t want to allow
division by zero; you will throw an
ArithmeticException
.
When the exception is thrown, the runtime examines each exception
handler in order and matches the first one it
can. When you run this with a=5
and
b=7
, the output is:
5 / 7 = 0.7142857142857143
As you’d expect, no exception is thrown. However, when you
change the value of a
to 0
, the
output is:
ArithmeticException caught!
The exception is thrown, and the runtime examines the first
exception: DivideByZeroException
. Because this
does not match, it goes on to the next handler,
ArithmeticException
, which does match.
In a final pass through, suppose you change a
to
7
and b
to
0
. This throws the
DivideByZeroException
.
You have to be particularly careful with the order of the
catch
statements, because the
DivideByZeroException
is derived from
ArithmeticException
. If you reverse the catch
statements, the DivideByZeroException
will match
the ArithmeticException
handler and the exception
will never get to the DivideByZeroException
handler. In fact, if their order is reversed, it will be impossible
for any exception to reach the
DivideByZeroException
handler. The compiler will
recognize that the DivideByZeroException
handler
cannot be reached and will report a compile error!
It is possible to distribute your
try
/catch
statements, catching
some specific exceptions in one function and more generic exceptions
in higher, calling functions. Your design goals should dictate the
exact design.
Assume you have a method A that calls another method B, which in turn
calls method C. Method C calls method D, which then calls method E.
Method E is deep in your code; methods B and A are higher up. If you
anticipate that method E might throw an exception, you should create
a try
/catch
block deep in your
code to catch that exception as close as possible to the place where
the problem arises. You might also want to create more general
exception handlers higher up in the code in case unanticipated
exceptions slip
by.
In some instances, throwing an exception and unwinding the stack can create a problem. For example, if you have opened a file or otherwise committed a resource, you might need an opportunity to close the file or flush the buffer.
In C#, this is less of a problem than in other languages, such as C++, because the garbage collection prevents the exception from causing a memory leak.
In the event, however, that there is some action you must take
regardless of whether an exception is thrown, such as closing a file,
you have two strategies to choose from. One approach is to enclose
the dangerous action in a try
block and then to
close the file in both the catch
and
try
blocks. However, this is an ugly duplication
of code, and it’s error prone. C# provides a better alternative
in the finally
block.
The code in the finally
block is guaranteed to be
executed regardless of whether an exception is thrown. The
TestFunc( )
method in Example 11-5
simulates opening a file as its first action. The method then
undertakes some mathematical operations, and then the file is closed.
It is possible that some time between opening and closing the file an
exception will be thrown. If this were to occur, it would be possible
for the file to remain open. The developer knows that no matter what
happens, at the end of this method the file should be closed, so the
file
close
function call is
moved to a finally
block, where it will be
executed regardless of whether an exception is thrown.
Example 11-5. Using a finally block
namespace Programming_CSharp { using System; public class Test { public static void Main( ) { Test t = new Test( ); t.TestFunc( ); } // try to divide two numbers // handle possible exceptions public void TestFunc( ) { try { Console.WriteLine("Open file here"); double a = 5; double b = 0; Console.WriteLine ("{0} / {1} = {2}", a, b, DoDivide(a,b)); Console.WriteLine ( "This line may or may not print"); } // most derived exception type first catch (System.DivideByZeroException) { Console.WriteLine( "DivideByZeroException caught!"); } catch { Console.WriteLine("Unknown exception caught"); }finally
{
Console.WriteLine ("Close file here.");
}
} // do the division if legal public double DoDivide(double a, double b) { if (b == 0) throw new System.DivideByZeroException( ); if (a == 0) throw new System.ArithmeticException( ); return a/b; } } } Output: Open file here DivideByZeroException caught! Close file here. Output when b = 12: Open file here 5 / 12 = 0.41666666666666669 This line may or may not print Close file here.
In this example, one of the catch
blocks has been
eliminated to save space and a finally
block has
been added. Whether or not an exception is thrown, the
finally
block is executed, and so in both output
examples you see the message: Close
file
here
.
18.191.14.93