Xử lý ngoại lệ trong C#

Một ngoại lệ (exception) được CLR (Common Language Runtime) hoặc mã chương trình ném ra nếu có lỗi trong chương trình. Những ngoại lệ này cần được xử lý để ngăn chặn sự cố chương trình. C# cung cấp hỗ trợ tích hợp để xử lý ngoại lệ bằng cách sử dụng khối try, catch finally.

Cú pháp khối try, catch finally trong C#:

try
{
    // code that may raise exceptions
}
catch(Exception ex)
{
    // handle exception
}
finally
{
    // final cleanup code
}

Theo cú pháp trên, đặt mã có thể đưa ra một ngoại lệ vào khối try theo sau là một khối catch hoặc finally. Đưa các khai báo biến ra ngoài khối try để chúng có thể được truy cập trong khối catch và khối finally.

Hãy xem xét các mã sau đây có thể đưa ra một ngoại lệ.

class Program
{
    static void Main(string[] args)
    {
        Console.Write("Enter Student Name: ");

        string studentName = Console.ReadLine();

        IList<string> studentList = FindAllStudentFromDatabase(studentName);

        Console.WriteLine("Total {0}: {1}", studentName, studentList.Count());

        Console.ReadKey();
    }

    private static IList<string> FindAllStudentFromDatabase(string studentName)
    {
        var studentList = // find all students with same name from the database 

        return studentList;
    }
}

Trong ví dụ trên, nó hiển thị tổng số sinh viên có cùng tên. Giả sử rằng phương thức FindAllStudentFromDatabase() lấy danh sách sinh viên có cùng tên từ cơ sở dữ liệu.

Ví dụ trên sẽ hoạt động tốt nếu có ít nhất một sinh viên được tìm thấy, nếu không, nó sẽ gây ra ngoại lệ NullReferenceException.

Chúng tôi không muốn hiển thị thông báo ngoại lệ cho người dùng thấy và chương trình dừng thực thi. Vì vậy, chúng ta cần xử lý ngoại lệ bằng cách sử dụng khối try catch như dưới đây.

class Program
{
    static void Main(string[] args)
    {
        Console.Write("Enter Student Name: ");

        string studentName = Console.ReadLine();
        
        try
        {
            IList<string> studentList = FindAllStudentFromDatabase(studentName);

            Console.WriteLine("Total {0}: {1}", studentName, studentList.Count());
        }
        catch(Exception ex)
        {
            Console.Write("No Students exists for the specified name.");
        }

        Console.ReadKey();
    }

    private static IList<string> FindAllStudentFromDatabase(string studentName)
    {
        var studentList = // find all students with same name from the database 

        return studentList;
    }
}

Như bạn có thể thấy trong ví dụ trên, studentList.Count() có thể đưa ra một ngoại lệ nếu studentList null. Vì vậy, bọc mã này vào khối try. Khối try chỉ đơn giản nói cho trình biên dịch biết cần phải theo dõi ngoại lệ nếu nó xảy ra. Nếu ngoại lệ xảy ra trong khối try thì nó phải được xử lý bằng cách sử dụng khối catch.

Lưu ý: khối try phải được theo sau bởi khối catch hoặc finally hoặc cả hai. Khối try mà không có khối catch hoặc finally sẽ có lỗi l biên dịch.

Các lớp ngoại lệ trong C#

Các ngoại lệ C# được đại diện bởi các lớp ngoại lệ. Các lớp ngoại lệ trong C# xuất phát trực tiếp hoặc gián tiếp từ lớp System.Exception. Một trong số đó là lớp System.ApplicationExceptionSystem.SystemException .

Lớp System.ApplicationException hỗ trợ các ngoại lệ được tạo bởi các chương trình ứng dụng.

Lớp System.SystemException là lớp cơ sở cho tất cả các ngoại lệ hệ thống được định nghĩa trước.

Bảng sau đây cung cấp một số lớp ngoại lệ được định nghĩa trước có nguồn gốc từ lớp Sytem.SystemException

Lớp Exception Miêu tả
System.IO.IOException Xử lý các lỗi về đọc ghi file
System.IndexOutOfRangeException Xử lý các lỗi được tạo khi một phương thức tham chiếu tới một chỉ mục bên ngoài dãy mảng
System.ArrayTypeMismatchException Xử lý các lỗi được tạo khi kiểu là không phù hợp với kiểu mảng
System.NullReferenceException Xử lý các lỗi được tạo từ việc tham chiếu một đối tượng null
System.DivideByZeroException Xử lý các lỗi được tạo khi chia cho số 0
System.InvalidCastException Xử lý lỗi được tạo trong khi ép kiểu
System.OutOfMemoryException Xử lý lỗi được tạo từ việc thiếu bộ nhớ
System.StackOverflowException Xử lý lỗi được tạo từ việc tràn ngăn xếp (stack)

Khối catch trong C#

Ngoại lệ xảy ra trong khối try có thể được xử lý bằng cách sử dụng khối catch như trong ví dụ trên. Mã trong khối catch sẽ chỉ thực thi khi xảy ra ngoại lệ.

mTrong C#, Một khối try có một hoặc nhiều khối catch kèm theo để xử lý các ngoại lệ khác nhau.

Việc sử dụng nhiều khối catch sẽ rất hữu ích khi bạn muốn xử lý các ngoại lệ khác nhau theo những cách khác nhau.

Ví dụ sau sẽ minh họa cho việc sử dụng nhiều khối catch để xử lý các ngoại lệ khác nhau:

class Program
{
    static void Main(string[] args)
    {
        Console.Write("Please enter two numbers: ");
        
        try
        {
            int num1 = int.Parse(Console.ReadLine());
            int num2 = int.Parse(Console.ReadLine());

            int result = num1 / num2;

            Console.WriteLine("{0} / {1} = {2}", num1, num2, result);
        }
        catch(DivideByZeroException ex)
        {
            LogError(ex);
            Console.Write("Cannot divide by zero. Please try again.");
        }
        catch(InvalidOperationException ex)
        {
            LogError(ex);
            Console.Write("Not a valid number. Please try again.");
        }
        catch(FormatException  ex)
        {
            LogError(ex);
            Console.Write("Not a valid number. Please try again.");
        }

        Console.ReadKey();
    }

}

Trong ví dụ trên, chúng tôi đã chỉ định nhiều khối catch để xử lý các loại ngoại lệ khác nhau, để chúng tôi có thể hiển thị thông báo phù hợp cho người dùng, tùy thuộc vào lỗi và do đó người dùng không lặp lại lỗi tương tự.

Lưu ý: Không được phép sử dụng nhiều khối catch cho cùng một loại ngoại lệ. Nó sẽ gây lỗi khi biên dịch.

Khối catch không hợp lệ

Khối catch không tham số và khối catch có tham số Exception không được phép trong cùng một câu lệnh try, vì cả hai đều làm cùng một việc.

try
{
    //code that may raise an exception
}
catch //cannot have both catch and catch(Exception ex)
{ 
    Console.WriteLine("Exception occurred");
}
catch(Exception ex) //cannot have both catch and catch(Exception ex)
{
    Console.WriteLine("Exception occurred");
}

Ngoài ra, khối catch{ } hoặc catch(Exception ex){ } thì phải ở vị trí cuối cùng. Trình biên dịch sẽ báo lỗi nếu bạn có các khối catch khác sau một catch{ } hoặc catch(Exception ex).

try
{
    //code that may raise an exception
}
catch
{ 
    // this catch block must be last block
}
catch (NullReferenceException nullEx)
{
    Console.WriteLine(nullEx.Message);
}
catch (InvalidCastException inEx)
{
    Console.WriteLine(inEx.Message);
}

Khối finally trong C#

Khối finally phải đặt sau một khối try hoặc catch. Khối finally sẽ luôn được thực thi cho dù có xảy ra ngoại lệ hay không. Khối finally thường được sử dụng để đóng kết nối, xóa dữ liệu tạm, hủy đối tượng, ...

Dưới đây là một ví dụ về khối finally trong C#:

static void Main(string[] args)
{
    int zero = 0;    
    
    try
    {
        int result = 5/zero;  // this will throw an exception       
    }
    catch(Exception ex)
    {
        Console.WriteLine("Inside catch block. Exception: {0}", ex.Message );
    }
    finally
    {
        Console.WriteLine("Inside finally block.");
    }
}

Kết quả khi chạy chương trình:

Inside catch block. Exception: Attempted to divide by zero.
Inside finally block.
Lưu ý: Chỉ có duy nhất một khối finally trong khối try catch finally. Ngoài ra, khối finally không thể có các từ khóa return, continue, break. Nó không cho phép tự điều khiển rời khỏi khối finally.

Khối try catch lồng nhau trong C#

C# cho phép các khối try catch lồng nhau. Trong khối try catch lồng nhau, một ngoại lệ sẽ được bắt và xử lý trong khối catch theo sau khối try nơi xảy ra ngoại lệ.

static void Main(string[] args)
{
    Student std = null;
           
    try
    {
        try
        {
            std.StudentName = "";
        }
        catch
        {
            Console.WriteLine("Inner catch");
        }
    }
    catch
    {
        Console.WriteLine("Outer catch");
    }
}

Kết quả khi chạy chương trình:

Inner catch

Nếu không có bất kỳ khối catch bên trong nào có loại ngoại lệ phù hợp, thì ngoại lệ sẽ chuyển sang khối catch bên ngoài cho đến khi tìm thấy bộ lọc ngoại lệ phù hợp. Hãy xem xét ví dụ sau.

static void Main(string[] args)
{
    Student std = null;
           
    try
    {
        try
        {
            // following throws NullReferenceException
            std.StudentName = "";
        }
        catch (InvalidOperationException innerEx)
        {
            Console.WriteLine("Inner catch");
        }
    }
    catch
    {
        Console.WriteLine("Outer catch");
    }
}

Kết quả:

Outer catch

Trong ví dụ trên, std.StudentName sẽ đưa ra ngoại lệ NullReferenceException, nhưng không có bất kỳ khối catch nào xử lý ngoại lệ NullReferenceException hoặc Exception. Vì vậy, nó sẽ được xử lý bởi khối catch bên ngoài.

Tạo lớp ngoại lệ tùy chỉnh trong C#

Bạn cũng có thể định nghĩa ngoại lệ của riêng bạn. Các lớp ngoại lệ do người dùng định nghĩa kế thừa từ lớp Exception. Ví dụ sau đây minh chứng điều này

using System;

namespace UserDefinedException 
{
   class TestTemperature 
   {
      static void Main(string[] args) 
      {
         Temperature temp = new Temperature();
         try 
         {
            temp.ShowTemp();
         } 
         catch(TempIsZeroException e) 
         {
            Console.WriteLine("TempIsZeroException: {0}", e.Message);
         }
         Console.ReadKey();
      }
   }
}

public class TempIsZeroException: Exception 
{
   public TempIsZeroException(string message): base(message) 
   {
   }
}

public class Temperature 
{
   int temperature = 0;
   
   public void ShowTemp() 
   {      
      if(temperature == 0) 
      {
         throw new TempIsZeroException("Zero Temperature found");
      } 
      else 
      {
         Console.WriteLine("Temperature: {0}", temperature);
      }
   }
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

TempIsZeroException: Zero Temperature found

Từ khóa throw trong C#

Một ngoại lệ có thể được ném ra bằng tay bằng cách sử dụng từ khóa throw. Bất kỳ loại ngoại lệ nào có nguồn gốc từ lớp Exception đều có thể được ném ra bằng cách sử dụng từ khóa throw. Xem ví dụ sau:

static void Main(string[] args)
{
    Student std = null;

    try
    {
        PrintStudentName(std);
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.Message );
    }                      

    Console.ReadKey();
}

private static void PrintStudentName(Student std)
{
    if (std == null)
    {
        throw new NullReferenceException("Student object is null.");
    }
    Console.WriteLine(std.StudentName);
}

Đây là kết quả khi biên dịch và chạy chương trình:

Student object is null.

Trong ví dụ trên, phương thức PrintStudentName() ném ra ngoại lệ NullReferenceException nếu đối tượng Student là null.

Xin lưu ý rằng throw tạo ra một đối tượng thuộc bất kỳ loại ngoại lệ hợp lệ nào bằng cách sử dụng từ khóa new. Từ khóa throw không thể được sử dụng với bất kỳ loại nào khác không xuất phát từ lớp Exception.

Ném lại ngoại lệ

Bạn cũng có thể ném lại một ngoại lệ từ khối catch để chuyển cho để chuyển cho trình xử lý theo cách họ muốn. Ví dụ sau đây ném lại một ngoại lệ.

static void Main(string[] args)
{
    try
    {
        Method1();
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.StackTrace);
    }                      
}

static void Method1()
{
    try
    {
        Method2();
    }
    catch(Exception ex)
    {
        throw;
    } 
}

static void Method2()
{
    string str = null;
    try
    {
        Console.WriteLine(str[0]);
    }
    catch(Exception ex)
    {
        throw;
    }
}

Đây là kết quả khi chạy chương trình trên:

at Program.Method2() in d:\Windows\Temp\kz1lzfqu.0.cs:line 40
at Program.Method1() in d:\Windows\Temp\kz1lzfqu.0.cs:line 27
at Program.Main() in d:\Windows\Temp\kz1lzfqu.0.cs:line 11

Trong ví dụ trên, một ngoại lệ xảy ra trong phương thức Method2(). Khối catch đơn giản chỉ ném ngoại lệ đó bằng cách chỉ sử dụng từ khóa throw (không có e). Ngoại lệ này sẽ được xử lý trong khối catch của phương thức Method1(), phương thức này tiếp tục ném ngoại lệ và cuối cùng nó được xử lý trong phương thức Main(). Dấu vết ngăn xếp (StackTrace) của ngoại lệ này sẽ cung cấp cho bạn chi tiết đầy đủ về nơi chính xác ngoại lệ này xảy ra.

Nếu bạn ném lại một ngoại lệ bằng tham số ngoại lệ thì nó sẽ không giữ ngoại lệ ban đầu mà tạo ngoại lệ mới. Ví dụ sau đây chứng minh điều này.

static void Main(string[] args)
{
    try
    {
        Method1();
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.StackTrace);
    }                      
}

static void Method1()
{
    try
    {
        Method2();
    }
    catch(Exception ex)
    {
        throw ex;
    } 
}

static void Method2()
{
    string str = null;
    try
    {
        Console.WriteLine(str[0]);
    }
    catch(Exception ex)
    {
        throw;
    } 
}

Đây là kết quả khi biên dịch và chạy chương trình:

at Program.Method1() in d:\Windows\Temp\xzsis2fq.0.cs:line 25
at Program.Main() in d:\Windows\Temp\xzsis2fq.0.cs:line 9

Trong ví dụ trên, ngoại lệ bắt được trong phương thức Main() sẽ hiển thị dấu vết ngăn xếp từ phương thức Method1 và phương thức Main. Nó sẽ không hiển thị phương thức Method2 trong dấu vết ngăn xếp khi chúng ta ném lại ngoại lệ trong phương thức Method1() bằng cách sử dụng throw ex. Vì vậy, không bao giờ ném một ngoại lệ bằng cách sử dụng throw <exception parameter>.

Những điểm cần nhớ:

  • Sử dụng các khối try, catch finally để xử lý các ngoại lệ trong C#.
  • Khối try phải được theo sau bởi một khối catch hoặc finally hoặc cả hai.
  • Có thể sử dụng nhiều khối catch để bắt và xử lý nhiều loại ngoại lệ khác nhau. Khối catch{..} hoặc catch(Exception ex){ } phải ở cuối cùng.
  • Không thể sử dụng đồng thời hai khối catch{..}catch(Exception ex){ }.
  • Khối finally phải ở sau khối try hoặc catch.
  • Khối finally sẽ luôn được thực thi bất kể có xảy ra ngoại lệ hay không.
  • Khối finally là nơi thích hợp để xử lý các đối tượng như đóng kết nối, xóa bộ nhớ đệm, hủy đối tượng, ...
  • Khối finally không thể sử dụng từ khóa return, continue, break vì nó không cho phép tự ý rời khỏi khối này.
  • Các khối try catch lồng nhau được phép sử dụng trong C#.
  • Một ngoại lệ sẽ được bắt và xử lý trong khối catchbên trong nếu tìm thấy bộ lọc thích hợp, nếu không sẽ bị bắt và xử lý bởi khối catch bên ngoài.
  • Sử dụng throw thay vì throw ex để giữ stack trace giúp truy vết và xử lý lỗi dễ dàng.
Lập Trình C#Lập Trình C# Cơ Bản
Bài Viết Liên Quan:
int[] và int[,] trong C#: Ai nhanh hơn
Trung Nguyen 10/10/2020
int[] và int[,] trong C#: Ai nhanh hơn

Hiểu được sự khác biệt giữa các loại mảng trong C# sẽ giúp bạn chọn cấu trúc dữ liệu chính xác cho mọi trường hợp.

Struct và class trong C#: Ai nhanh hơn
Trung Nguyen 09/10/2020
Struct và class trong C#: Ai nhanh hơn

Trong bài viết này, tôi sẽ so sánh sự khác biệt về hiệu suất giữa struct và class trong C#: Ai nhanh hơn.

Best practice cho performance trong C#
Trung Nguyen 03/10/2020
Best practice cho performance trong C#

Mục tiêu của bài viết này là cung cấp một danh sách không đầy đủ các code mẫu cần tránh, vì chúng rủi ro hoặc performance kém.

Đọc ghi file (File I/O) trong C#
Trung Nguyen 26/04/2020
Đọc ghi file (File I/O) trong C#

Hướng dẫn này sẽ giúp bạn tìm hiểu về đọc ghi file (File I/O) trong C# và sử dụng các lớp tiện ích để đọc ghi file.