Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CSharpNotesForProfessionals.pdf
Скачиваний:
57
Добавлен:
20.05.2023
Размер:
6.12 Mб
Скачать

Chapter 76: Exception Handling

Section 76.1: Creating Custom Exceptions

You are allowed to implement custom exceptions that can be thrown just like any other exception. This makes sense when you want to make your exceptions distinguishable from other errors during runtime.

In this example we will create a custom exception for clear handling of problems the application may have while parsing a complex input.

Creating Custom Exception Class

To create a custom exception create a sub-class of Exception:

public class ParserException : Exception

{

public ParserException() :

base("The parsing went wrong and we have no additional information.") { }

}

Custom exception become very useful when you want to provide additional information to the catcher:

public class ParserException : Exception

{

public ParserException(string fileName, int lineNumber) : base($"Parser error in {fileName}:{lineNumber}")

{

FileName = fileName; LineNumber = lineNumber;

}

public string FileName {get; private set;} public int LineNumber {get; private set;}

}

Now, when you catch(ParserException x) you will have additional semantics to fine-tune exception handling.

Custom classes can implement the following features to support additional scenarios.

re-throwing

During the parsing process, the original exception is still of interest. In this example it is a FormatException because the code attempts to parse a piece of string, which is expected to be a number. In this case the custom exception should support the inclusion of the 'InnerException':

//new constructor:

ParserException(string msg, Exception inner) : base(msg, inner) {

}

serialization

In some cases your exceptions may have to cross AppDomain boundaries. This is the case if your parser is running in its own AppDomain to support hot reloading of new parser configurations. In Visual Studio, you can use Exception template to generate code like this.

[Serializable]

public class ParserException : Exception

{

GoalKicker.com – C# Notes for Professionals

448

//Constructor without arguments allows throwing your exception without

//providing any information, including error message. Should be included

//if your exception is meaningful without any additional details. Should

//set message by calling base constructor (default message is not helpful). public ParserException()

: base("Parser failure.")

{}

//Constructor with message argument allows overriding default error message.

//Should be included if users can provide more helpful messages than

//generic automatically generated messages.

public ParserException(string message) : base(message)

{}

//Constructor for serialization support. If your exception contains custom

//properties, read their values here.

protected ParserException(SerializationInfo info, StreamingContext context) : base(info, context)

{}

}

Using the ParserException

try

{

Process.StartRun(fileName)

}

catch (ParserException ex)

{

Console.WriteLine($"{ex.Message} in ${ex.FileName}:${ex.LineNumber}");

}

catch (PostProcessException x)

{

...

}

You may also use custom exceptions for catching and wrapping exceptions. This way many di erent errors can be converted into a single error type that is more useful to the application:

try

{

int foo = int.Parse(token);

}

catch (FormatException ex)

{

//Assuming you added this constructor throw new ParserException(

$"Failed to read {token} as number.", FileName,

LineNumber, ex);

}

When handling exceptions by raising your own custom exceptions, you should generally include a reference the original exception in the InnerException property, as shown above.

Security Concerns

If exposing the reason for the exception might compromise security by allowing users to see the inner workings of your application it can be a bad idea to wrap the inner exception. This might apply if you are creating a class library

GoalKicker.com – C# Notes for Professionals

449

that will be used by others.

Here is how you could raise a custom exception without wrapping the inner exception:

try

{

// ...

}

catch (SomeStandardException ex)

{

// ...

throw new MyCustomException(someMessage);

}

Conclusion

When raising a custom exception (either with wrapping or with an unwrapped new exception), you should raise an exception that is meaningful to the caller. For instance, a user of a class library may not know much about how that library does its internal work. The exceptions that are thrown by the dependencies of the class library are not meaningful. Rather, the user wants an exception that is relevant to how the class library is using those dependencies in an erroneous way.

try

{

// ...

}

catch (IOException ex)

{

// ...

throw new StorageServiceException(@"The Storage Service encountered a problem saving your data. Please consult the inner exception for technical details.

If you are not able to resolve the problem, please call 555-555-1234 for technical assistance.", ex);

}

Section 76.2: Finally block

try

{

/* code that could throw an exception */

}

catch (Exception)

{

/* handle the exception */

}

finally

{

/* Code that will be executed, regardless if an exception was thrown / caught or not */

}

The try / catch / finally block can be very handy when reading from files.

For example:

FileStream f = null;

try

{

f = File.OpenRead("file.txt");

/* process the file here */

GoalKicker.com – C# Notes for Professionals

450

}

finally

{

f?.Close(); // f may be null, so use the null conditional operator.

}

A try block must be followed by either a catch or a finally block. However, since there is no catch block, the execution will cause termination. Before termination, the statements inside the finally block will be executed.

In the file-reading we could have used a using block as FileStream (what OpenRead returns) implements

IDisposable.

Even if there is a return statement in try block, the finally block will usually execute; there are a few cases where it will not:

When a StackOverflow occurs.

Environment.FailFast

The application process is killed, usually by an external source.

Section 76.3: Best Practices

Cheatsheet

DO

Control flow with control statements

Keep track of ignored (absorbed) exception by logging

Repeat exception by using throw

Throw predefined system exceptions

Throw custom/predefined exception if it is crucial to application logic

Catch exceptions that you want to handle

DO NOT manage business logic with exceptions.

DON'T

Control flow with exceptions

Ignore exception

Re-throw exception - throw new ArgumentNullException() or throw ex

Throw custom exceptions similar to predefined system exceptions

Throw custom/predefined exceptions to state a warning in flow

Catch every exception

Flow control should NOT be done by exceptions. Use conditional statements instead. If a control can be done with if-else statement clearly, don't use exceptions because it reduces readability and performance.

Consider the following snippet by Mr. Bad Practices:

// This is a snippet example for DO NOT object myObject;

void DoingSomethingWithMyObject()

{

Console.WriteLine(myObject.ToString());

}

When execution reaches Console.WriteLine(myObject.ToString()); application will throw an NullReferenceException. Mr. Bad Practices realized that myObject is null and edited his snippet to catch & handle

NullReferenceException:

// This is a snippet example for DO NOT object myObject;

void DoingSomethingWithMyObject()

{

GoalKicker.com – C# Notes for Professionals

451

try

{

Console.WriteLine(myObject.ToString());

}

catch(NullReferenceException ex)

{

//Hmmm, if I create a new instance of object and assign it to myObject: myObject = new object();

//Nice, now I can continue to work with myObject

DoSomethingElseWithMyObject();

}

}

Since previous snippet only covers logic of exception, what should I do if myObject is not null at this point? Where should I cover this part of logic? Right after Console.WriteLine(myObject.ToString());? How about after the try...catch block?

How about Mr. Best Practices? How would he handle this?

// This is a snippet example for DO object myObject;

void DoingSomethingWithMyObject()

{

if(myObject == null) myObject = new object();

// When execution reaches this point, we are sure that myObject is not null

DoSomethingElseWithMyObject();

}

Mr. Best Practices achieved same logic with fewer code and a clear & understandable logic.

DO NOT re-throw Exceptions

Re-throwing exceptions is expensive. It negatively impact performance. For code that routinely fails, you can use design patterns to minimize performance issues. This topic describes two design patterns that are useful when exceptions might significantly impact performance.

DO NOT absorb exceptions with no logging

try

{

//Some code that might throw an exception

}

catch(Exception ex)

{

//empty catch block, bad practice

}

Never swallow exceptions. Ignoring exceptions will save that moment but will create a chaos for maintainability later. When logging exceptions, you should always log the exception instance so that the complete stack trace is logged and not the exception message only.

try

{

//Some code that might throw an exception

}

catch(NullException ex)

{

GoalKicker.com – C# Notes for Professionals

452