跳轉到內容

C# 程式設計/異常

來自華夏公益教科書,開放的書籍,開放的世界

軟體程式設計師編寫程式碼以執行一些期望的動作。但是每個軟體都可能由於內部或外部原因而無法執行其期望的動作。C# 語言中的異常處理系統允許程式設計師以結構化的方式處理錯誤或異常情況,允許程式設計師將程式碼的正常流程與錯誤處理邏輯分開。

異常可以表示軟體執行期間發生的各種異常情況。這些條件可能是內部或外部造成的。執行失敗的外部條件包括,例如,與遠端元件連線的網路故障,使用檔案/系統資源的許可權不足,記憶體不足異常或 Web 服務丟擲的異常等。這些主要是由於我們的應用程式所依賴的環境元件丟擲的錯誤,例如作業系統、.NET 執行時或外部應用程式或元件。內部故障可能是由於軟體缺陷、設計的功能故障(根據業務規則所需的故障)、傳播的外部故障等,例如執行時系統檢測到的空物件引用、使用者輸入的無效輸入字串並由應用程式程式碼檢測到,或者使用者要求提取超出賬戶餘額的金額(業務規則)。

檢測錯誤條件的程式碼被稱為丟擲異常,而處理錯誤的程式碼被稱為捕獲異常。C# 中的異常是一個物件,它封裝了有關發生錯誤的各種資訊,例如異常時的堆疊跟蹤和描述性錯誤訊息。所有異常物件都是System.Exception類的例項,或者它的子類。.NET Framework 中定義了許多用於不同目的的異常類。程式設計師還可以定義自己的類,繼承自System.Exception或 .NET Framework 中其他合適的異常類。

微軟在 2.0 版本之前推薦開發者異常類應該繼承自ApplicationException異常類。2.0 版本釋出後,該建議已過時,使用者異常類現在應繼承自Exception[1]

有三個程式碼定義用於異常處理。這些是

  • try/catch - 做一些事情並捕獲錯誤(如果發生)。
  • try/catch/finally - 做一些事情,如果發生錯誤,則捕獲錯誤,但始終執行finally
  • try/finally - 做一些事情,但始終執行finally。任何發生的異常將在finally之後丟擲。

異常按從最具體到最不具體的順序捕獲。例如,如果您嘗試訪問一個不存在的檔案,CLR 會按以下順序查詢異常處理程式

  • FileNotFoundException
  • IOExceptionFileNotFoundException的基類)
  • SystemExceptionIOException的基類)
  • ExceptionSystemException的基類)

如果丟擲的異常不是從要捕獲的異常列表派生或不在該列表中,則它將被向上丟擲呼叫堆疊。

以下是一些不同型別異常的示例

try/catch

[編輯 | 編輯原始碼]

try/catch執行一個操作,如果發生錯誤,則將控制權轉移到 catch 塊,如果有有效的 catch 部分。

class ExceptionTest
{
     public static void Main(string[] args)
     {
          try
          {
               Console.WriteLine(args[0]);
               Console.WriteLine(args[1]);
               Console.WriteLine(args[2]);
               Console.WriteLine(args[3]);
               Console.WriteLine(args[4]);
          }
          catch (ArgumentOutOfRangeException e)
          {
               Console.WriteLine(e.Message);
          }
     }
}

以下是一個具有多個 catch 的示例

class ExceptionTest
{
     public static void Main(string[] args)
     {
          try
          {
               string fileContents = new StreamReader(@"C:\log.txt").ReadToEnd();
          }
          catch (UnauthorizedAccessException e) // Access problems
          {
               Console.WriteLine(e.Message);
          }
          catch (FileNotFoundException e)       // File does not exist
          {
               Console.WriteLine(e.Message);
          }
          catch (IOException e)                // Some other IO problem.
          {
               Console.WriteLine(e.Message);
          }
     }
}

在所有catch語句中,您可以省略異常型別和異常變數名

try
{
    int number = 1/0;
}
catch (DivideByZeroException)
{
    // DivideByZeroException
}
catch
{
    // some other exception
}

try/catch/finally

[編輯 | 編輯原始碼]

捕獲問題是一個好主意,但有時會導致您的程式處於無效狀態。例如,如果您開啟與資料庫的連線,發生錯誤,並且您丟擲異常。您將在哪裡關閉連線?在 try 和 exception 塊中?嗯,在執行關閉操作之前可能會出現問題。

因此,finally語句允許您處理“在所有情況下執行此操作”的情況。請參見下面的示例

using System;
class ExceptionTest
{
     public static void Main(string[] args)
     {
          SqlConnection sqlConn = null;

          try
          {
              sqlConn = new SqlConnection ( /*Connection here*/ );
              sqlConn.Open();
 
              // Various DB things
        
              // Notice you do not need to explicitly close the connection, as .Dispose() does this for you.
          }
          catch (SqlException e)
          {
               Console.WriteLine(e.Message);
          }
          finally
          {
               if (sqlConn != null && sqlConn.State != ConnectionState.Closed)
               {
                   sqlConn.Dispose();
               }
          }
     }
}

第二個示例

using System;
public class excepation
{
	public double num1, num2,result;
			
	public void add()
	{
		try
		{
			Console.WriteLine("enter your number");
			num1 = Convert.ToInt32(Console.ReadLine());
			num2 = Convert.ToInt32(Console.ReadLine());
			result = num1/num2;
		}
		catch(DivideByZeroException  e) //FormatException
		{
			Console.WriteLine("{0}",e.Message);
		}
		catch(FormatException ex)
		{
			Console.WriteLine("{0}",ex.Message);
		}
		finally
		{
			Console.WriteLine("turn over");
		}
	}
	public void display()
	{
		Console.WriteLine("The Result is: {0}",result);
	}
	public static void Main()
	{
		excepation ex = new excepation();
		ex.add();
		ex.display();
	}	
}

請注意,SqlConnection 物件是在try/catch/finally之外宣告的。原因是finally無法看到在try/catch中宣告的任何內容。透過在前面的作用域中宣告它,finally塊能夠訪問它。

try/finally

[編輯 | 編輯原始碼]

try/finally 塊允許你執行與上面相同操作,但區別在於丟擲的錯誤會由 catch 塊(如果可能)處理,然後向上拋到呼叫棧。

class ExceptionTest
{
     public static void Main(string[] args)
     {
          SqlConnection sqlConn = null;

          try
          {
              SqlConnection sqlConn = new SqlConnection ( /*Connection here*/ );
              sqlConn.Open();
 
              // Various DB bits
          }
          finally
          {
               if (sqlConn != null && sqlConn.State != ConnectionState.Closed)
               {
                   sqlConn.Dispose();
               }
          }
     }
}

重新丟擲異常

[編輯 | 編輯原始碼]

有時候,由於以下兩個原因,最好將錯誤向上拋到呼叫棧。

  1. 這不是你預期的發生的事情。
  2. 你在異常中添加了額外的資訊,以幫助診斷。


如何丟擲異常

[編輯 | 編輯原始碼]

一些開發者會這樣編寫空的 try/catch 語句

try
{
      // Do something
}
catch (Exception ex)
{
      // Ignore this here
}

不推薦這種方法。你正在吞掉錯誤並繼續執行。如果這個異常是 OutOfMemoryExceptionNullReferenceException,繼續執行是不明智的。因此,你應該始終捕獲你期望發生的異常,並將其他異常丟擲。

以下是另一個捕獲異常的錯誤示例

/* Read the config file, and return the integer value. If it does not exist, then this is a problem! */

try
{
     string value = ConfigurationManager.AppSettings["Timeout"];

     if (value == null)
         throw new ConfigurationErrorsException("Timeout value is not in the configuration file.");
}
catch (Exception ex)
{
     // Do nothing!
}

如你所見,ConfigurationErrorsException 將被 catch (Exception) 塊捕獲,但它被完全忽略了!這是糟糕的程式設計習慣,因為你正在忽略錯誤。

以下也是不好的做法

try
{
   ..
}
catch (Exception ex)
{
     throw ex;
}

CLR 現在會認為 throw ex; 語句是問題的根源,而實際上問題出在 try 部分。因此,永遠不要以這種方式重新丟擲。

如何捕獲異常

[編輯 | 編輯原始碼]

更好的方法是

/* Read the config file, and return the integer value. If it does not exist, then this is a problem! */

try
{
     string value = ConfigurationManager.AppSettings["Timeout"];

     if (value == null)
         throw new ConfigurationErrorsException("Timeout value is not in the configuration file.");
}
catch (Exception ex )
{
     throw; // <-- Throw the existing problem!
}

throw; 關鍵字表示保留異常資訊並將其向上拋到呼叫棧。

異常中的額外資訊

[編輯 | 編輯原始碼]

另一種方法是在異常中提供額外資訊(可能是區域性變數資訊)。在這種情況下,你將異常封裝在另一個異常中。你通常使用盡可能具體地描述問題的異常,或者建立你自己的異常,如果你找不到足夠具體的異常(或者如果你想要包含額外的資訊)。

public OrderItem LoadItem(string itemNumber)
{
    DataTable dt = null;

    try
    {
         if (itemNumber == null)
              throw new ArgumentNullException("Item Number cannot be null","itemNumber");

         DataTable dt = DataAccess.OrderItem.Load(itemNumber);
  
         if (dt.Rows == 0)
              return null;
         else if (dt.Rows > 1)
              throw new DuplicateDataException( "Multiple items map to this item.",itemNumber, dt);

         OrderItem item = OrderItem.CreateInstanceFromDataRow(dt.Rows[0]);

         if (item == null)
              throw new ErrorLoadingException("Error loading Item " + itemNumber, itemNumber, dt.Rows[0]);
    }
    catch (DuplicateDataException dde)
    {
         throw new ErrorLoadingException("OrderItem.LoadItem failed with Item " + 
                                                            itemNumber, dde); // <-- Include dde (as the InnerException) parameter
    }
    catch (Exception ex)
    {
         throw; // <-- We aren't expecting any other problems, so throw them if they occur.
    }
}

參考資料

[編輯 | 編輯原始碼]
  1. [ApplicationException 已過時]
華夏公益教科書