Ngôn Ngữ Lập Trình C#
Lớp MyCustomException được dẫn xuất từ System.ApplicationException và lớp này không
có thực thi hay khai báo gì ngoài một hàm khởi dựng. Hàm khởi dựng này lấy tham số là một
chuỗi và truyền cho lớp cơ sở. Trong trường hợp này, lợi ích của việc tạo ra ngoại lệ là làm
nổi bật điều mà chuơng trình muốn minh họa, tức là không cho phép số chia là zero. Sử dụng
ngoại lệ ArithmeticException thì tốt hơn là ngoại lệ chúng ta tạo ra. Nhưng nó có thể làm
nhầm lẫn cho những người lập trình khác vì phép chia với số chia là zero không phải là lỗi số
học.
Phát sinh lại ngoại lệ
Giả sử chúng ta muốn khối catch thực hiện một vài hành động đúng nào đó rồi sau đó
phát sinh lại ngoại lệ ra bên ngoài khối catch (trong một hàm gọi). Chúng ta được phép phát
sinh lại cùng một ngoại lệ hay phát sinh lại các ngoại lệ khác. Nếu phát sinh ra ngoại lệ khác,
chúng ta có thể phải nhúng ngoại lệ ban đầu vào bên trong ngoại lệ mới để phương thức gọi
có thể hiểu được lai lịch và nguồn gốc của ngoại lệ. Thuộc tính InnerException của ngoại lệ
mới cho phép truy cập ngoại lệ ban đầu.
Bởi vì InnerException cũng là một ngoại lệ, nên nó cũng có một ngoại lệ bên trong. Do
vậy, toàn bộ dây chuyền ngoại lệ là một sự đóng tổ (nest) của một ngoại lệ này với một ngoại
lệ khác. Giống như là con lật đật, mỗi con chứa trong một con và đến lượt con bên trong lại
chứa
Ví dụ 13.8: Phát sinh lại ngoại lệ & ngoại lệ inner.
namespace Programming_CSharp
{
using System;
// tạo ngoại lệ riêng
public class MyCustomException : System.Exception
{
public MyCustomException( string message, Exception inner):
base(message, inner)
{
}
}
public class Test
{
public static void Main()
{
Test t = new Test();
Xử Lý Ngoại Lệ
382
.
.
Ngôn Ngữ Lập Trình C#
t.TestFunc();
}
// chia hai số và xử lý ngoại lệ
public void TestFunc()
{
try
{
DangerousFunc1();
}
catch (MyCustomException e)
{
Console.WriteLine(“\n{0}”, e.Message);
Console.WriteLine(“Retrieving exception history ”);
Exception inner = e.InnerException;
while ( inner != null)
{
Console.WriteLine(“{0}”, inner.Message);
inner = inner.InnerException;
}
}
}
public void DangerousFunc1()
{
try
{
DangerousFunc2();
}
catch (System.Exception e)
{
MyCustomException ex = new
MyCustomException(“E3 – Custom Exception Situation”, e);
throw ex;
}
}
public void DangerousFunc2()
{
try
{
Xử Lý Ngoại Lệ
383
.
.
Ngôn Ngữ Lập Trình C#
DangerousFunc3();
}
catch (System.DivideByZeroException e)
{
Exception ex = new Exception(“E2 - Func2 caught divide by zero”, e);
throw ex;
}
}
public void DangerousFunc3()
{
try
{
DangerousFunc4();
}
catch (System.ArithmeticException)
{
throw;
}
catch (System.Exception)
{
Console.WriteLine(“Exception handled here.”);
}
}
public void DangerousFunc4()
{
throw new DivideByZeroException(“E1 – DivideByZero Exception”);
}
}
}
Kết quả:
E3 – Custom Exception Situation!
Retrieving exception history
E2 - Func2 caught divide by zero
E1 – DivideByZeroException
Để hiểu rõ hơn ta có thể dùng trình debugger để chạy từng bước chương trình khi đó ta sẽ
hiểu rõ từng bước thực thi cũng như việc phát sinh các ngoại lệ.
Xử Lý Ngoại Lệ
384
.
.
Ngôn Ngữ Lập Trình C#
Chương trình bắt đầu với việc gọi hàm DangerousFunc1() trong khối try:
try
{
DangerousFunc1();
}
DangerousFunc1() gọi DangerousFunc2(), DangerousFunc2() lại gọi DangerousFunc3(), và
cuối cùng DangerousFunc3() gọi DangerousFunc4(). Tất cả việc gọi này điều nằm trong khối
try. Cuối cùng, DangerousFunc4() phát sinh ra ngoại lệ DivideByzeroException. Ngoại lệ
này bình thường có chứa thông điệp bên trong nó, nhưng ở đây chúng ta tự do dùng thông
điệp mới. Để dễ theo dõi chúng ta đưa vào các chuỗi xác nhận tuần tự các sự kiện diễn ra.
Ngoại lệ được phát sinh trong DangerousFunc4() và nó được bắt trong khối catch trong hàm
DangerousFunc3(). Khối catch trong DangerousFunc3() sẽ bắt các ngoại lệ Arithmetic-
Exception ( như là DivideByZeroException), nó không thực hiện hành động nào mà chỉ đơn
giản là phát sinh lại ngoại lệ:
catch ( System.ArithmeticException)
{
throw;
}
Cú pháp để thực hiện phát sinh lại cùng một ngoại lệ mà không có bất cứ bổ sung hay hiệu
chỉnh nào là : throw.
Do vậy ngoại lệ được phát sinh cho DangerousFunc2(), khối catch trong DangerousFunc2()
thực hiện một vài hành động và tiếp tục phát sinh một ngoại lệ có kiểu mới. Trong hàm khởi
dựng của ngoại lệ mới, DangerousFunc2() truyền một chuỗi thông điệp mới (“E2 - Func2
caught divide by zero”) và ngoại lệ ban đầu. Do vậy ngoại lệ ban đầu (E1) trở thành ngoại lệ
bên trong của ngoại lệ mới (E2). Sau đó hàm DangerousFunc2() phát sinh ngoại lệ này (E2)
cho hàm DangerousFunc1().
DangerousFunc1() bắt giữ ngoại lệ này, làm một số công việc và tạo ra một ngoại lệ mới có
kiểu là MyCustomException, truyền vào hàm khởi dựng của ngoại lệ mới một chuỗi mới
(“E3 – Custom Exception Situation!”) và ngoại lệ được bắt giữ (E2). Chúng ta nên nhớ rằng
ngoại lệ được bắt giữ là ngoại lệ có chứa ngoại lệ DivideByZeroException (E1) bên trong nó.
Tại thời điểm này, chúng ta có một ngoại lệ kiểu MyCustomException (E3), ngoại lệ này chứa
bên trong một ngoại lệ kiểu Exception (E2), và đến lượt nó chứa một ngoại lệ kiểu
DivideByZeroException (E1) bên trong. Sau cùng ngoại lệ được phát sinh cho hàm TestFunc;
Khi khối catch của TestFunc thực hiện nó sẽ in ra thông điệp của ngoại lệ :
E3 – Custom Exception Situation!
sau đó từng ngoại lệ bên trong sẽ được lấy ra thông qua vòng lặp while:
while ( inner != null)
{
Xử Lý Ngoại Lệ
385
.
.
Ngôn Ngữ Lập Trình C#
Console.WriteLine(“{0}”, inner.Message);
inner = inner.InnerException;
}
Kết quả là chuỗi các ngoại lệ được phát sinh và được bắt giữ:
Retrieving exception history
E2 - Func2 caught divide by zero
E1 – DivideByZero Exception
Câu hỏi và trả lời
Câu hỏi 1: Việc sử dụng catch không có tham số có vẻ như có nhiều sức mạnh do chúng bắt
giữa tất cả các ngoại lệ. Tại sao chúng ta không luôn luôn sử dụng câu lệnh catch không có
tham số để bắt các lỗi?
Trả lời 1: Mặc dù sử dụng catch duy nhất có rất nhiều sức mạnh, nhưng nó cũng làm mất rất
nhiều thông tin quan trọng về ngoại lệ được phát sinh. Khi đó chúng ta sẽ không biết chính
xác loại ngoại lệ xảy ra và khó có thể bảo trì cũng như khắc phục những ngoại lệ sau này. Về
phía người dùng cũng vậy. Nếu chương trình gặp ngoại lệ mà không có thông báo rõ ràng
cho nguời dùng thì có thể làm cho họ hoang mang, và có thể đổ lỗi cho chương trình của
chúng ta không tốt ngay cả những lỗi không phải do ta. Ví dụ như lỗi hết tài nguyên bộ nhớ
do người dùng sử dụng quá nhiều chương trình hoạt động cùng lúc. Tóm lại là chúng ta nên
sử dụng catch với những tham số chi tiết để thực hiện tốt việc quản lý các ngoại lệ được phát
sinh.
Câu hỏi 2: Có phải tất cả những ngoại lệ được đối xử một cách bình đẳng?
Trả lời 2: Không phải, có hai loại ngoại lệ, ngoại lệ hệ thống và ngoại lệ của chương trình
ứng dụng. Ngoại lệ của chương trình ứng dụng thì sẽ không kết thúc chương trình. Còn
ngoại lệ hệ thống thì sẽ kết thúc chương trình. Nói chung đó là những ngoại lệ xuất hiện
trước đây. Hiện nay thì người ta chia ra nhiều mức độ ngoại lệ và tùy theo từng mức độ của
ngoại lệ mà chương trình của chúng ta sẽ được nhận những ứng xử khác nhau. Để biết thêm
chi tiết chúng ta có thể đọc thêm trong tài liệu .NET Framework về xử lý ngoại lệ.
Câu hỏi 3: Như câu trả lời bên trên tại sao tôi phải tìm hiểu nhiều về các ngoại lệ và cách thức
xử lý các ngoại lệ khi chúng được phát sinh?
Trả lời 3: Việc xây dựng một chương trình ứng dụng là hết sức phức tạp, chương trình luôn
tiếm ẩn những yếu tố không ổn định và có thể phát sinh các ngoại lệ dẫn đến những lỗi
không mong muốn. Việc thực hiện bắt giữ các ngoại lệ là hết sức cần thiết trong chương
trình, nó cho phép chúng ta xây dựng được chương trình hoàn thiện hơn và xử lý các thông
điệp ngoại lệ tốt hơn. Tìm hiểu những ngoại lệ đem đến cho chúng ta nhiều kinh nghiệm
trong việc xây dựng các chương trình phức tạp hơn.
Câu hỏi thêm
Câu hỏi 1: Hãy cho biết các từ khóa được sử dụng để xử lý ngoại lệ?
Xử Lý Ngoại Lệ
386
.
.