openplanning

Hướng dẫn xử lý ngoại lệ trong C#

  1. Exception là gì?
  2. Phân cấp các ngoại lệ
  3. Bắt ngoại lệ thông qua try-catch
  4. Khối try-catch-finally
  5. Gói một Exception trong một Exception khác
  6. Một số ngoại lệ thông dụng

1. Exception là gì?

Trước hết chúng ta hãy xem một ví dụ minh họa sau:

Trong ví dụ này có một đoạn code lỗi nguyên nhân do phép chia cho 0. Việc chia cho 0 gây ra ngoại lệ: DivideByZeroException
HelloException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class HelloException
    {
        public static void Main(string[] args)
        { 
            Console.WriteLine("Three"); 
            // Phép chia này hoàn toàn không có vấn đề.
            int value = 10 / 2; 
            Console.WriteLine("Two");

            // Phép chia này cũng vậy
            value = 10 / 1; 
            Console.WriteLine("One"); 
            int d = 0;

            // Phép chia này có vấn đề, chia cho 0.
            // Lỗi đã xẩy ra tại đây.
            value = 10 / d;

            // Và dòng code dưới đây sẽ không được thực thi.
            Console.WriteLine("Let's go!"); 
            Console.Read();
        }
    }
}
Kết quả chạy ví dụ:
Bạn có thể thấy thông báo lỗi trên màn hình Console, thông báo lỗi rất rõ ràng, xẩy ra ở dòng thứ mấy trên code.
Three
Two
One
Hãy xem luồng đi của chương trình qua hình minh họa dưới đây.
  • Chương trình đã chạy hoàn toàn bình thường từ các bước (1),(2) cho tới (5)
  • Bước thứ (6) xẩy ra vấn đề khi chia cho 0.
  • Chương trình đã nhẩy ra khỏi hàm main, và dòng code thứ (7) đã không được thực hiện.
Chúng ta sẽ sửa code của ví dụ trên.
HelloCatchException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class HelloCatchException
    {
        public static void Main(string[] args)
        { 
            Console.WriteLine("Three"); 
            // Phép chia này không có vấn đề.           
            int value = 10 / 2;  
            Console.WriteLine("Two");

            // Phép chia này không có vấn đề.
            value = 10 / 1;  
            Console.WriteLine("One");  
            int d = 0; 
            try
            {
                // Phép chia này có vấn đề, chia cho 0.
                // Lỗi đã xẩy ra tại đây.
                value = 10 / d;

                // Dòng code này sẽ không được chạy.
                Console.WriteLine("Value =" + value);
            }
            catch (DivideByZeroException e)
            {
                // Các đoạn code trong catch được thực thi
                Console.WriteLine("Error: " + e.Message);  
                Console.WriteLine("Ignore..."); 
            } 
            // Dòng code này được thực thi.
            Console.WriteLine("Let's go!");  
            Console.Read();
        }
    } 
}
Và kết quả chạy ví dụ:
Three
Two
One
Error: Attempted to divide by zero.
Ignore...
Let't go!
Chúng ta sẽ giải thích bằng hình minh họa dưới đây về luồng đi của chương trình.
  • Các bước (1)-(6) hoàn toàn bình thường.
  • Ngoại lệ xẩy ra tại bước (7), vấn đề chia cho 0.
  • Lập tức nó nhẩy vào thực thi lệnh trong khối catch, bước (8) bị bỏ qua.
  • Bước (9), (10) được thực hiện.
  • Bước (11), (12) được thực hiện.

2. Phân cấp các ngoại lệ

Đây là mô hình sơ đồ phân cấp của Exception trong CSharp.
  • Class ở mức cao nhất là Exception
  • Hai class con trực tiếp là SystemExceptionAplicationException.
Các Exception sẵn có của CSharp thông thường được bắt nguồn (derived) từ SystemException. Trong khi đó các Exception của người dùng (lập trình viên) nên thừa kế từ ApplicationException hoặc từ các class con của nó.
Một số Exception thông dụng sẵn có trong CSharp.

Kiểu ngoại lệ

Mô tả

Exception

Lớp cơ bản của mọi ngoại lệ.

SystemException

Lớp cơ bản của mọi ngoại lệ phát ra tại thời điểm chạy của chương trình.

IndexOutOfRangeException

Được ném ra tại thời điểm chạy khi truy cập vào một phần tử của mảng với chỉ số không đúng.

NullReferenceException

Ném ra tại thời điểm chạy khi một đối tượng null được tham chiếu.

AccessViolationException

Ném ra tại thời điểm chạy khi tham chiếu vào vùng bộ nhớ không hợp lệ.

InvalidOperationException

Ném ra bởi phương thức khi ở trạng thái không hợp lệ.

ArgumentException

Lớp cơ bản cho các ngoại lệ liên quan tới đối số (Argument).

ArgumentNullException

Lớp này là con của ArgumentException, nó được ném ra bởi phương thức mà không cho phép thông số null truyền vào.

ArgumentOutOfRangeException

Lớp này là con của ArgumentException, nó được ném ra bởi phương thức khi một đối số không thuộc phạm vi cho phép truyền vào nó.

ExternalException

Lớp cơ bản cho các ngoại lệ xẩy ra hoặc đến từ môi trường bên ngoài.

COMException

Lớp này mở rộng từ ExternalException, ngoại lệ đóng gói thông tin COM.

SEHException

Lớp này mở rộng từ ExternalException, nó tóm lược các ngoại lệ từ Win32.

3. Bắt ngoại lệ thông qua try-catch

Chúng ta viết một exception thừa kế từ class ApplicationException.
AgeException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
   class AgeException : ApplicationException
   {
       public AgeException(String message)
           : base(message)
       {
       }
   }
   class TooYoungException : AgeException
   {
       public TooYoungException(String message)
           : base(message)
       {
       }
   }
   class TooOldException : AgeException
   {
       public TooOldException(String message)
           : base(message)
       {
       }
   }
}
Và class AgeUtils có method tĩnh dùng cho việc kiểm tra tuổi.
AgeUtils.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class AgeUtils
    {
        // Phương thức này làm nhiệm vụ kiểm tra tuổi.
        // Nếu tuổi nhỏ hơn 18 method sẽ ném ra ngoại lệ TooYoungException
        // Nếu tuổi lớn hơn 40 method sẽ ném ra ngoại lệ TooOldException
        public static void checkAge(int age)
        {
            if (age < 18)
            {
                // Nếu tuổi nhỏ hơn 18, ngoại lệ sẽ được ném ra
                // Method này kết thúc tại đây.
                throw new TooYoungException("Age " + age + " too young");
            }
            else if (age > 40)
            {
                // Nếu tuổi lớn hơn 40, ngoại lệ sẽ được ném ra.
                // Method này kết thúc tại đây.
                throw new TooOldException("Age " + age + " too old");
            }
            // Nếu tuổi nằm trong khoảng 18-40.
            // Đoạn code này sẽ được chạy.
            Console.WriteLine("Age " + age + " OK!");
        }
    } 
}
TryCatchDemo1.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class TryCatchDemo1
    {
        public static void Main(string[] args)
        {
            // Bắt đầu tuyển dụng ...
            Console.WriteLine("Start Recruiting ...");

            // Kiểm tra tuổi của bạn.
            Console.WriteLine("Check your Age");
            int age = 50; 
            try
            { 
                AgeUtils.checkAge(age); 
                Console.WriteLine("You pass!"); 
            }
            catch (TooYoungException e)
            {
                // Thông báo về ngoại lệ "quá trẻ" ..
                Console.WriteLine("You are too young, not pass!");
                Console.WriteLine(e.Message); 
            }
            catch (TooOldException e)
            {
                // Thông báo về ngoại lệ "quá nhiều tuổi" ..
                Console.WriteLine("You are too old, not pass!");
                Console.WriteLine(e.Message); 
            } 
            Console.Read(); 
        }
    } 
}
Chạy ví dụ:
Start Recruiting ...
Check your Age
You are too old, not pass!
Age 50 too old
Ví dụ dưới đây, chúng ta sẽ bắt các ngoại lệ thông qua ngoại lệ ở cấp cao hơn. Ở cấp cao hơn nó sẽ tóm được ngoại lệ đó và tất cả các ngoại lệ con.
TryCatchDemo2.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class TryCatchDemo2
    {
        public static void Main(string[] args)
        {
            // Bắt đầu tuyển dụng ...
            Console.WriteLine("Start Recruiting ...");

            // Kiểm tra tuổi của bạn.
            Console.WriteLine("Check your Age");
            int age = 15; 
            try
            {
                // Chỗ này có thể ném ra (throw) ngoại lệ TooOldException,
                // hoặc TooYoungException
                AgeUtils.checkAge(age); 
                Console.WriteLine("You pass!"); 
            }
            // Nếu có ngoại lệ xẩy ra, kiểu AgeException
            // Khối catch này sẽ được chạy.
            catch (AgeException e)
            { 
                Console.WriteLine("Your age invalid, you not pass");
                Console.WriteLine(e.Message); 
            } 
            Console.Read();
        }
    } 
}
Chạy ví dụ:
Start Recruiting ...
Check your Age
Your age invalid, you not pass
Age 15 too young

4. Khối try-catch-finally

Trên kia chúng ta đã làm quen với việc bắt lỗi thông qua khối try-catch. Việc xử lý ngoại lệ đầy đủ là try-catch-finally.
try { 
  // Làm gì đó tại đây 
} catch (Exception1 e) { 
  // Làm gì đó tại đây 
} catch (Exception2 e) { 
  // Làm gì đó tại đây 
} finally { 
  // Khối finally luôn luôn được thực thi
  // Làm gì đó tại đây. 
}
TryCatchFinallyDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class TryCatchFinallyDemo
    {
        public static void Main(string[] args)
        { 
            String text = "001234A2"; 
            int value = toInteger(text); 
            Console.WriteLine("Value= " + value); 
            Console.Read();  
        } 
        public static int toInteger(String text)
        {
            try
            { 
                Console.WriteLine("Begin parse text: " + text); 
                // Tại đây có thể phát sinh ngoại lệ FormatException
                int value = int.Parse(text); 
                return value; 
            }
            catch (FormatException e)
            {
                // Trong trường hợp 'text' không phải là số.
                // Khối catch này sẽ được thực thi.
                Console.WriteLine("Number format exception: " + e.Message); 
                return 0; 
            }
            finally
            { 
                Console.WriteLine("End parse text: " + text); 
            } 
        }
    } 
}
Chạy ví dụ:
Begin parse text: 001234A2
Number format exception: Input string was not in a correct format.
End parse text: 001234A2
Value= 0
Đây là luồng đi (flow) của chương trình. Khối finally luôn được thực thi.

5. Gói một Exception trong một Exception khác

Chúng ta cần một vài class tham gia vào ví dụ này:
  • Person: Mô phỏng một người tham gia tuyển dụng vào công ty với các thông tin
    • Tên, tuổi, giới tính.
  • GenderException: Ngoại lệ giới tính.
  • ValidateException: Ngoại lệ đánh giá thí sinh.
  • ValidateUtils: Class có method tĩnh đánh giá thí sinh đủ tiêu chuẩn không.
    • Tiêu chuẩn là những người độ tuổi 18-40
    • Và là Nam.
Person.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
  class Person
  {
      public static readonly string MALE = "male";
      public static readonly string FEMALE = "female";
      private string name;
      private string gender;
      private int age;
      public Person(string name, string gender, int age)
      {
          this.name = name;
          this.gender = gender;
          this.age = age;
      }
      public string GetName()
      {
          return name;
      }
      public string GetGender()
      {
          return gender;
      }
      public int GetAge()
      {
          return age;
      }
  }
}
GenderException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
   class GenderException : ApplicationException
   {
       public GenderException(String message)
           : base(message)
       { 
       }
   }
}
Lớp ValidateException bao lấy một Exception khác.
ValidateException.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class ValidateException : ApplicationException
    { 
        // Gói (wrap) một Exception trong một Exception.
        public ValidateException(Exception e) : base("Something invalid", e)
        { 
        }
    } 
}
ValidateUtils.java
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{ 
    class ValidateUtils
    {
        // Phương thức kiểm tra một người tham gia tuyển dụng.
        public static void CheckPerson(Person person)
        {
            try
            {
                // Kiểm tra tuổi.
                // Hợp lệ là trong khoảng 18-40
                // Method này có thể ném ra TooOldException,TooYoungException.     
                AgeUtils.checkAge(person.GetAge()); 
            }
            catch (Exception e)
            {
                // Nếu không hợp lệ
                // Gói ngoại lệ này bởi ValidateException, và throw.
                throw new ValidateException(e); 
            } 
            // Nếu người đó là Nữ, nghĩa là không hợp lệ.
            if (person.GetGender() == Person.FEMALE)
            { 
                GenderException e = new GenderException("Do not accept women");
                throw new ValidateException(e); 
            }
        }
    } 
}
WrapperExceptionDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class WrapperExceptionDemo
    {
        public static void Main(string[] args)
        { 
            // Một ứng viên tham gia tuyển dụng.
            Person person = new Person("Marry", Person.FEMALE, 20); 
            try
            {
                // Ngoại lệ có thể xẩy ra tại đây.
                ValidateUtils.CheckPerson(person); 
            }
            catch (ValidateException wrap)
            { 
                // Lấy ra nguyên nhân thực sự.
                // Mà có thể là TooYoungException, TooOldException, GenderException.
                Exception cause = wrap.GetBaseException(); 
                if (cause != null)
                {
                    Console.WriteLine("Message: " + wrap.Message);
                    Console.WriteLine("Base Exception Message: " + cause.Message);
                }
                else
                {
                    Console.WriteLine("Message: " + wrap.Message);

                }
            } 
            Console.Read();
        }
    } 
}
Chạy ví dụ:
Age 20 OK!
Message: Something invalid
Base Exception Message: Do not accept women

6. Một số ngoại lệ thông dụng

Bây giờ bạn có thể xem một vài ví dụ với các ngoại lệ thông dụng.
NullReferenceException
Đây là một trong các ngoại lệ thông dụng nhất, và hay gây ra lỗi cho chương trình. Ngoại lệ được ném ra khi bạn gọi hàm hoặc truy cập vào các trường của một đối tượng chưa được khởi tạo (đối tượng null).
NullReferenceExceptionDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class NullReferenceExceptionDemo
    {
        // Ví dụ đây là một phương thức có thể trả về chuỗi null.
        public static string GetString()
        {
            if (1 == 2)
            {
                return "1==2 !!";
            }
            return null;
        } 
        public static void Main(string[] args)
        {
            // Đây là một đối tượng có tham chiếu khác null.
            string text1 = "Hello exception";

            // Lấy độ dài của chuỗi.
            int length = text1.Length; 
            Console.WriteLine("Length text1 = " + length);

            // Đây là một đối tượng có tham chiếu (reference) null.
            String text2 = GetString(); // text2 = null.

            // Lấy độ dài của chuỗi.
            // NullReferenceException sẽ xẩy ra tại đây. 
            length = text2.Length; // ==> Runtime Error! 
            Console.WriteLine("Finish!"); 
            Console.Read();
        }
    } 
}
Trong thực tế giống việc xử lý các ngoại lệ khác, bạn có thể sử dụng try-catch để bắt ngoại lệ này mà xử lý. Tuy nhiên, đó là cách máy móc, thông thường chúng ta nên kiểm tra để đảm bảo rằng đối tượng là khác null trước khi sử dụng nó.

Bạn có thể sửa code trên giống dưới đây, để tránh NullReferenceException:
// Đây là một đối tượng có tham chiếu null.
String text2 = GetString(); // ==> return null

// Kiểm tra để đảm bảo rằng 'text2' là khác null,
// Thay vì sử dụng try-catch.
if (text2 != null)
{
      length = text2.Length;
}
IndexOutOfRangeException
Đây là ngoại lệ nó được ném ra khi bạn cố truy cập vào phần tử có chỉ số không hợp lệ trên mảng. Chẳng hạn mảng có 10 phần tử, mà bạn lại truy cập vào phần tử có chỉ số 20.
IndexOutOfRangeExceptionDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ExceptionTutorial
{
    class IndexOutOfRangeExceptionDemo
    {
        public static void Main(string[] args)
        { 
            String[] strs = new String[] { "One", "Two", "Three" }; 
            // Truy cập vào phần tử tại chỉ số 0.
            String str1 = strs[0]; 
            Console.WriteLine("String at 0 = " + str1);

            // Truy cập vào phần tử có chỉ số 5
            // IndexOutOfRangeException xẩy ra tại đây.
            String str2 = strs[5]; 
            Console.WriteLine("String at 5 = " + str2); 
            Console.Read(); 
        }
    }
}
Để tránh IndexOutOfRangeException bạn nên kiểm tra mảng thay vì sử dụng try-catch.
if (strs.length > 5)
{
     String str2 = strs[5];
     Console.WriteLine("String at 5 = " + str2);
}
else
{
     Console.WriteLine("No elements with index 5");
}