c#用异常实现基本错误处理

     调用方法时,错误处理是一个必须考虑的问题。尤其要懂得的是,如何将一个错误报告给调用者。
     本节探讨了如何利用异常处理(exception handling)机制来处理错误。

     利用异常处理,方法可以将有关错误的信息传给调用方法,同时不需要为此显式地提供任何参数。
     代码清单4-17略微修改了第1章的HeyYou程序。这一次,它不是请求用户输入姓氏,而是请求输入年龄。

     代码清单4-17 将一个string转换成int

     using System;class ExceptionHandling
     {
         static void Main() 
         {
            string firstName;
            string ageText;
            int age;    
            Console.WriteLine("Hey you!");     
            Console.Write("Enter your first name: ");
            firstName = System.Console.ReadLine();  
            Console.Write("Enter your age: ");
            ageText = Console.ReadLine();
            age = int.Parse(ageText);     
            Console.WriteLine("Hi {0}! You are {1} months old.", firstName, age*12);
         }
      } 

     输出4-11展示了代码清单4-17的结果。

         Hey you!Enter your first name: InigoEnter your age: 42Hi Inigo! You are 504 months old.
 

      System.Console.ReadLine()的返回值存储在一个名为ageText的变量中,然后传给int数据类型提供的一个Parse()方法。
     该方法获取代表数字的一个string值,然后把它转换为int类型。

     初学者主题:42作为字符串和整数

     C#是一种强类型语言。换言之,不仅数据值是紧要的,与数据关联的类型同样是紧要的。
     所以,值为42的一个字符串值和值为42的一个整数值是完全不同的。其中,字符串由4和2这两个字符构成,而int是数值42。

     最终,System.Console.WriteLine()语句以月份为单位打印年龄(age*12)。

     但是,用户完全有可能输入一个无效的整数。例如,假定用户输入“forty-two”,那么会发生什么呢?Parse()方法不能完成这样的一个转换。
     它希望用户输入只包含数字的一个字符串。假如Parse()方法接收到一个无效的值,它需要某种方式将这一事实反馈给调用者。

    捕捉错误(1)

         为了通知调用者参数是无效的,int.Parse()会引发异常(throw an exception)。引发异常会终止执行当前分支,
      并跳到调用栈中用于处理异常的第一个代码块。

         但是,由于当前尚未提供任何异常处理,所以程序会向用户报告遇到了一个未处理的异常(unhandled exception)。
      假定系统中没有注册任何调试器,错误信息就会出现在控制台上,如输出4-12所示。

     输出4-12

        Hey you!Enter your first name:
        InigoEnter your age: forty-twoUnhandled Exception:
        System.FormatException:
         Input string wasnot in a correct format.at System.Number.ParseInt32(String s, NumberStyles style,NumberFormatInfo info)
         at ExceptionHandling.Main() 

      显然,像这样的错误消息并不是特别有用。为了解决这个问题,有必要提供一个机制对错误进行恰当的处理,
      例如向用户报告一条更有意义的错误消息。

      这个过程称为捕捉异常(catching an exception)。
      代码清单4-18展示了具体的语法,输出4-13展示了结果。

      代码清单4-18 捕捉异常

     using System;
     class ExceptionHandling
     {
        static int Main() 
        {
          string firstName;
          string ageText;
          int age;int result = 0;     
          Console.Write("Enter your first name: ");
          firstName = Console.ReadLine();    
          Console.Write("Enter your age: ");
          ageText = Console.ReadLine();    
          try

         {
            age = int.Parse(ageText);Console.WriteLine("Hi {0}! You are {1} months old.",firstName, age*12);
         }
         catch (FormatException )
         {
            Console.WriteLine("The age entered, {0}, is not valid.",ageText);result = 1;
          }
         catch(Exception exception)
         {
           Console.WriteLine("Unexpected error: {0}", exception.Message);result = 1;
         }
         finally{
           Console.WriteLine("Goodbye {0}",firstName);}return result;}
           } 
输出4-13
Enter your first name: InigoEnter your age: forty-twoThe age entered, forty-two, is not valid.Goodbye Inigo  
 

     首先,要用一个try块将可能引发异常的代码(age = int.Parse())包围起来。这个块是从一个try关键字开始的。
    try关键字告诉编译器:开发者认为块中的代码有可能引发一个异常;如果真的引发了异常,那么某个catch块就要尝试处理这个异常。

     在一个try块之后,必须紧跟着一个或多个catch块(或一个finally块)。在catch块的标头(参见本章稍后的“高级主题:泛化catch”)中,
     可以选择指定异常的数据类型。只要数据类型与异常类型匹配,对应的catch块就会执行。但是,假如一直找不到合适的catch块,
     引发的异常就会变成一个未处理的异常,就好像没有进行异常处理一样。


捕捉错误(2)

      例如,假定用户输入的年龄是“forty-two”,那么int.Parse()会引发System.Format- Exception类型的一个异常,
      控制权会移交给后面的一系列catch块(System.FormatException表明字符串格式不正确,无法进行解析)。
    由于第一个catch块就与int.Parse()引发的异常类型匹配,所以会执行这个块中的代码。
    但假如try块中的语句引发的是一个不同类型的异常,那么执行的就是第二个catch块,因为几乎所有异常最终都属于System.Exception类型。

       如果没有System.FormatException catch块,那么即使int.Parse引发的是一个System. FormatException异常,
    也会执行System.Exception catch块。这是由于System.Format- Exception也属于System.Exception类型(
    换言之,System.FormatException是泛化异常类System.Exception的一个更具体的子类)。

     虽然catch块的数量可以随意,但处理异常的顺序千万不要随意。catch块必须按照从最具体到最不具体排列。
     System.Exception数据类型是最不具体的,所以它应该最后出现。System.Format- Exception排在第一,
     因为它是代码清单4-18所处理的最具体的异常。

      无论try块中的代码是否引发一个异常,finally块都会执行。finally块的作用是提供一个最终位置,
    在其中放入无论是否发生异常都要执行的代码。finally块最适合用来执行资源清理。事实上,完全可以只写一个try块和一个finally块,
    而不写任何catch块。无论try块是否引发异常,甚至无论是否写了一个catch块来处理异常,finally块都会执行。
    代码清单4-19演示了一个try/finally块,输出4-14展示了结果。

代码清单4-19 捕捉异常

using System;
  class ExceptionHandling
    {
        static int Main()
        {
           string firstName;string ageText;int age;int result = 0;    
           Console.Write("Enter your first name: ");firstName = Console.ReadLine();    
           Console.Write("Enter your age: ");ageText = Console.ReadLine();     
        try
        {
           age = int.Parse(ageText);
             Console.WriteLine("Hi {0}! You are {1} months old.",firstName, age*12);
          }
       finally
       {
           Console.WriteLine("Goodbye {0}",firstName);}return result;
        }
      }
    } 
输出4-14
Enter your first name: InigoEnter your age: forty-twoUnhandled Exception:
System.FormatException:
  Input string was not in a correct format.at System.Number.StringToNumber
(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
at ExceptionHandling.Main()Goodbye Inigo 

上述代码执行时会向用户显示一个未处理的异常,并执行finally块。

高级主题:Exception类继承

     所有异常都派生自System.Exception类。所以,它们都可以用catch(System.Exception exception)块进行处理。然而,
     一个更好的做法是编写专门的catch块来处理更具体的派生类型(比如System.FormatException),从而获取有关异常的具体信息,
    有的放矢地进行处理,并避免使用
大量条件逻辑来判断具体发生了什么错误。

     这正是C#规定catch块必须从“最具体”到“最不具体”排列的原因。例如,用于捕捉System. Exception的catch语句不能出现在捕捉
System.FormatException的catch语句之前,因为System.FormatException较System.Exception更加具体。
一个方法可以引发许多异常类型。表4-2总结了一些较为常见的类型。

表4-2 常见的异常类型

异常类型
 描述
 
System.Exception
 这是最泛化的异常,其他所有异常

类型都从它派生
 
System.ArgumentException
 传给方法的一个参数无效
 
System.ArgumentNullException
 一个不应该为null的参数为null
 
System.ApplicationException
 一个自定义的应用程序异常,开发者

可以用它标识特殊的、

非致命的应用程序错误
 
System.FormatException
 参数格式不符合调用的方法的参数规范
 
System.IndexOutOfRangeException
 试图访问一个不存在的数组元素
 
System.InvalidCastException
 因无效的类型转换或显式转换引发的异常
 
System.NotImplementedException
 虽然找到了对应的方法签名,但该方

法尚未完全实现
 
System.NullReferenceException
 试图访问尚未包含任何数据的一个变量
 
System.ArithmeticException
 发生了一个无效的数学运算,但其中不包括被零除
 
System.ArrayTypeMismatchException
 试图将类型有误的元素存储到数组中
 
System.StackOverflowException
 通常意味着一个无限循环,方法不停地回调自身(称为递归)
 


高级主题:泛化catch

可以指定一个不获取任何参数的catch块,如代码清单4-20所示。

代码清单4-20 常规catch块

try{age = int.Parse(ageText);System.Console.WriteLine("Hi {0}! You are {1} months old.",firstName, age*12); }catch (System.FormatException exception){System.Console.WriteLine("The age entered ,{0}, is not valid.",ageText);result = 1;}catch(System.Exception exception){System.Console.WriteLine("Unexpected error: {0}", exception.Message);result = 1;}catch { System.Console.WriteLine("Unexpected error!");result = 1;} finally{System.Console.WriteLine("Goodbye {0}",firstName);}...
 

     没有指定数据类型的catch块称为泛化catch块(generic catch block),它等价于指定获取object数据类型的catch块,例如catch(object exception){...}。由于所有类最终都是从object派生,所以没有数据类型的catch块必须出现在最后。

     泛化catch块很少使用,因为没有办法捕捉有关异常的任何信息。除此之外,C#不允许引发object类型的一个异常,只有使用C++这样的语言写的库才允许任意类型的异常。

C# 2.0中的行为稍微有别于之前版本的C#。在C# 2.0中,假如遇到用另一种语言写的代码,而且它会引发不是从System.Exception类派生的异常,那么该异常对象会被包装到一个System. Runtime.CompilerServices.RuntimeWrappedException中,后者是从System.Exception派生的。换言之,在C#程序集中,所有异常(无论它们是否从System.Exception派生)都会表现得和从System.Exception派生一样。

     结果就是,用于捕捉System.Exception的catch块会捕捉之前的块没有捕捉到的所有异常, 同时,System.Exception catch块之后的一个常规catch块永远得不到调用。所以,在C# 2.0中,假如在捕捉System.Exception的catch块之后添加了一个常规catch块,编译器就会报告一条警告消息,指出常规catch块永远都不会执行。