Home > Article > Backend Development > Best Practices for Exception Handling in .NET (Translated)
Original address: Click to open the link
##This article is translated from an article on CodeProject Article, original address.
Table of Contents
Introduction
"My software program never fails." Do you believe it? I'm almost certain everyone will yell that I'm a liar. "It is almost impossible for a software program to be bug-free!"In fact, it is not impossible to develop a trustworthy and sound software program. Note that I am not referring to the software used to control nuclear power plants, but to some common commercial software. These software may run on servers or PCs. They can work continuously for weeks or even months. A major problem occurs. As you can guess, what I just meant is that the software has a relatively low error rate, you can quickly find the cause of the error and fix it quickly, and the errors will not cause major data damage.
In other words, what I mean is that the software is relatively stable.
It is understandable that there are bugs in software. But if it's a recurring bug and you can't fix it quickly because there's not enough information, that's unforgivable.
In order to better understand what I said above, let me give you an example: I often see countless commercial software giving such error prompts when encountering insufficient hard disk:
"Failed to update customer information, please contact the system administrator and try again."
No other information is recorded except these. It is a very time-consuming process to figure out what caused this error. Programmers may need to make various guesses before actually finding the cause of the problem.
Note that in this article, I mainly talk about how to better handle exceptions in .NET programming. I do not intend to discuss how to display appropriate "error prompt information" because I think this job belongs to the UI interface. developers, and much of it depends on the type of UI and who will end up using the software. For example, the "error message" of a text editor for ordinary users should be completely different from that of a Socket communication framework, because the latter's direct users are programmers. Be prepared for the worst Follow some basics Design principles can make your program more robust and improve the user experience when errors occur. The "improving user experience" I'm talking about here doesn't mean that the error prompt form can make users happy, but it means that errors that occur will not damage the original data or crash the entire computer. If your program encounters an insufficient hard disk error, but the program does not cause any other negative effects (it only prompts an error message and does not cause other problems, translator's note), then the user experience is improved. Check in advance Strong type checking and validation is A powerful way to avoid bugs. The sooner you detect a problem, the sooner you can fix it. It is not easy and quite annoying to figure out after a few months "Why does the ProductID column in the InvoiceItems table have a CustomerID data?" If you use a class instead of basic types (such as int, string) to store customer data, the compiler will not allow what just happened (referring to confusing CustomerID and ProductID, translator's note) to happen. Don’t trust external data External data is not Reliable, our software programs must be rigorously checked before using them. Whether these external data come from the registry, database, hard disk, socket, or files you write with your keyboard, all these external data must be strictly checked before use. Many times, I see programs that completely trust the configuration file because the programmers who develop these programs always assume that no one will edit the configuration file and corrupt it. Trusted devices: camera, mouse and keyboard When you need to use external data, you may encounter the following situations: 1) Insufficient security permissions 2) The data does not exist 3) The data is incomplete 4) The data is complete, but the format is wrong The above situation may occur regardless of whether the data source is a key in the registry, a file, a socket, a database, a Web service, or a serial port. All external data is subject to failure. "Write operation" may also fail Not possible A trusted data source is also an untrusted data warehouse. When you store data, similar situations may still occur: 1) Insufficient security permissions 2) The device does not exist 3) There is not enough space 4) A physical error occurred in the storage device This This is why some compression software creates a temporary file while working and then renames it when the work is completed, instead of directly modifying the source file. The reason is that if the hard disk is damaged (or the software is abnormal), the original data may be lost. (The translator has encountered this situation. There was a power outage when backing up data, and the original old backup was damaged. Translator’s note) Safe Programming A friend of mine told me: A good programmer never writes bad code in his program. I think this is a necessary but not sufficient condition for being a good programmer. Below I've compiled some "bad code" you might write when you do exception handling: Don't throw "new Exception() ” Please don’t do this. Exception is a very abstract exception class. Catching this type of exception usually has many negative effects. Normally we should define our own exception classes and distinguish between exceptions thrown by the system (framework) and exceptions thrown by ourselves. Do not store important exception information in the Message property Exceptions are encapsulated in classes. When you need to return exception information, please store the information in some separate properties (not in the Message property), otherwise it will be difficult for people to parse the information they need from the Message property. For example, when you just need to correct a spelling error, if you write the error message and other prompt content in the Message property in the form of String, how can others simply get the error message they want? It's hard to imagine how much effort they have to do. Each thread should contain a try/catch block ## Generally exception handling is placed in a relatively centralized place in the program. Each thread needs to have a try/catch block, otherwise you will miss some exceptions and cause difficult to understand problems. When a program starts multiple threads to handle background tasks, you usually create a type to store the results of each thread's execution. At this time, please don't forget to add a field to the type to store the exceptions that may occur in each thread. Otherwise, the main thread will not know the exceptions of other threads. In some "fire and forget" situations (meaning that the main thread no longer cares about the running status of the thread after starting the thread, translator's note), you may need to copy the exception handling logic in the main thread to your child thread. go. Record the exception after catching it No matter you It doesn't matter which method your program uses to record logs - log4net, EIF, Event Log, TraceListeners, or text files. The important thing is: when you encounter an exception, you should log it somewhere. But please only log once, otherwise you will end up with a very large log file containing a lot of repeated information. Don’t just record the value of Exception.Message, you also need to record Exception.ToString() When we talk about logging, don’t forget that we should log the value of Exception.ToString(), not Exception.Message. Because Exception.ToString() contains "stack trace" information, internal exception information and Message. Usually this information is very important, and if you only log Exception.Message, you may only see a prompt like "Object reference does not point to an instance in the heap". To catch specific exceptions If you want To catch exceptions, please try to catch specific exceptions (not Exceptions) as much as possible. I often see beginners say that a good piece of code is code that cannot throw exceptions. In fact, this statement is wrong. Good code should throw corresponding exceptions when necessary, and good code can only catch exceptions that it knows how to handle (note this sentence, translator's note). The following code serves as an explanation of this rule. I bet the guy who wrote the code below would kill me if he saw it, but it's a piece of code taken from a real programming job. The first class MyClass is in one assembly, and the second class GenericLibrary is in another assembly. It runs normally on the development machine, but on the test machine it always throws an exception of "Invalid data!" even though the data entered every time is legal. Can you tell me why? public class MyClass
{
public static string ValidateNumber(string userInput)
{
try
{
int val = GenericLibrary.ConvertToInt(userInput);
return "Valid number";
}
catch (Exception)
{
return "Invalid number";
}
}
}
public class GenericLibrary
{
public static int ConvertToInt(string userInput)
{
return Convert.ToInt32(userInput);
}
}
The reason for this problem is that exception handling is not very specific. According to the introduction on MSDN, the Convert.ToInt32 method only throws three exceptions: ArgumentException, FormatException and OverflowException. So, we should only handle these three exceptions.
问题发生在我们程序安装的步骤上,我们没有将第二个程序集(GenericLibrary.dll)打包进去。所以程序运行后,ConvertToInt方法会抛出FileNotFoundException异常,但是我们捕获的异常是Exception,所以会提示“数据不合法”。
不要中止异常上抛
最坏的情况是,你编写catch(Exception)这样的代码,并且在catch块中啥也不干。请不要这样做。
清理代码要放在finally块中
大多数时候,我们只处理某一些特定的异常,其它异常不负责处理。那么我们的代码中就应该多一些finally块(就算发生了不处理的异常,也可以在finally块中做一些事情,译者注),比如清理资源的代码、关闭流或者回复状态等。请把这当作习惯。
有一件大家容易忽略的事情是:怎样让我们的try/catch块同时具备易读性和健壮性。举个例子,假设你需要从一个临时文件中读取数据并且返回一个字符串。无论什么情况发生,我们都得删除这个临时文件,因为它是临时性的。
让我们先看看最简单的不使用try/catch块的代码:
string ReadTempFile(string FileName) { string fileContents; using (StreamReader sr = new StreamReader(FileName)) { fileContents = sr.ReadToEnd(); } File.Delete(FileName); return fileContents; }
这段代码有一个问题,ReadToEnd方法有可能抛出异常,那么临时文件就无法删除了。所以有些人修改代码为:
string ReadTempFile(string FileName) { try { string fileContents; using (StreamReader sr = new StreamReader(FileName)) { fileContents = sr.ReadToEnd(); } File.Delete(FileName); return fileContents; } catch (Exception) { File.Delete(FileName); throw; } }
这段代码变得复杂一些,并且它包含了重复性的代码。
那么现在让我们看看更简介更健壮的使用try/finally的方式:
string ReadTempFile(string FileName) { try { string fileContents; using (StreamReader sr = new StreamReader(FileName)) { fileContents = sr.ReadToEnd(); } File.Delete(FileName); return fileContents; } catch (Exception) { File.Delete(FileName); throw; } }
变量fileContents去哪里了?它不再需要了,因为返回点在清理代码前面。这是让代码在方法返回后才执行的好处:你可以清理那些返回语句需要用到的资源(方法返回时需要用到的资源,所以资源只能在方法返回后才能释放,译者注)。
不要忘记使用using
仅仅调用对象的Dispose()方法是不够的。即使异常发生时,using关键字也能够防止资源泄漏。(关于对象的Dispose()方法的用法,可以关注我的书,有一章专门介绍。译者注)
不要使用特殊返回值去表示方法中发生的异常
因为这样做有很多问题:
1)直接抛出异常更快,因为使用特殊的返回值表示异常时,我们每次调用完方法时,都需要去检查返回结果,并且这至少要多占用一个寄存器。降低代码运行速度。
2)特殊返回值能,并且很可能被忽略
3)特殊返回值不能包含堆栈跟踪(stack trace)信息,不能返回异常的详细信息
4)很多时候,不存在一个特殊值去表示方法中发生的异常,比如,除数为零的情况:
public int pide(int x, int y) { return x / y; }
不要使用“抛出异常”的方式去表示资源不存在
微软建议在某些特定场合,方法可以通过返回一些特定值来表示方法在执行过程中发生了预计之外的事情。我知道我上面提到的规则恰恰跟这条建议相反,我也不喜欢这样搞。但是一些API确实使用了某些特殊返回值来表示方法中的异常,并且工作得很好,所以我还是觉得你们可以谨慎地遵循这条建议。
我看到了.NET Framework中很多获取资源的API方法使用了特殊返回值,比如Assembly.GetManifestStream方法,当找不到资源时(异常),它会返回null(不会抛出异常)。
不要将“抛出异常”作为函数执行结果的一种
这是一个非常糟糕的设计。代码中包含太多的try/catch块会使代码难以理解,恰当的设计完全可以满足一个方法返回各种不同的执行结果(绝不可能到了必须使用抛出异常的方式才能说明方法执行结果的地步,译者注),如果你确实需要通过抛出异常来表示方法的执行结果,那只能说明你这个方法做了太多事情,必须进行拆分。(这里原文的意思是,除非确实有异常发生,否则一个方法不应该仅仅是为了说明执行结果而抛出异常,也就是说,不能无病呻呤,译者注)
可以使用“抛出异常”的方式去着重说明不能被忽略的错误
我可以举个现实中的例子。我为我的Grivo(我的一个产品)开发了一个用来登录的API(Login),如果用户登录失败,或者用户并没有调用Login方法,那么他们调用其他方法时都会失败。我在设计Login方法的时候这样做的:如果用户登录失败,它会抛出一个异常,而并不是简单的返回false。正因为这样,调用者(用户)才不会忽略(他还没登录)这个事实。
不要清空了堆栈跟踪(stack trace)信息
堆栈跟踪信息是异常发生时最重要的信息,我们经常需要在catch块中处理一些异常,有时候还需要重新上抛异常(re-throw)。下面来看看两种方法(一种错误的一种正确的):
错误的做法:
try { // Some code that throws an exception } catch (Exception ex) { // some code that handles the exception throw ex; }
为什么错了?因为当我们检查堆栈跟踪信息时,异常错误源变成了“thorw ex;”,这隐藏了真正异常抛出的位置。试一下下面这种做法:
try { // Some code that throws an exception } catch (Exception ex) { // some code that handles the exception throw; }
有什么变化没?我们使用“throw;”代替了“throw ex;”,后者会清空原来的堆栈跟踪信息。如果我们在抛出异常时没有指定具体的异常(简单的throw),那么它会默认地将原来捕获的异常继续上抛。这样的话,上层代码捕获的异常还是最开始我们通过catch捕获的同一个异常。
拓展阅读:
C# 异常处理(Catch Throw)IL分析
异常类应标记为Serializable
很多时候,我们的异常需要能被序列化。当我们派生一个新的异常类型时,请不要忘了给它加上Serializable属性。谁会知道我们的异常类会不会用在Remoting Call或者Web Services中呢?
使用”抛出异常”代替Debug.Assert
当我们发布程序后,不要忘了Debug.Assert将会被忽略。我们在代码中做一些检查或者验证工作时,最好使用抛出异常的方式代替输出Debug信息。
将输出Debug信息这种方式用到单元测试或者那些只需要测试当软件真正发布后确保不会出错的场合。
每个异常类至少包含三个构造方法
做这件事相当简单(直接从其他的类型粘贴拷贝相同的代码即可),如果你不这样做,那么别人在使用你编写的异常类型时,很难遵守上面给出的一些规则的。
我指的哪些构造方法呢?这三个构造方法可以参见这里。
不要重复造轮子
已经有很多在异常处理方面做得比较好的框架或库,微软提供的有两个:
Exception Management Application Block
Microsoft Enterprise Instrumentation Framework
Note that these libraries may not be of much use to you if you don't follow some of the rules I mentioned above.
VB.NET
If you have read the entire After reading this article, you will find that all the sample code is written in C#. That's because C# is my preferred .NET language, and VB.NET has some special rules of its own.
Simulating the using statement in C
#Unfortunately Yes, there is no using statement in VB.NET. Every time you release an object's unmanaged resources, you have to do this:
If you don't follow If you call the DISpose method in the above way, errors are likely to occur (For the call of the Dispose method, please pay attention to 新书. Translator's Note).
Do not use unstructured exception handling (On Error Goto)
Unstructured exception handling is also called "On Error Goto". Djikstra said in 1974 that "goto statements are harmful rather than helpful". This was already 30 years ago! Please remove all goto statements from your code, I assure you, they do no harm. (Edsger Dykstra proposed the "goto harmful theory", semaphores and PV primitives, which solved the interesting dining philosophers problem. When talking about the Fortran language in the book "Software Story" Been there. Translator's Note)
#Summary
##I hope this article can enable some people to improve their coding quality. I also hope that this article is the beginning of a discussion on how to handle exceptions effectively and make the programs we write more robust.
Translator’s words:
I have a shortcoming, I don’t know if there are any netizens like me. I am a slow-burner, and the same goes for technology. I only started to feel something after the peak of popularity of many things. The main reason is that I am not very fond of new things; the second reason is that I always feel that I change the things I have learned before I have mastered them, which means giving up halfway. In fact, I also know that this is very bad. After all, the IT industry is a rapidly developing industry, and it will fall behind as soon as it fails to keep up.
##It is precisely when I encounter such contradictory situations that when I learn knowledge, I focus on learning the communication between technologies. Nature, the so-called universality, is something that is unlikely to change or decline in ten, twenty or even thirty years. If the company you are currently working in has been using a certain set of frameworks in the actual development process, if you insist on If you don't focus on "how to use this framework to make a good system", you may fall behind in a few years. And if you study the commonalities in programming, such as protocols, interaction principles between systems, etc., these are used in every network communication system, whether it is a seemingly outdated PC program, a Web program, or a currently popular Mobile APP will be used, and the basic principles are the same. The more I watch it, the more I realize that new things come out that seem to be changing the soup without changing the medicine (slightly exaggerated:-))
Therefore, I give it to those who are like me and don’t really follow. The advice for people who are new to new things, or those who have been engaged in a certain type of fixed development work for a long time, is to find out the commonalities between technologies, and don’t stay on the surface of technology unless you are interested enough in new things and have sufficient energy.
The above words were also shared during our company’s seminar.
## Author: Zhou Jianzhi
Source: http://www.php.cn/
Replenish:
Regarding the CLR’s “two-round traversal” exception handling strategy.
When an application has a multi-layer nested exception catching structure, if an exception occurs at the lowest layer (in fact, the same is true in the middle layer), the CLR will first search for the catch statement block at the layer where the exception is raised. Is there any "compatible" code for this type of exception? If not, "jump" to the previous layer to search. If there is no "compatible" code for the previous layer, continue to search for the "previous layer" of the previous layer. From this to the top level of the application.
This is the "first round" traversal of the CLR's application of nested exception catching structures - finding the appropriate exception handler.
If an exception handler is found at a certain layer, note that the CLR will not execute it immediately. Instead, it will return to the "accident scene" and perform the "second round" traversal again to execute the finally of all "intermediate" levels. statement block, then execute
to find the exception handler, and finally, traverse from this layer to the top, executing all finally statement blocks.