Chương 7 Quản lý lỗ
7.1. Ném và bắt biệt lệ
Trong C# chúng ta có thể ném bất kỳ một đối tượng nào thuộc lớp hay lớp con của lớp System.Exception (viết tắt là Exception). Vùng tên System khai báo sẵn nhiều lớp biệt lệ hữu ích chẳng hạn như ArgumentNullException, InValidCastException, OverflowException…
7.1.1. Lệnh ném throw
Để báo hiệu một tình huống bất thường trong một lớp C#, ta ném ra một biệt lệ
Exception và sau đó ném nó ra. throw new System.Exception();
Ném một biệt lệ sẽ làm chương trình tạm dừng lập tức và CLR tìm kiếm một trình quản lý biệt lệ. Nếu hàm ném không có trình giải quyết biệt lệ, stack sẽ được duyệt ngược (unwind) bằng cách pop ra cho đến khi gặp được trình giải quyết biệt lệ. Nếu vẫn không tìm thấy cho đến tận hàm Main(), chương trình sẽ bị dừng lại.
Ví dụ 7-1. Ném một biệt lệ
using System;
namespace Programming_CSharp {
public class Test {
public static void Main( ) {
Console.WriteLine("Enter Main..."); Test t = new Test( );
t.Func1( );
Console.WriteLine("Exit Main..."); }
public void Func1( ) {
Console.WriteLine("Enter Func1..."); Func2( );
Console.WriteLine("Exit Func1..."); }
public void Func2( ) {
Console.WriteLine("Enter Func2...");
throw new System.Exception( );
Console.WriteLine("Exit Func2..."); } } } Kết quả: Enter Main... Enter Func1... Enter Func2...
Exception occurred: System.Exception: An exception of type System.Exception was thrown.
at Programming_CSharp.Test.Func2( ) in ...exceptions01.cs:line 26 at Programming_CSharp.Test.Func1( ) in ...exceptions01.cs:line 20 at Programming_CSharp.Test.Main( ) in ...exceptions01.cs:line 12
Ví dụ trên in thông báo ra màn hình console khi bắt đầu và kết thúc mỗi hàm. Hàm Main() tạo một đối tượng kiểu Test và gọi hàm Func1(). Sau khi in thông báo Enter Func1, hàm Func1() gọi hàm Func2(). Func2() in ra câu thông báo bắt đầu và ném ra một biệt lệ. Chương trình sẽ tạm ngưng thực thi và CLR tìm trình giải quyết biệt lệ trong hàm Func2(). Không có, vùng nhớ stack được unwind cho đến hàm Func1(). Vẫn không có, vùng nhớ stack tiếp tục được unwind cho đến hàm Main(). Vẫn không có, trình giải quyết biệt lệ mặc định được gọi. Thông báo lỗi hiển thị
trên màn hình.
7.1.2. Lệnh bắt catch
Trình giải quyết biệt lệ đặt trong khối lệnh catch, bắt đầu bằng từ khóa catch. Trong ví dụ 11-2, lệnh ném throw được đặt trong khối lệnh try, lệnh bắt đặt trong khối catch.
Ví dụ 7-2.Bắt một biệt lệ.
using System;
namespace Programming_CSharp {
public class Test {
public static void Main( ) {
Console.WriteLine("Enter Main..."); Test t = new Test( );
t.Func1( );
Console.WriteLine("Exit Main..."); }
public void Func1( ) {
Console.WriteLine("Enter Func1..."); Func2( );
Console.WriteLine("Exit Func1..."); }
public void Func2( ) {
Console.WriteLine("Enter Func2...");
try {
Console.WriteLine("Entering try block..."); throw new System.Exception( );
Console.WriteLine("Exiting try block..."); }
catch {
Console.WriteLine("Exception caught and handled."); } Console.WriteLine("Exit Func2..."); } } } Kết quả: Enter Main... Enter Func1... Enter Func2... Entering try block...
Exception caught and handled. Exit Func2...
Exit Func1... Exit Main...
Ví dụ này y hệt như ví dụ 11-1 ngoại trừ chương trình được đặt trong khối lệnh try/catch. Ta đặt các đoạn mã dễ gây lỗi trong khối lệnh try, chẳng hạn như đoạn mã truy cập tập tin, xin cấp phát vùng nhớ…
Theo sau khối lệnh try là khối lệnh catch. Khối lệnh catch trong ví dụ là khối lệnh catch chung vì ta không thể đoán trước được loại biệt lệ nào sẽ phát sinh. Nếu biết chính xác loại biệt lệ nào phát sinh, ta sẽ viết khối lệnh catch cho loại biệt lệ đó (sẽđề cập ở phần sau).
7.1.2.1. Sửa chữa lỗi lầm
Trong ví dụ 11-2, lệnh bắt catch chỉ đơn giản thông báo rằng một biệt lệ đã
được bắt và quản lý. Trong ứng dụng thực tế, chúng ta sẽ viết các đoạn mã giải quyết lỗi ởđây. Ví dụ nếu người dùng cố mở một tập chỉ đọc, ta hẳn cho gọi một phương thức cho phép người dùng thay đổi thuộc tính tập tin. Nếu trường hợp hết bộ nhớ, ta hẳn cho người dùng cơ hội đóng các ứng dụng khác. Nếu tất cả đều thất bại, khối lệnh catch sẽ cho in các thông báo mô tả chi tiết lỗi để người dùng biết rõ vấn đề.
7.1.2.2. Duyệt lại (unwind) vùng nhớ stack
Nếu xem kết quả ví dụ 11-2 cẩn thận, ta sẽ thấy các thông báo bắt đầu hàm Main(), Func1(), Func2() và khối lệnh try; tuy nhiên lại không thấy thông báo kết thúc khối try mặc dù nó đã thoát khỏi hàm Func2(), Func1() và hàm Main().
Khi một biệt lệ xảy ra, khối try ngừng thực thi ngay lập tức và quyền được trao cho khối lệnh catch. Nó sẽ không bao giờ quay trở lại khối try và vì thế không thể
in dòng lệnh thoát khối try. Sau khi hoàn tất khối lệnh catch, các dòng lệnh sau khối catch được thực thi tiếp tục.
Không có khối catch, vùng nhớ stack được duyệt ngược, nhưng nếu có khối catch việc này sẽ không xảy ra. Biệt lệ đã được giải quyết, không còn lỗi nữa, chương trình tiếp tục thực thi. Điều này sẽ rõ ràng hơn nếu đặt try/catch trong hàm Func1() như trong ví dụ 11-3.
Ví dụ 7-3. Bắt biệt lệ trong hàm gọi.
using System;
namespace Programming_CSharp {
public class Test {
public static void Main( ) {
Console.WriteLine("Enter Main..."); Test t = new Test( );
t.Func1( );
Console.WriteLine("Exit Main..."); }
public void Func1( ) {
Console.WriteLine("Enter Func1...");
try {
Console.WriteLine("Entering try block..."); Func2( );
Console.WriteLine("Exiting try block..."); }
catch {
Console.WriteLine( "Exception caught and handled." ); }
Console.WriteLine("Exit Func1..."); }
public void Func2( ) {
Console.WriteLine("Enter Func2..."); throw new System.Exception( ); Console.WriteLine("Exit Func2..."); } } } Kết quả: Enter Main... Enter Func1... Entering try block... Enter Func2...
Exit Func1... Exit Main...
Bây giờ biệt lệ không được giải quyết trong trong hàm Func2(), nó được giải quyết trong hàm Func1(). Khi Func2() được gọi, nó in dòng Enter Func2 và sau đó ném một biệt lệ. Chương trình tạm ngừng thực thi, CLR tìm kiếm trình giải quyết biệt lệ trong hàm Func2(). Không có. Vùng nhớ stack được duyệt ngược và CLR tìm thấy trình giải quyết biệt lệ trong hàm Func1(). Khối lệnh catch được gọi, chương trình tiếp tục thực thi sau khối lệnh catch này, in ra dòng Exit của Func1() và sau đó là của Main(). Dòng Exit Try Block và dòng Exit Func2 không được in.
7.1.2.3. Tạo một lệnh catch chuyên dụng
Ta có thể tạo một lệnh catch chuyên dụng quản lý một loại biệt lệ. Ví dụ 11-4 mô tả cách xác định loại biệt lệ nào ta nên quản lý.
Ví dụ 7-4. Xác định biệt lệ phải bắt
using System;
namespace Programming_CSharp {
public class Test {
public static void Main( ) {
Test t = new Test( ); t.TestFunc( );
}
// thử chia hai số
// và giải quyết các biệt lệ public void TestFunc( ) { try { double a = 5; double b = 0; Console.WriteLine("{0}/{1}={2}", a, b, DoDivide(a,b)); }
// các biệt lệ thuộc lớp con phải đứng trước
catch (System.DivideByZeroException) { Console.WriteLine("DivideByZeroException caught!"); } catch (System.ArithmeticException) { Console.WriteLine("ArithmeticException caught!"); }
// biệt lệ tổng quát đứng sau cùng
catch
{
Console.WriteLine("Unknown exception caught"); }
}
// thực hiện phép chia hợp lệ
public double DoDivide(double a, double b) {
if (b == 0)
throw new System.DivideByZeroException( ); if (a == 0)
throw new System.ArithmeticException( ); return a/b; } } } Kết quả: DivideByZeroException caught!
Trong ví dụ này, DoDivide() sẽ không cho phép chia một số cho 0 hay chia 0 cho số khác. Nó sẽ ném ra biệt lệ DivideByZeroException nếu ta cố chia cho không. Nếu ta đem chia 0 cho số khác, sẽ không có biệt lệ thích hợp: vì chia không cho một số là một phép toán hợp lệ và không nên ném bất kỳ biệt lệ nào. Tuy nhiên giả sử trong ví dụ này ta không muốn đem 0 chia cho số khác và sẽ ném ra biệt lệ
ArithmeticException.
Khi một biệt lệ được ném ra, CLR tìm kiếm trình giải quyết biệt lệ theo theo trình tự, và chọn trình giải quyết phù hợp với biệt lệ. Khi chạy chương trình với a = 5 và b = 7, kết quả là: 5 / 7 = 0.7142857142857143
Không có biệt lệ nào phát sinh. Tuy nhiên nếu thay a = 0, kết quả sẽ là: ArithmeticException caught!
Một biệt lệ được ném ra, và CLR xác định trình giải quyết biệt lệ đầu tiên: DivideByZeroException. Không đúng, CLR sẽđi đến trình giải quyết biệt lệ kế tiếp, ArithmeticException.
Cuối cùng, nếu a=7, và b=0 biệt lệ DivideByZeroException được ném ra. Ghi chú: Bời vì DevideByZero thừa kế từ ArithmeticException, nên trong ví dụ trên nếu hoán vị hai khối lệnh catch thì có thể khối lệnh catch bắt biệt lệ
DivideByZeroException sẽ không bao giờ được thực thi. Thay vào đó khối catch bắt biệt lệ ArithmeticException sẽ bắt các biệt lệ DivideByZeroException. Trình biên dịch sẽ nhận ra điều này và báo lỗi. Thông thường hàm sẽ bắt các biệt lệ
chuyên dụng cho riêng mục tiêu của hàm, còn các biệt lệ tổng quát hơn sẽ do các hàm cấp cao hơn bắt.