建议59:不要在不恰当的场合下引发异常
常见的不易于引发异常的情况是对在可控范围内的输入和输出引发异常。
private void SaveUser3(User user) { if (user.Age < 0) { throw new ArgumentOutOfRangeException("Age不能为负数。"); } // 保存用户 }
此方法起码有两个地方欠妥:
1)判读Age不能为负数。这是一个正常的业务逻辑,它不应该被处理为一个异常。
2)应采用Tester-Doer来验证输入。
应该添加一个Tester方法:
private bool CheckAge(int age, ref string msg) { if (age < 0) { msg = "Age不能为负数。"; return false; } else if (age > 100) { msg = "Age不能>100。"; return false; } return true; }
调用代码:
string msg = string.Empty; if (CheckAge(30, ref msg)) { SaveUser(user); }
要掌握两条原则:
- 正常业务流程不应该使用异常处理。
- 不要总是尝试去捕获异常,而应该允许异常向调用堆栈上传播。
那么,到底在怎样的情况下引发异常呢?
情况一
如果运行代码后会造成内存泄露、资源不可用,或者应用程序状态不可恢复,则引发异常。
我们来看似乎能马上推翻上例所得结论的一个场景。在微软提供的Console类中有很多类似这样的代码:
if ((value < 1) || (value > 100)) { throw new ArgumentOutOfRangeException("value", value, Environment.GetResourceString("ArgumentOutOfRange_CursorSize")); }
或者:
if (value == null) { throw new ArgumentNullException("value"); }
但是,要注意,本文提到的是:对在可控范围内的输入输出不引发异常。区别就在于“可控”这两个字。所谓“可控”,可定义为:发生异常后,系统资源仍然可用,或资源状态可恢复。
Console这个类虽然也提供了Tester-Doer模式,让调用者可以有更多的方法来验证输入。但是永远不要保证调用者对你的类有足够的了解,他有可能调用你的任何公开方法,而不会考虑先后顺序;所以应该为这类方法引发一些必要的异常。但是,如果你自己写了一个Student业务类,判断年龄,年龄小于0这样的判断,就不应该引发异常,因为那是一个正常控制流。
情况二
在捕获异常的时候,如果需要包装一些更有用的信息,则引发异常。
这类异常的引发在UI层特别有用。系统引发的异常所带的Message往往更倾向于技术性的描述,而在UI层,异常的用户很可能是最终用户。如果我们需要将异常的Message信息呈现给最终用户,更好的做法是包装异常,然后引发一个含有友好信息的新异常。
情况三
如果底层异常在高层操作的上下文中没有意义,则可以考虑捕获这些底层异常,并引发新的有意义的异常。如将一个InvalidCastException引发为新的ArgumentException。
正确引发异常的一个典型的例子就是捕获底层API错误代码,并抛出。查看Console这个类,还会发现很多地方有类似的代码:
int errorCode = Marshal.GetLastWin32Error(); if (errorCode == 6) { throw new InvalidOperationException(Environment.GetResourceString("Invalid Operation_ConsoleKeyAvailableOnFile")); }
Console为我们封装了调用windows API返回的错误代码,而让代码引发一个新的异常。
显然,需要调用Windows API或第三方API提供的接口时,如果对方的异常报告机制使用的是错误代码,最好重新引发该接口提供的错误,因为你需要让自己的团队更好的理解这些错误。
转自:《编写高质量代码改善C#程序的157个建议》陆敏技