深入讨论C#语言的异常机制、使用规范

先引用下异常在功能上的标准说明:

异常是在程序执行期间出现的问题。
C# 中的异常是对程序运行时出现的特殊情况的一种响应,比如尝试除以零。
异常提供了一种把程序控制权从某个部分转移到另一个部分的方式。
C# 异常处理时建立在四个关键词之上的:try、catch、finally 和 throw。

try:一个 try 块标识了一个将被激活的特定的异常的代码块。后跟一个或多个 catch 块。
catch:程序通过异常处理程序捕获异常。catch 关键字表示异常的捕获。
finally:finally 块用于执行给定的语句,不管异常是否被抛出都会执行。
       例如,如果您打开一个文件,不管是否出现异常文件都要被关闭。
throw:当问题出现时,程序抛出一个异常。使用 throw 关键字来完成。



BCL定义了许多类,每一个类代表一个指定的异常类型,当一个异常发生时,CLR:


  • 创建该类型的异常对象

  • 寻找适当的catch子句以处理 它

所有异常类都从根本上派生自system.exception类,异常继承层次如下

image.png


异常类类型包括:基类:System.Exception;

系统级异常:System.SystemException;

应用程序级异常:System.ApplicationException。


(1).由System.SystemException派生的异常类型:

System.AccessViolationException     在试图读写受保护内存时引发的异常。

System.ArgumentException         在向方法提供的其中一个参数无效时引发的异常。

System.Collections.Generic.KeyNotFoundException     指定用于访问集合中元素的键与集合中的任何键都不匹配时所引发的异常。

System.IndexOutOfRangeException     访问数组时,因元素索引超出数组边界而引发的异常。

System.InvalidCastException     因无效类型转换或显示转换引发的异常。

System.InvalidOperationException     当方法调用对于对象的当前状态无效时引发的异常。

System.InvalidProgramException     当程序包含无效Microsoft中间语言(MSIL)或元数据时引发的异常,这通常表示生成程序的编译器中有bug。

System.IO.IOException     发生I/O错误时引发的异常。

System.NotImplementedException     在无法实现请求的方法或操作时引发的异常。

System.NullReferenceException     尝试对空对象引用进行操作时引发的异常。

System.OutOfMemoryException     没有足够的内存继续执行程序时引发的异常。

System.StackOverflowException     挂起的方法调用过多而导致执行堆栈溢出时引发的异常。


(2).由System.ArgumentException派生的异常类型:

System.ArgumentNullException     当将空引用传递给不接受它作为有效参数的方法时引发的异常。

System.ArgumentOutOfRangeException     当参数值超出调用的方法所定义的允许取值范围时引发的异常。


(3).由System.ArithmeticException派生的异常类型:

System.DivideByZeroException     试图用零除整数值或十进制数值时引发的异常。

System.NotFiniteNumberException     当浮点值为正无穷大、负无穷大或非数字(NaN)时引发的异常。

System.OverflowException     在选中的上下文中所进行的算数运算、类型转换或转换操作导致溢出时引发的异常。


(4).由System.IOException派生的异常类型:

System.IO.DirectoryNotFoundException     当找不到文件或目录的一部分时所引发的异常。

System.IO.DriveNotFoundException     当尝试访问的驱动器或共享不可用时引发的异常。

System.IO.EndOfStreamException     读操作试图超出流的末尾时引发的异常。

System.IO.FileLoadException     当找到托管程序却不能加载它时引发的异常。

System.IO.FileNotFoundException     试图访问磁盘上不存在的文件失败时引发的异常。

System.IO.PathTooLongException     当路径名或文件名超过系统定义的最大长度时引发的异常。


(5).其他常用异常类型:

ArrayTypeMismatchException     试图在数组中存储错误类型的对象。

BadImageFormatException     图形的格式错误。

DivideByZeroException     除零异常。

DllNotFoundException     找不到引用的dll。

FormatException     参数格式错误。

MethodAccessException     试图访问私有或者受保护的方法。

MissingMemberException     访问一个无效版本的dll。

NotSupportedException     调用的方法在类中没有实现。

PlatformNotSupportedException     平台不支持某个特定属性时抛出该错误。




创建用户自定义异常

自定义的异常类派生自ApplicationException类

    class Program
    {
        static void Main(string[] args)
        {
            //用户自定义的异常类是派生自ApplicationException类
            Temperture temp = new Temperture();
            try
            {
                temp.showTemp();
            }
            catch(TempIsZeroException e)
            {
                Console.WriteLine("TempIsZeroException:{0}", e.Message);
            }
            Console.ReadKey();

        }
    }
   //创建自定义异常
    public class TempIsZeroException:ApplicationException
    { 
        public TempIsZeroException(string message):base(message)
        {

        }
    }
    public class Temperture
    {
        int temperature = 0;
        public void showTemp()
        {
            if(temperature==0)
            {
                throw (new TempIsZeroException("Zero Temperature found"));
            }
            else
            {
                Console.WriteLine("Temperature:{0}", temperature);
            }
        }
    }


搜索调用栈的示例

    class Program
    {
        static void Main(string[] args)
        {
            Myclass MCLs = new Myclass();
            try
            {
                MCLs.A();
            }
            catch(DivideByZeroException e)
            {
                Console.WriteLine("catch clause in Main()");
            }
            finally
            {
                Console.WriteLine("finally clause in Main()");
            }
            Console.WriteLine("After try statement in Main");
            Console.WriteLine("----------------------keep runing");
            Console.ReadKey();
        }
    }
    class Myclass
    {
        public void A()
        {
            try
            {
                B();
            }
            catch(System.NullReferenceException)
            {
                Console.WriteLine("catch clause in A()");
            }
            finally
            {
                Console.WriteLine("finally clause in A()");
            }
        }
        void B()
        {
            int x = 10, y = 0;
            try
            {
                x /= y;
            }
            catch(System.IndexOutOfRangeException)
            {
                Console.WriteLine("catch clause in B()");
            }
            finally
            {
                Console.WriteLine("finally clasue in B()");
            }
        }
    }

  • Main调用A,调用B,B遇到一个DivideByZeroExceprion异常。

  • 系统检查B的catch段寻找匹配的catch子句。虽然它有一个Index0ut0fRangeException的子句,但没有DivideByZerException的。

  • 系统然后延着调用栈向下移动并检查A的catch段,在那里它发现A也没有匹配的atch子句

  • 系统继续延调用栈向下,并检查Main的catch子句部分,在那里它发现Main确实有一个DivideByZeroException的catch子句。

  • 尽管匹配的catch子句现在被定位了,但并不执行。相反,系统回到栈的顶端,执行B的finally子句,并把B从调用栈中弹出。

  • 系统移动到A,执行它的fina]1y子句,并把A从调用栈中弹出。

  • 最后,Main的匹配catch子句被执行,接着是它的fina11y子句。然后执行在Main的try语句结尾之后继续。


image.png

异常抛出

可以使用throw语句显式引发一个异常,语法如下:

    class Program
    {
        static void Main(string[] args)
        {
            //带对象的异常抛出
            string s = null;
            Myclass.PrintArg(s);
            Myclass.PrintArg("hello world");
            Myclass01.PrintArg(s);
            Console.ReadKey();

        }
    }
    class Myclass
    {
        public static void PrintArg(string arg)
        {
            try
            {
                if(arg==null)
                {
                    ArgumentNullException myEx = new ArgumentNullException("arg");
                    throw myEx;
                }
            }
            catch (ArgumentNullException e)
            {
                Console.WriteLine("Message:{0}", e.Message);
            }
        }
    }
    class Myclass01
    {
        public static void PrintArg(string arg)
        {
            try
            {
                try
                {
                    if(arg == null)
                    {
                        ArgumentNullException myEx = new ArgumentNullException("arg");
                        throw myEx;
                    }
                    Console.WriteLine(arg);
                }
                catch(ArgumentNullException e)
                {
                    Console.WriteLine("Inner catch:{0}", e.Message);
                    throw;//重新抛出异常,没有附加参数
                }
            }
            catch
            {
                Console.WriteLine("Outer catch:Handling an Exception.");
            }
        }
    }




(一)与返回值相比,异常俱有的优势:


1)返回值有多种用途,报告错误只是其中之一;但使用异常报告错误则增强了API的一致性。应该始终用异常来传递错误,绝对不要将异常用于其它用途。

2)返回值报告错误时,错误处理离发生错误的代码很近。但是使用异常时,开发人员有更多的选择。

3)使用异常来报告错误,错误处理代码可以集中放在try代码的后面。

4)错误码容易被忽略,而异常无法忽略,从而使代码更稳固

5)使用返回值报告错误时,如果调用失败,程序会在结果不正确的情况下继续执行。

     使用异常时,一旦发生错误,线程就被暂停,调用代码有机会处理异常。如果调用栈上没有地方处理异常,应用程序即被终止。

    操作失败后如果让程序继续运行,可能有潜在的安全问题,这样做不还如让程序直接崩溃,绝对不要让程序自己吞下异常继续运行。

   应用程序应该只处理自己能够理解的异常。



二)抛出异常


1)关于异常最大的误解是:异常是用于”异常情况“的。事实上,异常是用来传递错误情况的。

2)不使用异常的借口大致归纳为两类:一是不喜欢异常处理语法;二是因为抛出异常的性能不如返回错误码。

3)异常应该是一个框架来报靠错误的主要方法。

4)要通过抛异常的方式报告操作失败

5)如果代码遇到某种严重问题无法继续安全的执行时,应该通过System.Environment.FailFast来终止进程,而不要抛出异常

6)不要在正常的控制流中使用异常,如果能够避免的话。

     例如使用 Teser-Doer或者Try-Parse模式以避免引发异常。

7)每秒抛出100个异常可能会严重影响程序性能

8)不要让公有成员根据某个选项来决定是否抛出异常,例如:

     public Type GetType(string name, bool throwOnError)

     不要把异常做为公有成员的返回值或者输出参数:

     public Exception DoSometing(){ ...}

    


除了文件读写和数据库操作,还有许多其他的C#资源需要释放或管理。以下是一些常见的资源类型及其释放方法:

  1. 网络资源
    如果你使用了诸如Socket、HttpClient或其他网络相关的类,确保在完成操作后关闭它们。这些类通常实现了IDisposable接口,所以可以使用using语句或确保在合适的地方调用Dispose方法。

  2. 图形和图像资源
    如果你使用了GDI+或其他图形库来创建图像或绘制图形,确保在完成后释放相关的资源。例如,使用Graphics对象后,应调用其Dispose方法。

  3. 定时器和线程
    如果你创建了线程或使用了定时器,确保在完成后正确地停止和释放它们。线程可以通过调用Thread.AbortThread.Join来停止,定时器可以通过设置其Enabled属性为false来停止。

  4. 数据库连接
    除了文件读写,数据库连接也是需要释放的重要资源。确保在完成数据库操作后关闭和释放数据库连接。

  5. 锁和同步原语
    如果使用了锁(如lock语句)或其他的同步原语(如MonitorMutex等),确保在完成后释放这些锁。

  6. COM对象
    如果你的程序中使用了COM对象,确保在不再需要这些对象时调用其Release方法来释放它们。

  7. 内存和其他非托管资源
    对于由C#代码分配的内存或其他非托管资源,通常不需要手动释放,因为垃圾回收器(GC)会负责回收它们。但要确保不再使用这些资源,以避免悬挂引用。

  8. 事件订阅
    如果订阅了事件,确保在不再需要时取消订阅,以避免潜在的内存泄露和不必要的处理。

为了有效地管理资源,推荐使用“资源获取即初始化”(RAII)模式,该模式在C#中通过实现IDisposable接口来实现。在C# 8.0及更高版本中,还可以使用using declarations来自动调用资源的释放方法。此外,利用C#的垃圾回收机制和弱引用(WeakReference)类来管理某些资源的生命周期也是重要的实践。




首先,异常处理应该是系统设计规约的一部分出现在系统设计文档中,而不仅仅是一种技术实现。

作为设计文档的一部分,异常处理应该着眼于系统容错性和稳定性(正如楼主提到的那样)。然后在根据这个规约,再来具体讨论和选择异常处理中使用的各种技术细则。 比如,在设计服务时,必须在服务的调用接口处有异常处理,否则客户端传过来的任何有害数据都可能让服务器挂掉。

比如,对异常的处理在系统的设计中,必须有明确说明,不能随便在哪个模块中处理异常。

软件中有bug是可以理解的。但是如果是经常出现的bug,并且因为没有足够的提示信息导致你不能迅速修复它,那么这种情况是不可被原谅的。

为了更好地理解我上面所说的话,我举个例子:我经常看见无数的商业软件在遇到硬盘不足时给出这样的错误提示:

“更新客户资料失败,请与系统管理员联系然后重试”。

除了这些外,其他任何信息都没有被记录。要搞清楚到底什么原因引起的这个错误是一件非常耗时的过程,在真正找到问题原因之前,程序员可能需要做各种各样的猜测。

下面就来说说C#异常处理18条最佳实践

1. 不要抛出“new Exception()”

请别这样做。Exception是一个非常抽象的异常类,捕获这类异常通常会产生很多负面影响。通常情况下应该定义我们自己的异常类,并且需要区分系统(framework)抛出的异常和我们自己抛出的异常。

2. 不要将重要的异常信息存储在Message属性中

异常都封装在类中。当你需要返回异常信息时,请将信息存储在一些单独的属性中(而不要放在Message属性中),否则人们很难从Message属 性中解析出他们需要的信息。比如当你仅仅需要纠正一下拼写错误,如果你将错误信息和其它提示内容一起以String的形式写在了Message属性中,那 么别人该怎样简单地获取他们要的错误信息呢?你很难想象到他们要做多少努力。

3. 每个线程要包含一个try/catch块

一般异常处理都放在了程序中一个比较集中的地方。每个线程都需要有一个try/catch块,否则你会漏掉某些异常从而出现难以理解的问题。当一个 程序开启了多个线程去处理后台任务时,通常你会创建一个类型来存储各个线程执行的结果。这时候请不要忘记了为类型增加一个字段来存储每个线程可能发生的异 常,否则的话,主线程不会知道其他线程的异常情况。在一些“即发即忘”的场合(意思主线程开启线程后不再关心线程的运行情况,译者注),你可能需要将主线 程中的异常处理逻辑复制一份到你的子线程中去。

4. 捕获异常后要记录下来

不管你的程序是使用何种方式记录日志——log4net、EIF、Event Log、TraceListeners或者文本文件等,这些都不重要。重要的是:当你遇到异常后,应该在某个地方将它记录在日志中。但是请仅仅记录一次, 否则的话,你最后会得到一个非常大的日志文件,包含了许多重复信息。

5. 不要只记录Exception.Message的值,还需要记录Exception.ToString()

当我们谈到记录日志时,不要忘了我们应该记录Exception.ToString()的值,而不是Exception.Message。因为 Exception.ToString()包含了“堆栈跟踪”(stack trace)信息,内部异常信息以及Message。通常这些信息非常重要,而如果你只记录Exception.Message的话,你只可能看到类似 “对象引用未指向堆中实例”这样的提示。

6. 要捕获具体的异常

如果你要捕获异常,请尽可能的捕获具体异常(而非Exception)。

我经常看见初学者说,一段好的代码就是不能抛出异常的代码。其实这说法是错误的,好的代码在必要时应该抛出相应的异常,并且好的代码只能捕获它知道该怎么处理的异常(注意这句话,译者注)。

下面的代码作为对这条规则的说明。我敢打赌编写下面这段代码的那个家伙看见了会杀了我的,但是它确实是摘取自真实编程工作中的一段代码。

第一个类MyClass在一个程序集中,第二个类GenericLibrary在另一个程序集中。在开发的机器上运行正常,但是在测试机器上却总是抛出“数据不合法!”的异常,尽管每次输入的数据都是合法的。

你们能说说这是为什么吗?

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);
    }
}

这个问题的原因就是异常处理不太具体。根据MSDN上的介绍,Convert.ToInt32方法仅仅会抛出ArgumentException、FormatException以及OverflowException三个异常。所以,我们应该仅仅处理这三个异常。

问题发生在我们程序安装的步骤上,我们没有将第二个程序集(GenericLibrary.dll)打包进去。所以程序运行 后,ConvertToInt方法会抛出FileNotFoundException异常,但是我们捕获的异常是Exception,所以会提示“数据不 合法”。

7. 不要中止异常上抛

最坏的情况是,你编写catch(Exception)这样的代码,并且在catch块中啥也不干。请不要这样做。

8. 清理代码要放在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
    {
        using (StreamReader sr = new StreamReader(FileName))
        {
            return sr.ReadToEnd();
        }
    }
    finally
    {
        File.Delete(FileName);
    }
}

变量fileContents去哪里了?它不再需要了,因为返回点在清理代码前面。这是让代码在方法返回后才执行的好处:你可以清理那些返回语句需要用到的资源(方法返回时需要用到的资源,所以资源只能在方法返回后才能释放,译者注)。

9. 不要忘记使用using

仅仅调用对象的Dispose()方法是不够的。即使异常发生时,using关键字也能够防止资源泄漏。

10.不要使用特殊返回值去表示方法中发生的异常

因为这样做有很多问题:

1)直接抛出异常更快,因为使用特殊的返回值表示异常时,我们每次调用完方法时,都需要去检查返回结果,并且这至少要多占用一个寄存器。降低代码运行速度。

2)特殊返回值能,并且很可能被忽略

3)特殊返回值不能包含堆栈跟踪(stack trace)信息,不能返回异常的详细信息

4)很多时候,不存在一个特殊值去表示方法中发生的异常,比如,除数为零的情况:

public int divide(int x, int y)
{
    return x / y;
}

11. 不要使用“抛出异常”的方式去表示资源不存在

微软建议在某些特定场合,方法可以通过返回一些特定值来表示方法在执行过程中发生了预计之外的事情。我知道我上面提到的规则恰恰跟这条建议相反,我 也不喜欢这样搞。但是一些API确实使用了某些特殊返回值来表示方法中的异常,并且工作得很好,所以我还是觉得你们可以谨慎地遵循这条建议。

我看到了.NET Framework中很多获取资源的API方法使用了特殊返回值,比如Assembly.GetManifestStream方法,当找不到资源时(异常),它会返回null(不会抛出异常)。

12. 不要将“抛出异常”作为函数执行结果的一种

这是一个非常糟糕的设计。代码中包含太多的try/catch块会使代码难以理解,恰当的设计完全可以满足一个方法返回各种不同的执行结果(绝不可 能到了必须使用抛出异常的方式才能说明方法执行结果的地步,译者注),如果你确实需要通过抛出异常来表示方法的执行结果,那只能说明你这个方法做了太多事 情,必须进行拆分。

13. 可以使用“抛出异常”的方式去着重说明不能被忽略的错误

我可以举个现实中的例子。我为我的Grivo(我的一个产品)开发了一个用来登录的API(Login),如果用户登录失败,或者用户并没有调用 Login方法,那么他们调用其他方法时都会失败。我在设计Login方法的时候这样做的:如果用户登录失败,它会抛出一个异常,而并不是简单的返回 false。正因为这样,调用者(用户)才不会忽略(他还没登录)这个事实。

14.不要清空了堆栈跟踪(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捕获的同一个异常。

15.异常类应标记为Serializable

很多时候,我们的异常需要能被序列化。当我们派生一个新的异常类型时,请不要忘了给它加上Serializable属性。谁会知道我们的异常类会不会用在Remoting Call或者Web Services中呢?

16.使用”抛出异常”代替Debug.Assert

当我们发布程序后,不要忘了Debug.Assert将会被忽略。我们在代码中做一些检查或者验证工作时,最好使用抛出异常的方式代替输出Debug信息。

将输出Debug信息这种方式用到单元测试或者那些只需要测试当软件真正发布后确保不会出错的场合。

17.不要重复造轮子

已经有很多在异常处理方面做得比较好的框架或库,微软提供的有两个:

Exception Management Application Block

Microsoft Enterprise Instrumentation Framework

注意,如果你不遵守我上面提到的一些规则,这些库对你来讲可能没什么用。

18.不要信任外部数据

外部数据是不可靠的,我们的软件程序在使用它们之前必须严格检查。无论这些外部数据来自于注册表、数据库、硬盘、socket还是你用键盘编写的文 件,所有这些外部数据在使用前必须严格进行检查。很多时候,我看到一些程序完全信任配置文件,因为开发这些程序的程序员总是认为没有人会编辑配置文件并损 坏它。

强类型检查和验证是避免bug发生的有力方法。你越早发现问题,就越早修复问题。几个月后再想搞清楚“为什么InvoiceItems表中的 ProductID栏会存在一个CustomerID数据?”是一件不太容易并且相当恼火的事情。如果你使用一个类代替基本类型(如int、 string)去存储客户(Customer)的数据的话,编译器就不会允许刚才那件事情(指将CustomerID和ProductID混淆)


本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:
本帖最后由 勇哥,很想停止 于 2024-01-09 08:01:17 编辑

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2024年5月    »
12345
6789101112
13141516171819
20212223242526
2728293031
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864