Effective c# (5) – exception handling

Time:2021-8-15

Programs always make mistakes, because even if developers do it carefully, unexpected things will happen. Making the code stable in case of exceptions is a key skill that every c# programmer should master.
. net framework design guidelines suggests that if the method cannot complete the operation requested by the caller, you can consider throwing an exception. At this time, you must provide various information so that the caller can diagnose the problem accordingly.
In addition, it must be ensured that if the application can recover from errors, it must be in some known state.

Consider throwing an exception when the method convention is violated

If the method cannot fulfill its contract with the caller, it should be allowed to throw an exception. These failures should be indicated by exceptions. However, it should be noted that exceptions are not suitable as a conventional means of controlling program flow, because throwing exceptions is expensive and will lead to many try catches in the code. Therefore, another set of methods should be provided at the same time, so that developers can judge whether the operation can be executed smoothly before executing the operation, so as to take corresponding measures when it cannot be executed smoothly, rather than wait until an exception is thrown.
Take the file. Open of the class library as an example. It will throw an exception when it cannot complete the operation; However, file. Exists is also provided to determine whether the file exists. Therefore, the caller can judge the exists before opening. Of course, in addition to the non existence of the file, the occupation of the file and lack of permission will also lead to the failure of opening. This is a detailed problem of file operation, but this design idea can be used for reference.
Assuming that the dowork method is provided, it can be implemented according to the previous idea

public bool TryDoWork()
{
  if(!TestConditions())
    return false;
  DoWork();
  return true;
}

public void DoWork(){...}

public bool TestConditions()
{
  ...
}

Create exceptions specifically for your application

Exceptions are a mechanism for reporting errors. Sometimes you need to create custom exceptions. But first of all, it should be clear that not all errors must be expressed as exceptions. As for which errors need to be expressed as exceptions, there is no fixed law to follow. Generally speaking, if a situation must be handled or reported immediately, otherwise it will affect the application for a long time, an exception should be thrown. For example, if there is a data integrity problem in the database, an exception should be thrown immediately; However, if it is just impossible to record the folding and opening status of an attempt, because it will not cause serious impact, it can be considered to return only the error code.

Then, you don’t need to create an exception class for all throw statements, but it’s not appropriate to throw them all with the exception base class.
The main reason why we want to create different exception classes is to enable the caller to capture those conditions through different catch clauses, so as to adopt different processing methods. Therefore, we can judge whether to create a new exception class or reuse the existing class based on this point.

Once you decide to create your own exception class, you must follow the corresponding principles:

  • Inherit exception base class
  • Subclasses should provide the same constructor overload as the exception base class, and then delegate the corresponding work to the base class

Give priority to strong exception assurance

When an operation throws an exception, it should be responsible for managing its own state, which will directly affect whether the person who catches the exception has more room to deal with the exception.

There are three types of guarantees for exceptions:

  • Basic guarantee ensures that when an exception leaves the function that generates the exception, the resources in the program will not be leaked, and all objects are in a valid state. This is equivalent to specifying the effect that the method throwing the exception must achieve after running its finally clause.
  • Strong guarantee is made on the basis of basic guarantee. It requires that the state of the whole program cannot be changed because an operation throws an exception.
  • No throw guarantees that the method that performs the operation will never throw an exception.

. net CLR provides some basic guarantees, such as memory management in case of exceptions. Unless your resources implement the IDisposable interface, resource leakage is unlikely to occur in this case.

Examples of no throw guarantee include finalizer, dispose method and when clause of catch. In addition, the no throw guarantee should also be observed when writing the delegate target method. In these cases, any exception should not be separated from its scope.

Among these three attitudes, strong assurance is a compromise. It not only allows the program to throw an exception and recover from it, but also makes it easier for developers to deal with the exception.

Under the strong exception guarantee, if an operation throws an exception, the state of the application must be the same as before the operation. This operation is either completely successful or completely failed. If it fails, as like as two peas of execution, the program should not be partially successful.
For example, when modifying set data, in order to ensure strong exception, you can consider making a defensive copy of the data to be modified, and then perform operations on the copied data. If the operation runs smoothly without throwing an exception, replace the original data with this data to change the state of the program; When an exception occurs, the original data is still complete.
It can also be seen from the above example that to ensure strong exception assurance, the performance of the program will often be reduced. However, in many cases, the ability to recover from errors is more important than a slight improvement in performance.

Consider using an exception filter to override the logic of catching exceptions first and then throwing them again

In case of catch exception, it is sometimes necessary to judge the program state, object state or attributes in the exception before handling it. You usually think of judging in the catch block and finally throwing the exception again.
But the exception filter is more recommended, because after using the exception filter, the code generated by the compiler will first judge the value of the exception filter, and then consider whether to perform stack unwrapping. Therefore, the original location of the exception can be retained, and all information in the call stack (including the value of local variables) can also remain unchanged.

In contrast, if used in a catch blockthrow eIf the exception is thrown again, the place where the exception reported by the system occurs is the place where the throw statement is located, which will lead to the loss of the exception stack information and the directthrowAlthough the information of the original stack can be retained, this writing method processed in the catch block will enter the catch block and expand the stack every time, which will produce large running overhead.

Make rational use of the side effects of exception filters

Generally speaking, the conditions in the exception filter should always be met in some cases. If they can never be met, the filter will lose its meaning. However, sometimes, in order to monitor the exceptions in the program, you can still consider writing this filter that always returns false. At this time, the call stack has not been really expanded, but the exception information can be obtained.
For example, it can be used to record exceptions:

public static void Filter()
{
  try
  {
    // ...
  }
  catch (Exception e) when (ForWhen(e)) { }
  catch (FormatException e)
  {
    // handle exception
  }
}

public static bool ForWhen(Exception e)
{
  Console.WriteLine($"captured in when, msg:{e.Message}");
  return false;
}

takecatch (Exception e) when (ForWhen(e)) { }All exceptions can be recorded before all catches, but attention should also be paid here:

  • This line of code should be the exception base class. Unless there is a special purpose, only catch some exceptions
  • The when condition always returns false
  • The code executing when condition judgment shall be guaranteed with no throw

Reference books

Effective c#: 50 effective ways to improve c# code (3rd edition of the original book) Bill Wagner