Cấu trúc bắt lỗi
Bắt lỗi với cấu trúc try .. except và try .. finally
Bắt lỗi với cấu trúc try .. except và try .. finally
Trong chương trình có những lỗi không thể lường trước được. Có những lỗi có thể dễ dàng nhận biết và sửa. Trong đó có những lỗi rất khó lường, xảy ra bất ngờ khi chạy chương trình. Lúc đó chương trình sẽ crash và tệ hơn là hỏng cơ sở dữ liệu.
Lấy ví dụ khi chia hai biến A / B, ta có thể biết được lỗi chia cho 0 khi biến B bằng 0 và có thể đặt một điều kiện B <> 0.
Nhưng có những lỗi như lỗi đĩa khi đọc ghi file, tràn bộ nhớ, lỗi mạng khi đang tải xuống một tập tin từ internet. Những lỗi đó không thể lường trước được.
Tương tự như các ngôn ngữ hiện đại khác, Delphi cung cấp cấu trúc bắt lỗi để bắt những lỗi trên (gọi là các ngoại lệ - exception).
var
a, b: Integer;
c: Single;
...
begin
try
c := a/b;
except
on E: Exception do
begin
WriteLn(E.ClassName);
WriteLn(E.Message);
end;
end;
end.
Hãy chạy thử đoạn chương trình trên (nhớ thêm các phần khác để thành chương trình hoàn chỉnh). Các bạn sẽ hiểu được cách thức làm việc của cấu trúc try ... except.
Cấu trúc hoạt động: Các khối lệnh trong khối try được thực hiện tuần tự, nếu có lỗi những lệnh tiếp theo sẽ bị bỏ qua và nhảy ngay đến thực hiện khối except. Nếu toàn bộ khối try không có lỗi thì đến khối except sẽ bị bỏ qua.
Trong thư viện unit System.SysUtils có định nghĩa một số lớp con của lớp Exception, mang tiền tố E ở đầu, ví dụ EZeroDivide (để bắt lỗi chia cho 0), EInOutError (để bắt lỗi vào ra), ...
Lấy đoạn code mẫu ở trên, ta triển khai các trường hợp lỗi với các thông báo khác nhau
var
a, b: Integer;
c: Single;
...
begin
try
c := a/b;
except
on E: EZeroDivide do
WriteLn('Khong the chia cho 0');
on E: EInOutError do
WriteLn('Co loi vao ra');
else
WriteLn('Loi khac');
end;
end.
Sau từ khóa on là một biến exception (E) kiểu lỗi tùy chỉnh (EZeroDivide, EInOutError, ...) và kèm theo từ khóa do, sau do là một câu lệnh hoặc cụm begin end để xử lí lỗi đó.
Từ khóa else ở cuối cùng trong khối except để bắt các lỗi còn lại không được liệt kê ở trên.
Những ví dụ trên, ta đã sử dụng lớp Exception
on E: Exception do ...
Ta gọi E là một thể hiện (instance) của lớp Exception. E là object, Exception là class.
Điểm đặc biệt của đối tượng Exception là bạn không cần phải giải phóng nó sau khi tạo như các đối tượng thông thường khác.
Từ khóa raise ném ra một lỗi, và bắt buộc phải đặt trong một on Exception do. Nó cho phép dừng chương trình, xem lỗi. Cú pháp
on E: Exception do
begin
WriteLn('Co loi');
raise E.Create('Mot loi khong xac dinh');
end;
Nói chung phần raise này khá khó hiểu, nôm na là thông báo cho trình gỡ lỗi (debugger) biết cụ thể về lỗi, chứ để mặc định lỗi sẽ tự động raise một thông báo khó hiểu, dùng cú pháp raise để tạo ra thông báo tùy chỉnh dễ hiểu hơn.
Trong C#, mọi đối tượng được tạo ra, sử dụng, cuối cùng được phá hủy và giải phóng bởi bộ gom rác (GC - Garbage Collector). Thế nên lập trình viên C# sẽ không cần quản lí bộ nhớ vì đã có GC giúp.
Điểm yếu của việc sử dụng GC là chúng ta không biết đối tượng sẽ bị phá hủy lúc nào, dẫn đến đôi khi có lỗi vi phạm bộ nhớ (vì cố gắng đọc dữ liệu từ một đối tượng đã bị phá hủy). Và GC hoạt động ngẫu nhiên để giải phóng bộ nhớ, dẫn đến tình trạng lag ở một số game bằng C# và Java.
Delphi không như C# và Java, bắt buộc chúng ta phải kĩ lưỡng giải phóng đối tượng thủ công khi đã sử dụng xong, và rèn luyện tính kỉ luật khi viết code.
Ví dụ, đoạn code tạo một đối tượng List từ lớp TStringList, để đọc tất cả dòng trong file DanhSach.txt và in ra màn hình
procedure PrintList;
var
List: TStringList;
begin
List := TStringList.Create;
List.LoadFromFile('DanhSach.txt');
WriteLn(List.Text);
List.Free;
end.
Chuyện gì sẽ xảy ra nếu file DanhSach.txt không tồn tại. Một ngoại lệ EInOutError được ném ra, chương trình con bị dừng.
Không như các biến cục bộ, các đối tượng không tự động bị hủy khi dừng chương trình con.
Và khi đó, đối tượng List chưa được giải phóng (chưa thực hiện List.Free), một lỗi tràn bộ nhớ (Memory leak) xuất hiện.
Tuy bộ nhớ cho đối tượng List không nhiều nhưng hãy thử nghĩ nếu thủ tục PrintList trên được thực hiện 10 000 lần, bộ nhớ bị rò rỉ sẽ rất lớn do mỗi lần chạy PrintList tạo ra một List riêng biệt.
Do đó, Delphi cung cấp cấu trúc try ... finally để xử lí những đối tượng đó.
Dưới đây là đoạn mã đã được chỉnh sửa. Mỗi khi có lỗi xuất hiện trong khối try, các lệnh tiếp theo bị bỏ qua và nhảy ngay đến khối finally để phá hủy đối tượng, từ đó không tạo ra memory leak.
procedure PrintList;
var
List: TStringList;
begin
List := TStringList.Create;
try
List.LoadFromFile('DanhSach.txt');
WriteLn(List.Text);
finally
List.Free;
end;
end.
Bạn có thể sử dụng các khối try ... except và try ... finally lồng nhau như sau
try
try
// Làm gì đó ở đây
except
// Xử lí lỗi
end;
finally
// Free đối tượng
end;
Tôi và cộng đồng Delphi đang trông chờ vào tính năng rút gọn mã kết hợp cả except và finally như ví dụ sau, mong Embarcadero sẽ ra mắt tính năng này sớm.
try
// Làm gì đó
except
// Xử lí lỗi
finally
// Free đối tượng
end;