P&P笔记(X1) Exception Handle (一)

这一篇主要整理Exception的机制.

什么是Exception以及Exception Handle

回答这个问题, 下面这段讲Exception与Assert区别的文字, 从另一个角度解释了什么是异常. 有醍醐灌顶, 正本清源的感觉.

Exception与Assert的区别

链接https://docs.microsoft.com/en-us/cpp/cpp/errors-and-exception-handling-modern-cpp

Exceptions and asserts are two distinct mechanisms for detecting run-time errors in a program. Use asserts to test for conditions during development that should never be true if all your code is correct. There is no point in handling such an error by using an exception because the error indicates that something in the code has to be fixed(是代码本身出了问题), and doesn’t represent a condition that the program has to recover from at run time. An assert stops execution at the statement so that you can inspect the program state in the debugger; an exception continues execution from the first appropriate catch handler. Use exceptions to check error conditions that might occur at run time even if your code is correct, for example, “file not found” or “out of memory.” You might want to recover from these conditions, even if the recovery just outputs a message to a log and ends the program. Always check arguments to public functions by using exceptions. Even if your function is error-free, you might not have complete control over arguments that a user might pass to it.

再看C# in a nutshell中怎么说

An assertion is something that, if violated, indicates a bug in the current method’s code.
Throwing an exception based on argument validation indicates a bug in the caller’s code.

例如一个复杂的算法内部在进行下一个计算时, 上一个计算得到的值必须符合某个范围. 如果在范围外, 则表示该算法的计算步骤有误, 这种情况下, 使用的是断言. 断言判断的是程序本身的逻辑问题.异常, 就像链接中所说的那样, 程序本身没有问题. 是运行时的问题, 例如输入, 网络异常, 读写异常等等. 有些异常可以恢复, 例如FileNotFoundException; 有些异常不能恢复, 例如StackOverflowException. 异常处理的做法是, 对于可恢复的异常, 根据程序上下文, 恢复程序的正常执行; 对于那些不可恢复的异常, 则记录日志, 再次抛出异常, 终止程序执行.

throw还是不throw

对于函数设计而言, 什么情况下需要抛出异常, 什么情况下不抛出异常, 可以用if来替代.

一种说法是尽量少throw, 例如

Best Practices - Exception Handling in C# .NET
https://www.c-sharpcorner.com/UploadFile/84c85b/best-practices-exception-handling-in-C-Sharp-net/
中提到2点

  1. Do Not Throw Exceptions to Control Application Flow
  2. Use Validation Code to Reduce Unnecessary Exceptions

像除以0这种, 用if判断即可, 不要throw exception.

另一种说法是Always check arguments to public functions by using exceptions
看List的源码发现很多throw exception
https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,cf7f4095e4de7646

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 public List(int capacity) {
if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);

public List(IEnumerable<T> collection) {
if (collection == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);

public List<T> FindAll(Predicate<T> match) {
if( match == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
Contract.EndContractBlock();

List<T> list = new List<T>();
for(int i = 0 ; i < _size; i++) {
if(match(_items[i])) {
list.Add(_items[i]);
}
}
return list;
}

该如何选择?

其实仔细看一下上述List的方法, 可以发现一些线索.像构造方法这种, 如果输入参数有误, 是没有办法通过if来规避的, 只能抛出异常; 再看FindAll方法, API的设计是需要返回一个列表. 假如match为null时不抛出异常, 那么是return null还是return new list()呢? return new List的话, 即使match不是null, 也有可能返回. 即所有的item都不匹配. return null的话, 从API的设计角度, 是不恰当的. 调用者调用List的FindAll方法, 不应该返回null. 当不知道返回什么时, throw exception是一个更好的选择, throw new exception可以当做返回值, 编译器不会报错. 而且List属于底层库. 它没有程序上下文, 也不需要去recover. 直接像上层抛出异常即可.

再例如:

1
2
3
4
5
6
7
public List<People> GetAllCustomersByAge(int age)
{
if (age < 18 || age > 150)
{
throw new ArgumentOutOfRangeException("Age must be between 18 and 150.", nameof(age));
}
}

虽然这是上层逻辑, 但该方法内部缺少恢复的程序上下文, 抛出异常是合适的.

现在再来讨论可以不抛出异常的情况.
https://github.com/dotnet/training-tutorials/blob/master/content/csharp/getting-started/exceptions.md
该链接中有这样一段文字和示例:

When to Avoid Throwing and Catching

It is important that you only throw exceptions when it’s appropriate, when your code is in an unintended(没预料到的) situation. If the situation seems likely(预料到的), but requires taking a certain action, just handle it using normal application flow control elements like if statements.

A rule of thumb for determining if you need to throw an exception is if you encounter a situation in a method that doesn’t allow you to return a valid result. In that case, an exception may be the best solution, or you may need to reconsider what your method is returning. The following two examples demonstrate two ways to deal with a method failing to perform its intended task:

1
2
3
4
5
6
7
8
9
public void SetMemberBirthday(int memberId, DateTime birthday)
{
Member member = _memberList.SingleOrDefault(m => m.Id == memberId);
if (member == null)
{
throw new MemberNotFoundException(id);
}
member.Birthday = birthday;
}

没有找到member是一种预料之中的情况, 这种情况下, 可以更改API的设计, 来避免抛出异常

1
2
3
4
5
6
7
8
9
10
11
public bool SetMemberBirthday(int memberId, DateTime birthday)
{
Member member = _memberList.SingleOrDefault(m => m.Id == memberId);
if (member == null)
{
Logger.LogWarning($"SetMemberBirthday Error: Member {memberId} not found. Birthday not set {birthday}.");
return false; // false tells the caller that the operation failed.
}
member.Birthday = birthday;
return true;
}

通过以上的描述, 可以体会下面的结论:

  1. 异常用在unintended situation中. 什么样算是unintended, 这需要程序员自己界定. 这是一个难点.
  2. 如果是intended situation, 可以通过更改API的接口设计(返回值)来避免抛出异常.
  3. 像List的FindAll方法, 它的接口意图是明确的. 它的返回值没法更改. 这种不知道返回什么的情况下, 用
    throw exception是最合适的选择.

上面提到的是API的设计, 更上一层, 在类的设计层面去避免异常. 下面摘自Best practices for exceptions
https://docs.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions

Handle common conditions without throwing exceptions

For conditions that are likely to occur but might trigger an exception, consider handling them in a way that will avoid the exception. For example, if you try to close a connection that is already closed, you’ll get an InvalidOperationException. You can avoid that by using an if statement to check the connection state before trying to close it.

1
2
3
4
if (conn.State != ConnectionState.Closed)
{
conn.Close();
}

conn类提供了State属性, 用来判断; 如果没有改属性, 则需要捕获异常

1
2
3
4
5
6
7
8
9
try
{
conn.Close();
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.GetType().FullName);
Console.WriteLine(ex.Message);
}

The method to choose depends on how often you expect the event to occur.

  • Use exception handling if the event doesn’t occur very often, that is, if the event is truly exceptional and indicates an error (such as an unexpected end-of-file). When you use exception handling, less code is executed in normal conditions.

  • Check for error conditions in code if the event happens routinely and could be considered part of normal execution. When you check for common error conditions, less code is executed because you avoid exceptions.

Design classes so that exceptions can be avoided. A class can provide methods or properties that enable you to avoid making a call that would trigger an exception. For example, a FileStream class provides methods that help determine whether the end of the file has been reached. These can be used to avoid the exception that is thrown if you read past the end of the file.

swallow还是rethrow

对于下层调用抛出的异常, 是吞掉呢? 还是向上propagate. 这个主要看当前这一层, 有没有足够的程序上下文, 来进行恢复. 如果没有那样的能力, 只能rethrow exception. 下面这段文字摘自MSDN, 讲的非常明了.

https://docs.microsoft.com/en-us/cpp/cpp/how-to-design-for-exception-safety

Basic Techniques
A robust exception-handling policy requires careful thought and should be part of the design process. In general, most exceptions are detected and thrown at the lower layers of a software module, but typically these layers do not have enough context to handle the error or expose a message to end users. In the middle layers, functions can catch and rethrow an exception when they have to inspect the exception object, or they have additional useful information to provide for the upper layer that ultimately catches the exception. A function should catch and “swallow” an exception only if it is able to completely recover from it. In many cases, the correct behavior in the middle layers is to let an exception propagate up the call stack. Even at the highest layer, it might be appropriate to let an unhandled exception terminate a program if the exception leaves the program in a state in which its correctness cannot be guaranteed.
No matter how a function handles an exception, to help guarantee that it is “exception-safe,” it must be designed according to the following basic rules.