- C#從入門到精通(第2版)
- 龍馬高新教育策劃 國家863中部軟件孵化器編著
- 176字
- 2019-01-02 05:29:10
第7章 異常處理與程序調試
本章視頻教學錄像:27分鐘
C#程序的編寫和運行過程中,可能出現各種錯誤和意外情況,主要包括程序編寫過程中的語法錯誤、邏輯錯誤,以及程序運行過程中的異常。C#提供了程序調試和異常處理機制,以保證程序的正確性和可靠性。本章介紹異常處理和程序調試的基本機制。
本章要點(已掌握的在方框中打鉤)
□ C#錯誤類型
□ 異常處理概述
□ 異常處理過程
□ 程序調試
7.1 C#錯誤類型
本節視頻教學錄像:4分鐘
編寫C#程序的過程中不可避免地會出現各種各樣的錯誤,如標點符號缺失、關鍵字寫錯,程序邏輯錯誤、運行時錯誤等。為了能夠快速確定錯誤的原因,盡快排除程序錯誤,通常把程序錯誤劃分為3種類型:語法錯誤、運行時錯誤和邏輯錯誤。通過程序調試可以發現并糾正這些錯誤。
1. 語法錯誤
語法錯誤是指代碼不符合C#語言的語句。Visual Studio的編譯系統能查出此類錯誤并報告錯誤的原因,有語法錯誤的代碼不能通過編譯,改正后才能通過編譯。
如下圖所示,在Visual Studio中運行包含語法錯誤的程序,則系統會提示“發生生成錯誤”,并在編輯窗口下方的錯誤列表中列出錯誤的數量且為每個錯誤給出具體說明。雙擊錯誤列表中某個錯誤行,系統自動將插入點跳轉到代碼窗口中該錯誤的對應位置。對于語法錯誤,根據錯誤提示信息修改,然后重新編譯程序直到通過。

2. 運行時錯誤
運行時錯誤相對復雜一些,指的是在程序的運行過程中產生的錯誤,也就是異常。如數組下標越界、要打開的文件不存在等。
如下圖所示,在Visual Studio中運行程序,如果產生運行時錯誤,則系統提示出現異常,并高亮顯示引發異常的程序語句,方便程序設計者修改。

3. 邏輯錯誤
邏輯錯誤是指程序沒有實現編程人員的設計意圖和功能。有這類錯誤的程序可以運行,但是程序運行結果與預期不同。邏輯錯誤一般是因算法考慮不周引起的,也有些是因為編碼時疏忽,如將“+”寫成“*”等。
例如,下面的程序設計目標是求10的階乘,由于保存結果的變量s的初值被賦值為0,導致最終輸出結果為0,程序結果與設計目標不同,發生邏輯錯誤。
static void Main(string[] args)
{ int s=0; for (int i = 0; i <= 10; i++) s = s * i; Console.WriteLine(s); }
邏輯錯誤是Visual Studio無法識別的,要解決邏輯錯誤,則必須通過設置斷點,進行程序調試,跟蹤程序的運行過程,分析錯誤原因,然后修改程序代碼。
7.2 異常處理概述
本節視頻教學錄像:4分鐘
異常是程序執行時遇到的錯誤情況或意外行為,異常的出現通常是無法完全避免的。可能引發異常的原因有許多種,如在打開文件時文件不存在、網絡資源不可用、讀/寫磁盤出錯和內存申請失敗等都可能引發異常,導致程序中止。程序員應該盡可能對可能出現的異常進行控制和處理,以保證程序運行過程的穩定可靠,避免程序的非正常終止。
7.2.1 異常和異常處理
異常處理就是編程人員在程序編寫過程中對可能發生的錯誤和異常預先采取的處理措施。如可能存在異常,就要進行異常處理,以保證程序正常運行,適當的異常處理可以避免系統終止當前操作,否則程序可能會出現故障,系統也可能崩潰。例如,需要從鍵盤輸入一個數字,當輸入的是字符串時,則會由于輸入數據的類型不正確導致程序無法正常繼續運行。
static void Main(string[] args) { int x = int.Parse(Console.ReadLine()); … }
在以上代碼中,程序試圖從鍵盤輸入數字存入變量x,但如果程序運行過程中,用戶輸入了一個字符串,如“hello”,則Visual Studio 2013會提示程序運行中遇到異常,并給出異常類型和相應說明。這里給出的是“格式異常”(FormatException),同時給出造成異常的原因:輸入字符串的格式不正確。
7.2.2 異常類
異常通常是由應用程序(用戶程序等)或運行庫(公共語言運行庫和應用程序運行庫)引發的。.NET提供了大量與異常有關的類,用來處理異常,每一個異常類都表示一種異常。Exception類是所有異常的基類,當發生異常時,系統或當前正在執行的應用程序會通過引發包含關于該錯誤的信息的異常來報告異常。異常發生后,將由該應用程序或默認異常處理程序進行處理。
異常類繼承關系的結構如下圖所示。

有些異常在基本操作失敗時由 .NET Framework的公共語言運行庫(CLR)自動引發,如“零做除數時”就會引發DivideByZeroException異常。下表列出了常用的異常。

對于.NET類來說,一般的異常類System.Exception派生于System.Object,通常不在代碼中拋出這個System.Exception對象,因為它無法確定錯誤的具體情況。通常用的異常類都從Exception類繼承。其中有以下兩種主要類型的異常類,它們構成了幾乎所有的應用程序和運行庫異常的基礎。
⑴ ApplicationException:用戶定義的應用程序異常類型的基類。ApplicationException繼承Exception,但是不提供擴展功能,必須開發ApplicationException的派生類,以實現自定義異常的功能。
⑵ SystemException:系統異常類。CLR拋出的異常稱為系統異常。這些異常通常被看作是不可恢復的、致命的錯誤。系統異常直接從名為System.SystemException的基類中派生,該基類又從System.Exception中派生。
System.Exception中有許多屬性和方法,常用的如下表所示。

7. 3 異常處理過程
本節視頻教學錄像:9分鐘
7.3.1 try-catch語句
try-catch語句是C#提供的異常處理語句,語法如下。
try { 可能出現異常的語句序列; } catch(異常類型 異常對象) { 對可能出現的異常進行處理; }
提示
try塊中的語句是程序員希望程序實現的功能部分,但語句的執行過程中可能遇到異常。catch塊中包含的代碼處理各種異常,這些異常是try塊中的語句執行時遇到的。如果try塊中的代碼正常執行,則catch塊中的語句將不被執行。
【范例7-1】 異常處理例子,實現如字符串為空,拋出ArgumentNullException異常。
⑴ 啟動Visual Studio 2013,新建一個控制臺應用程序,項目名稱為“TryCatchExam”。
⑵ 在Program.cs中的Main方法中添加如下代碼。
01 try 02 {
03 int x=int.Parse(Console.ReadLine()); //輸入整型值 04 int y=10; 05 int z=y/x; 06 } 07 catch(Exception e) //捕獲異常,參數為異常類Exception的對象e 08 { 09 Console.WriteLine(e.Message); //輸出被捕獲的異常對象e的Message屬性值 10 } 11 Console.ReadKey();
輸入aaaa,程序執行結果如下圖所示。

輸入0,程序執行結果如下圖所示。

【范例分析】
使用try-catch語句進行異常處理后,Visual Studio 2013將不再提示異常,異常處理由catch塊的語句接管。捕獲異常的流程是:程序運行進入try塊,執行try塊中的語句,當程序流離開try塊后,如沒有異常發生,將執行try-catch之后的語句。如果在try塊中某個語句執行時遇到錯誤,程序流就跳轉到catch塊進行處理,執行catch塊中的語句,進行異常處理。
例7-1中的異常是由CLR拋出的系統異常,第一次執行輸入aaaa,try塊執行到第3行出現異常,跳轉到catch塊,輸出異常信息“輸入字符串的格式不正確”。第二次執行輸入0,try塊執行到第5行出現異常,跳轉到catch塊,輸出異常信息“嘗試除以零”。本例在try塊中遇到異常后由系統自動拋出異常,我們只負責異常處理。程序執行過程中可能出現不止一種異常,程序員需要盡可能考慮到所有情況并做好相應處理。
7.3.2 try-catch-finally語句
除了try-catch語句,C#異常處理語句還有try-catch-finally語句,語法如下。
try { 可能出現異常的語句序列; } catch(異常類型 異常對象) { 對可能出現的異常進行處理; }
finally { 最后要執行的代碼,進行必要的清理操作,以釋放資源 }
與try-catch語句相比,try-catch-finally語句多了一個finally塊,無論try塊的語句執行過程中是否發生異常,finally塊中的語句都將得到執行,finally塊的執行在try塊和catch塊之后。finally塊可以包含執行清理的代碼,例如,可以在finally塊中關閉在try塊中打開的連接或者打開的文件。
例如,在try塊中打開文件,由于發生異常導致文件未被正常關閉,則需要在finally塊中關閉文件。此外,在try-catch語句后邊的語句也可以放到finally塊內,例7-1可以改為:
01 try 02 { 03 //打開文件 04 int x=int.Parse(Console.ReadLine()); //輸入整型值 05 int y=10; 06 int z=y/x; 07 //關閉文件 08 } 09 catch(Exception e) //捕獲異常,參數為異常類Exception的對象e 10 { 11 Console.WriteLine(e.Message); //輸出被捕獲的異常對象e的Message屬性值 12 } 13 finallly 14 { 15 //關閉文件 16 Console.ReadKey(); 17 }
7.3.3 throw語句
在try-catch和try-catch-finally語句中的try塊中,除了由系統自動拋出異常外,也可以使用throw語句拋出異常;使用throw語句既可以引發系統異常,也可以引發自定義異常。throw使用格式如下。
throw異常對象;
例如,
throw new ArgumentNullException(); //拋出值不能為空的異常
throw new ArgumentNullException( ) 實例化了ArgumentNullException類的一個異常對象,并拋出。只要在try塊語句的執行過程中遇到一個throw語句,就會立即轉到與這個try塊對應的catch塊以進行異常處理。
【范例7-2】 異常處理例子,如果字符串為空,拋出ArgumentNullException異常。
⑴ 啟動Visual Studio 2013,新建一個控制臺應用程序,項目名稱為“ThrowExam”。
⑵ 在Program.cs中添加一個方法ProcessString,代碼如下。
01 static void ProcessString(string str) 02 { 03 if(str==null) //如果str參數為null,則拋出值不能為空的異常 04 { 05 throw new ArgumentNullException(); 06 } 07 }
⑶ 在Program.cs的Main方法中添加如下代碼。
01 Console.WriteLine("輸出結果為:"); //提示輸出結果 02 try //try塊用來放置可能有異常的代碼 03 { 04 string str1=null; //聲明一個字符串變量,賦值為空 05 ProcessString(str); //調用方法ProcessString() 06 } 07 catch(ArgumentNullException e) //捕獲異常并輸出異常信息 08 { 09 Console.WriteLine("這是異常:{0}",e.Message); 10 } 11 finally 12 { //finally塊放置最后要執行的代碼 13 Console.ReadKey(); 14 }
【范例分析】
比較例7-1和例7-2中catch塊的參數類型,例7-1中catch塊的參數e是System.Exception類的對象,Exception類是所有異常類的基類,因此例7-1中的catch塊可以接收所有類型的異常,輸出異常信息。例7-1中catch塊的參數e是System. ArgumentNullException類的對象,catch塊只能接收ArgumentNullException類的異常,如果try塊中拋出的是其他異常則不接收。如果需要接收其他類型的異常,那么需要定義一個新的參數為該類型異常的catch塊。由此,例7-1的try-catch語句可以改為如下。
try { … }
catch(FormatException e) { Console.WriteLine("這是FormatException,異常信息為:{0}", e.Message); } catch (DivideByZeroException e) { Console.WriteLine("這是DivideByZeroException,異常信息為:{0}", e.Message); }
在catch塊中可以使用throw語句再次引發已由catch語句捕獲的異常,這樣做的意義是,可以將try-catch語句中處理不了的異常再次使用throw語句拋出到更高一級進行處理。
【范例7-3】 多次throw例子。
⑴ 啟動Visual Studio 2013,新建一個控制臺應用程序,項目名稱為“ThrowExam2”。
⑵ 在Program.cs中添加方法FuncA和FuncB,代碼如下。
01 static void FuncA() 02 { 03 throw new ArgumentException("This is exception in FuncA"); 04 } 05 static void FuncB() 06 { 07 try 08 { 09 FuncA(); 10 } 11 catch(Exception e) 12 { 13 throw; //再次拋出捕獲的異常 14 } 15 }
⑶ 在Program.cs的Main方法中添加如下代碼。
01 try 02 { 03 FuncB(); 04 } 05 catch(Exception e) 06 { 07 Console.WriteLine(e.Message); //輸出異常消息 08 } 09 Console.ReadKey();
程序執行結果如下圖所示。

【范例分析】
Main方法調用FuncB方法,FuncB方法調用FuncA方法。FuncA方法拋出異常,FuncB方法中的try-catch語句的catch塊接收異常后再次拋出,Main方法接收FuncB中的catch塊再次拋出的異常。這種情況通常發生在FuncB的try-catch語句接收到異常后,發現該異常無法處理或者該異常情況需要通知更高一級(即FuncB方法的調用者),再次將異常拋出給本方法的調用者,這里是Main方法去處理,當然,FuncB中的catch塊也可以根據對異常的處理情況選擇將收到的異常不加改變地再次拋出,或者修改異常的相關數據后再次拋出,甚至生成一個新的異常對象然后拋出,一切都取決于程序員設計程序的需要。此外,可以嘗試一下,將FuncB中catch塊的throw語句注釋掉,看看Main方法是否還會接收到異常。
7.3.4 自定義異常類
如果系統提供的異常類不能夠與程序中的異常相匹配,就需要自定義異常類。自定義異常類的語法如下。
class自定義異常類名:異常基類名 { //語句塊 }
提示
一般從System.Exception類或其他常見異常類派生自定義異常類。異常類名稱通常以Exception結尾,如NewException、MyException等。
一般要在自定義異常類中定義3個構造函數,一個是默認構造函數,一個用來設置消息屬性,一個用來設置Message屬性和InnerException屬性,這三個構造函數可以從異常基類繼承。自定義異常類時,也可以添加新的屬性,但僅當新屬性提供的數據有助于解決異常時,才將其添加到異常類。
【范例7-4】 自定義一個新的異常類TestException。
⑴ 在Program.cs中添加自定義異常類CustomException代碼,代碼如下。
01 public class TestException:Exception 02 { 03 public TestException():base() //繼承基類的無參構造函數 04 { } 05 public TestException(string msg):base(msg) //繼承基類有一個參數的構造函數 06 { }
07 public TestException(string msg,Exception inner):base(msg,inner) 08 { } //繼承基類的有兩個參數的構造函數 09 }
⑵ 在Program.cs的Main方法中添加以下代碼。
01 Console.WriteLine("請輸入整數"); //提示輸入整數 02 try 03 { 04 int x=int.Parse(Console.ReadLine()); 05 if((x/2)*2==x) 06 throw new TestException("輸入的是偶數!"); 07 else 08 throw new TestException("輸入的是奇數!"); 09 } 10 catch(TestException e) 11 { 12 Console.WriteLine(e.Message); 13 } 14 Console.ReadKey();
程序執行結果如下圖所示。

【范例分析】
自定義異常類比較簡單,自定義異常類的使用方法與C#提供的異常類相同。本例在步驟⑴中定義了一個異常類,在步驟⑵中使用了try…catch結構來捕獲自定義異常并處理。
7.4 程序調試
本節視頻教學錄像:7分鐘
程序調試的主要目的是解決程序中的邏輯錯誤,通過設置斷點,跟蹤觀察程序的執行過程,發現造成邏輯錯誤的具體語句,然后修改程序實現設計目標。
7.4.1 設置斷點
程序的執行過程是連貫的,為了跟蹤觀察程序的運行狀態,我們需要控制程序的運行過程,使得程序能夠暫停在某些特定的位置,這種控制可以通過設置斷點來實現。斷點是程序暫停執行的地方,當程序運行到斷點位置時,程序暫停執行,進入中斷模式,程序設計者可以觀察程序的運行狀態,如某些變量的值,對程序進行分析。
在Visual Studio中打開需要調試的程序,在代碼編輯窗口可以添加、刪除斷點。要在程序的某個語句所在位置添加或者刪除斷點,有以下幾種方式。
單擊代碼窗口最左邊灰色區域對應語句行的位置添加斷點,再次單擊刪除斷點,左側的紅色圓點即為斷點。

單擊語句所在行,然后按F9鍵添加斷點,再次按【F9】鍵刪除斷點。
單擊語句所在行,然后單擊鼠標右鍵,在彈出的快捷菜單中選擇【斷點】【插入斷點】命令。
7.4.2 啟動、繼續和停止調試
設置好斷點以后,即可啟動程序調試,要啟動調試,可以使用以下方法。
● 選擇菜單【調試】【啟動調試】或者按【F5】鍵啟動調試。
● 單擊工具欄上的啟動按鈕 啟動調試。
啟動調試后,程序會暫停在第一個斷點位置,觀察完第一個斷點位置的程序運行狀態后,需要讓程序繼續調試,操作步驟與啟動調試相同,只是菜單和工具欄上的文字都變成“繼續”,這時程序繼續執行并在下一個斷點處暫停。
啟動調試后,可以選擇菜單【調試】【停止調試】或者單擊工具欄上的停止調試按鈕
來停止程序調試。
7.4.3 單步調試
啟動調試后,可以在菜單中選擇【調試】【逐語句】或者【調試】
【逐過程】進行逐語句或逐過程調試。
逐語句調試相當于為程序中的每個語句都加上一個斷點,每次執行一條語句,也可以按【F11】鍵進行逐語句調試,遇到某一語句調用了其他函數,對該語句執行逐語句調試,則程序會暫停在該函數的第一行語句。
逐過程調試也可以通過按【F10】鍵實現,遇到某一語句調用了其他函數,對該語句執行逐過程調試,則程序會暫停在該語句的下一條語句,不再轉入函數內部。
程序執行到被調用函數內部,需要停止逐語句調試時,可以選擇【調試】【跳出】,程序將返回到調用函數。
7.4.4 調試監控
在程序調試過程中,通常需要監測程序的運行狀態,Visual Studio提供了局部變量窗口、監視窗口、自動窗口和快速監視窗口等對程序的運行狀態進行監控。
1. 局部變量窗口
選擇菜單【調試】【窗口】
【局部變量】,即可打開局部變量窗口。局部變量窗口允許查看在局部過程中聲明的變量的當前值,但不能修改。

2. 自動窗口
選擇菜單【調試】【窗口】
【自動窗口】,即可打開自動窗口。自動窗口與局部變量窗口類似,但自動窗口顯示當前語句和先前語句中使用的變量。此外,用戶不能添加或刪除自動窗口顯示的項目,但可以修改某項的值。
3. 監視窗口
選擇菜單【調試】【窗口】
【監視】,有【監視1】、【監視2】、【監視3】和【監視4】4個監視窗口可用。可以從程序中選中變量或者表達式后拖動到監視窗口,用于判斷程序的運行是否有錯。
4. 快速監視窗口
選擇菜單【調試】 快速監視】即可打開快速監視對話框。快速監視窗口為查看和計算變量與表達式提供了一個快捷的途徑。其中,【重新計算】可以計算表達式的最新值,【添加監視】可以將表達式加入到監視窗口中。需要注意的是,必須關閉快速監視窗口才能繼續進行程序調試。

7.5 高手點撥
本節視頻教學錄像:2分鐘
7.5.1 使用多catch塊處理異常
try-catch語句中的catch塊可以捕獲異常,被捕獲的異常可以是所有異常或者程序員需要捕獲的某類異常。實際上,一個try-catch語句中可以包含多個catch塊,每個catch塊用來捕獲一類異常。出現異常時,將執行第一個能夠處理該異常的catch塊,而忽略其他catch塊,即使其他catch指定的異常類型能夠兼容要被處理的異常。
例如,
try { } catch(異常類型1)
{ } catch(異常類型2) { } … catch(異常類型n) { }
由于異常類型之間存在繼承和派生關系,有可能某個異常能夠被多個異常類型兼容。因此,catch塊的排列次序需要從具體到通用,即把處理具體類型異常的catch塊放在前邊,而把兼容多種異常類型的catch塊放在后邊。
7.5.2 引發異常時要注意的問題
我們已經知道,異常機制的引入是為了處理程序執行時遇到的錯誤情況或意外行為。引發異常時需要注意以下問題。
(1)優先考慮使用System命名空間中提供的現有異常類型,除非特別需要,即某種錯誤或意外情況需要的處理方式與現有任何異常類型的處理方式都不相同,才需要自定義異常類型。
(2)盡可能引發最具體的異常類型。例如,如果某方法收到一個null參數,則該方法應引發System. ArgumentNullException,而不是引發該異常的基類型System.ArgumentException。也就是說,如果可能,盡可能引發子異常類型而非父異常類型,因為由父異常類型繼承派生而來的子異常類型更加具體,更能準確描述錯誤和意外的情況。使用最具體的異常類型方便異常處理過程中對異常情況的判斷和處理。
7.6 實戰練習
一、思考題
1. 有幾種類型的錯誤?
2. 如何實現自定義異常?
二、操作題
定義一個異常類DivZero,假如除數為零,則顯示“除數不能為0”的異常信息。