Chương 4: Xử lý ngoại lệ
Tác giả: Sưu tầm
Khái quát
Trong nhiều quyển sách dạy lập trình, thì xử lý ngoại lệ thường nằm ở những chương cuối của một quyển sách. Tuy nhiên, với quyển sách này, thì phần này lại nằm ở đầu, vì có nhiều lý do.
Lý do thứ nhất là do xử lý ngoại lệ là một vấn đề “thâm căn cố đế” đối với thời gian chạy .NET và do đó nó rất chung cho lập trình C#. Lập trình C++ có thể không xử lý ngoại lệ, nhưng nó không phải là tuỳ chọn đối với C#.
Lý do thứ hai là vì điều này sẽ cho phép ta có những ví dụ tốt hơn. Nếu xử lý ngoại lệ là đưa ra muộn, thì những ví dụ sẽ không thể sử dụng vấn đề này, và điều đó có nghĩa là những ví dụ không thể được viết một cách tốt nhất.
Thật là không may điều này có nghĩa là những lớp được sử dụng mà không được xử lý ngoại lệ. Đọc phần sau đây để hiển rõ hơn, còn về lớp sẽ được nói rõ hơn ở chương sau.
Điều gì xảy ra với giá trị trả về?
Đa số lập trình viên có lẽ đã từng viết chương trình giống như dạng sau:
bool success = CallFunction();
if (!success){
// kiểm soát lỗi
}
Cách viết này vẫn đúng, nhưng đối với mỗi giá trị trả về phải được kiểm tra lỗi. Nếu chương trình được viết như trên, thì lỗi được trả lại sẽ bị bỏ qua. Đó là điều kiện cho bug xuất hiện.
Có rất nhiều cách thức khác nhau để truyền thông điệp giữa các hàm; một số hàm là trả ra một giá trị HRESULT, một số khác trả lại giá trị Boolean, và một số khác nữa sử dụng một số cơ chế khác nào đó.
Trong thời gian chạy .NET, những ngoại lệ là phương pháp cơ bản của điều khiển xử lý lỗi. Những ngoại lệ là cách tốt hơn so với những giá trị trả về bởi vì chúng có thể bỏ qua mà ta không biết.
Try và Catch
Để giải quyết những ngoại lệ, chương trình cần được tổ chức hơi khác một chút. Những phần của chương trình mà có thể ném những ngoại lệ là được đặt trong khối try, và những phần để xử lý ngoại lệ thì được đặt trong khối catch. Ta có ví dụ sau:
using System;
class Test{
static int Zero = 0;
public static void Main(){
// chờ các ngoại lệ ở đây
try{
int j = 22 / Zero;
}
// các ngoại lệ xảy ra trong try được chuyển đến đây
catch (Exception e){
Console.WriteLine(“Exception ” + e.Message);
}
Console.WriteLine(“After catch”);
}
}
Bên trong khối try là một biểu thức mà nó có thể phát sinh một ngoại lệ. Trong trường hợp này, nó sẽ phát sinh một ngoại lệ là DivideByZeroException (lỗi chia cho 0). Khi phép chia xảy ra, thời gian chạy sẽ ngừng xử lý mã và tìm một khối try trong chương trình mà trong đó ngoại lệ xảy ra. Khi nó tìm thấy những khối try, nó tiếp tục đi liên hệ đến những khối catch.
Nếu nó tìm thấy những khối catch, nó sẽ chọn ra một khối hợp lý nhất (có nhiều cách để nó xác định cái nào là thích hợp nhất), và thực hiện đoạn mã bên trong khối catch. Chương trình bên trong khối catch phải xử lý một sự kiện hoặc ném trả lại nó.
Ví dụ trên là chương trình bắt một ngoại lệ và viết ra màn hình thông điệp được chứa bên trong một đối tượng ngoại lệ.
Phân cấp ngoại lệ
Tất cả các ngoại lệ của C# đều bắt nguồn từ lớp Exception, là một phần của Common Language Runtime (thời gian chạy ngôn ngữ chung). Khi một ngoại lệ xuất hiện, thì một khối catch thích hợp được xác định bằng cách so sánh kiểu của ngoại lệ với tên của ngoại lệ được đề cập. Một khối catch với một so sánh chính xác sẽ dẫn tới một ngoại lệ chung hơn. Trở lại với ví dụ:
using System;
class Test{
static int Zero = 0;
public static void Main(){
try{
int j = 22 / Zero;
}
// bắt một ngoại lệ đặc biệt
catch (DivideByZeroException e){
Console.WriteLine(“DivideByZero {0}”, e);
}
// bắt bất kỳ ngoại lệ nào còn lại
catch (Exception e){
Console.WriteLine(“Exception {0}”, e);
}
}
}
Khối catch mà nó sẽ bắt một lỗi chia cho 0 là một sự đối sánh rất đặc biệt, và do đó nó là được xử lý.
Một ví dụ hơi phức tạp hơn:
using System;
class Test{
static int Zero = 0;
static void AFunction(){
int j = 22 / Zero;
// dòng sau không bao giờ được thực thi.
Console.WriteLine(“In AFunction()”);
}
public static void Main(){
try{
AFunction();
}
catch (DivideByZeroException e){
Console.WriteLine(“DivideByZero {0}”, e);
}
}
}
Điều gì xảy ra ở đây?
Một phép chia được xử lý khi một ngoại lệ phát sinh. Thời gian chạy bắt đầu tìm một khối try bên trong hàm Afunction(), nhưng nó không tìm thấy, nên nó sẽ nhảy ra tìm ở bên ngoài hàm Afunction(), và nó kiểm tra trong hàm Main(). Nó tìm thấy một khối try và sau đó nó tìm một khối catch để có thể đối sánh. Sau đó, một khối catch sẽ xử lý. Đôi khi, sẽ không có những khối catch phù hợp.
using System;
class Test{
static int Zero = 0;
static void AFunction(){
try{
int j = 22 / Zero;
}
// ngoại lệ này không được đối sánh
catch (ArgumentOutOfRangeException e){
Console.WriteLine(“OutOfRangeException: {0}”, e);
}
Console.WriteLine(“In AFunction()”);
}
public static void Main(){
try{
AFunction();
}
// ngoại lệ này không được đối sánh
catch (ArgumentException e){
Console.WriteLine(“ArgumentException {0}”, e);
}
}
}
Không có khối catch nào trong hàm Afunction() và cũng không có trong Main() để đối sánh với ngoại lệ được ném ra. Khi điều này xuất hiện, thì ngoại lệ sẽ được xử lý bởi trình xử lý ngoại lệ “cơ hội cuối”. Cách xử lý của trình xử lý là phụ thuộc vào cách mà thời gian chạy được quy định, nhưng nó sẽ thường xuất hiện một thông báo chứa thông tin ngoại lệ và dừng chương trình.
Chuyển ngoại lệ cho nơi gọi
using System;
public class Summer {
int sum = 0;
int count = 0;
float average;
public void DoAverage() {
try {
average = sum / count;
}
catch (DivideByZeroException e) {
// do some cleanup here
throw e;
}
}
}
class Test {
public static void Main() {
Summer summer = new Summer();
try {
summer.DoAverage();
}
catch (Exception e) {
Console.WriteLine(“Exception {0}”, e);
}
}
}
using System;
public class Summer {
int sum = 0;
int count = 0;
float average;
public void DoAverage() {
try {
average = sum / count;
}
catch (DivideByZeroException e) {
// wrap exception in another one,
// adding additional context.
throw (new DivideByZeroException(“Count is zero in DoAverage()”, e));
}
}
}
public class Test {
public static void Main() {
Summer summer = new Summer();
try {
summer.DoAverage();
}
catch (Exception e) {
Console.WriteLine(“Exception: {0}”, e);
}
}
}
using System;
public class CountIsZeroException: Exception {
public CountIsZeroException() {
}
public CountIsZeroException(string message): base(message) {
}
public CountIsZeroException(string message, Exception inner)
: base(message, inner) {
}
}
public class Summer {
int sum = 0;
int count = 0;
float average;
public void DoAverage() {
if (count == 0)
throw(new CountIsZeroException(“Zero count in DoAverage”));
else
average = sum / count;
}
}
class Test {
public static void Main() {
Summer summer = new Summer();
try {
summer.DoAverage();
}
catch (CountIsZeroException e) {
Console.WriteLine(“CountIsZeroException: {0}”, e);
}
}
}
using System;
using System.IO;
class Processor {
int count;
int sum;
public int average;
void CalculateAverage(int countAdd, int sumAdd) {
count += countAdd;
sum += sumAdd;
average = sum / count;
}
public void ProcessFile() {
FileStream f = new FileStream(“data.txt”, FileMode.Open);
try {
StreamReader t = new StreamReader(f);
string line;
while ((line = t.ReadLine()) != null) {
int count;
int sum;
count = Int32.FromString(line);
line = t.ReadLine();
sum = Int32.FromString(line);
CalculateAverage(count, sum);
}
f.Close();
}
// always executed before function exit, even if an
// exception was thrown in the try.
finally {
f.Close();
}
}
}
class Test {
public static void Main() {
Processor processor = new Processor();
try {
processor.ProcessFile();
}
catch (Exception e) {
Console.WriteLine(“Exception: {0}”, e);
}
}
}