Delegate, Anonymous Method và Lambda Expression Trong C#

Delegate, Anonymous Method và Lambda Expression Trong C#

Khi bạn học lập trình C/C++ có lẽ bạn đã nghe đến con trỏ hàm và lợi ích của nó. Có lẽ chính vì điều này mà các kỹ sư Microsoft đã đưa vào C# một khái niệm tương tự là delegate. Vậy delegate là gì và sử dụng chúng như nào? Trong bài viết này mình sẽ chia sẻ những hiểu biết của mình về delegate để giải đáp câu hỏi trên.

Ví dụ được sử dụng trong suốt bài viết là class MyDelegateTest và được gọi trong hàm main như sau:

static void Main(string[] args) {
     var delegateTest = new MyDelegateTest();
     delegateTest.Run();
}

1. Delegate là gì?

Delegate là kiểu dữ liệu tham chiếu.

  • Delegate tham chiếu tới một hoặc nhiều hàm. Trong đó các hàm phải có cùng một định dạng (cùng kiểu dữ liệu trả về và cùng tham số đầu vào).
  • Tham chiếu có thể thay đổi trong quá trình ứng dụng đang chạy.
  • Delegate không được phép kế thừa.

1.1. Khai báo Delegate

          [Access modifier] delegate <Return type> Name([Parameter]);


Trong đó:

     + [Access modifier] có thể là private, internal, protected hoặc public.
     + delegate là từ khóa bắt buộc.
     + <Return type> kiểu dữ liệu trả về.
     + [Parameter] tham số truyền vào hàm. Có thể có hoặc không có tham số truyền vào.

Ví dụ:

private delegate int Add(int x, int y);
private delegate void WriteMessage(string message);

1.2. Cách khởi tạo

Có 2 cách khởi tạo như ví dụ sau:

public class MyDelegateTest
    {
        private delegate int Add(int x, int y);

        public void Run()
        {
            //  Cách khởi tạo 1
            var firstCalc = new Add(DelegateAdd);

            var firstVal = firstCalc(10, 20);
            Console.WriteLine(firstVal);    //  Kết quả: 30

            firstVal = firstCalc.Invoke(10, 20);
            Console.WriteLine(firstVal);    //  Kết quả: 30

            //  Cách khởi tạo 2
            Add secondCalc = DelegateAdd;
            var secondVal = secondCalc(10, 20);
            Console.WriteLine(secondVal);    //  Kết quả: 30

            secondVal = secondCalc.Invoke(10, 20);
            Console.WriteLine(secondVal);    //  Kết quả: 30
        }

        private int DelegateAdd(int x, int y)
        {
            return x + y;
        }
    }

Trong ví dụ trên, bạn hãy lưu ý 2 dòng sau:

firstVal = firstCalc.Invoke(10, 20);
secondVal = secondCalc.Invoke(10, 20);

Để đảm bảo sử dụng được hàm Invoke, bạn cần kiểm tra đối tượng đã được khởi tạo chưa và 2 dòng trên sẽ được thay thế bằng:

if (firstCalc != null) firstVal = firstCalc.Invoke(10, 20);
if (secondCalc != null) secondVal = secondCalc.Invoke(10, 20);

1.3. Áp dụng

Delegate được sử dụng nhiều nhất trong:

  • Lập trình hướng sự kiện - lập trình Window Forms, WPF
  • LINQ
  • Validate dữ liệu

a) Lập trình hướng sự kiện – lập trình Window Forms, WPF

Nếu ai đã từng làm việc với Window Forms chắc cũng không lạ gì với những dòng sau:

var cmdClick = new System.Windows.Forms.Button();
cmdClick.Click += new System.EventHandler(cmdClick_Click);
cmdClick.Enter += new System.EventHandler(cmdClick_Enter);

b) LINQ

Mình sẽ không đi chi tiết vào LINQ mà chỉ đưa ra 1 ví dụ cụ thể như sau: có danh sách số nguyên >= 0. Bài toán đặt ra là tìm các số chia hết cho 2.

var args = new List<int> { 0, 1, 3, 4, 6, 8 };
var d = args.Where(f => f % 2 == 0);
foreach (var item in d) Console.Write(item.ToString() + " ");   //  Kết quả: 0 4 6 8


c) Validate dữ liệu

Đối với cá nhân mình thì mình còn hay sử dụng trong trường hợp cần validate dữ liệu với nhiều trường hợp.

    public class User
    {
        public string Username { get; set; }
        public string Password { get; set; }
    }

    public class MyDelegateTest
    {
        private delegate int Validate(User user);

        public void Run()
        {
            var list = new List<Validate>()
            {
                UsernameIsNotNull,
                UsernameHasBeenAtLeast6Chars,
                PasswordIsNotNull
            };

            var user = new User
            {
                Username = "XXX",
                Password = "YYY"
            };

            var errCode = 0;
            foreach (var v in list)
            {
                errCode = v(user);
                if (errCode >= 0) continue;

                break;
            }
            if (errCode == 0) Console.WriteLine("Ok.");
            else Console.WriteLine("Not ok.");
        }

        private int PasswordIsNotNull(User user)
        {
            return string.IsNullOrEmpty(user.Password) ? -1 : 0;
        }

        private int UsernameHasBeenAtLeast6Chars(User user)
        {
            return !string.IsNullOrEmpty(user.Username) && user.Username.Length >= 6 ? 0 : -1;
        }

        private int UsernameIsNotNull(User user)
        {
            return string.IsNullOrEmpty(user.Username) ? -1 : 0;
        }
    }

1.4. Phân loại

Có 2 loại delegate:

  • Tham chiếu đến 1 hàm
  • Tham chiếu đến nhiều hàm

Trong trường hợp tham chiếu đến nhiều hàm, chúng ta có thể sử dụng toán tử +/- để thêm/bớt tham chiếu. Các hàm sẽ được thực hiện theo thứ tự thêm/bớt tham chiếu.

    public class MyDelegateTest
    {
        private delegate void Add(int x, int y);

        public void Run()
        {
            var firstAdd = new Add(FirstAdd);
            var secondAdd = new Add(SecondAdd);

            //  Output:
            //      Delegate 1: 30
            //      Delegate 2: 30
            var add1 = firstAdd + secondAdd;
            add1(10, 20);

            //  Output:
            //      Delegate 2: 30
            //      Delegate 1: 30
            var add2 = secondAdd + firstAdd;
            add2(10, 20);
        }

        private void SecondAdd(int x, int y)
        {
            Console.WriteLine("Delegate 2: {0}", x + y);
        }

        private void FirstAdd(int x, int y)
        {
            Console.WriteLine("Delegate 1: {0}", x + y);
        }
    }


Trong trường hợp delegate có giá trị trả về, khi sử dụng thêm/bớt tham chiếu thì kết quả trả về là kết quả trả về của hàm được thêm tham chiếu cuối cùng.

Lưu ý: đối với delegate có kết quả trả về thì việc bớt không có tác dụng.

    public class MyDelegateTest
    {
        private delegate int Add(int x, int y);

        public void Run()
        {
            var firstAdd = new Add(FirstAdd);
            var secondAdd = new Add(SecondAdd);
            var thirdAdd = new Add(ThirdAdd);

            var add1 = firstAdd + secondAdd;
            var rs = add1(10, 20);
            Console.WriteLine(rs);  //  Output: -10

            var add2 = secondAdd + firstAdd;
            rs = add2(10, 20);
            Console.WriteLine(rs);  //  Output: 30

            var add3 = firstAdd + secondAdd - thirdAdd;
            rs = add3(10, 20);
            Console.WriteLine(rs);  //  Output: 30
        }

        private int ThirdAdd(int x, int y)
        {
            return 2 * x + 2 * y;
        }

        private int SecondAdd(int x, int y)
        {
            return x - y;
        }

        private int FirstAdd(int x, int y)
        {
            return x + y;
        }
    }


Generic delegate

Từ bản C# 2.0, các kỹ sư Microsoft đã đưa kiểu generic vào sử dụng và generic delegate cũng không phải là ngoại lệ. Trong phạm vi bài viết này, mình giả định rằng các bạn đã biết về kiểu generic nên mình chỉ đưa ra ví dụ để các bạn có thể thấy được cách khai báo generic delegate.

Ví dụ: generic delegate trong đó kiểu trả về là T; có 2 tham số truyền vào có kiểu là T1 và T2.

public class MyDelegateTest
    {
        private delegate T Add<T1, T2, T>(T1 x, T2 y);

        public void Run()
        {
            var intAdd = new Add<int, int, int>(IntAdd);
            var rsInt = intAdd(10, 20);
            Console.WriteLine(rsInt);   //  Output: 30

            var doubleAdd = new Add<double, double, double>(DoubleAdd);
            var rsDouble = doubleAdd(10.5d, 20.8d);
            Console.WriteLine(rsDouble);    //  Output: 31.3
        }

        private double DoubleAdd(double x, double y)
        {
            return x + y;
        }

        private int IntAdd(int x, int y)
        {
            return x + y;
        }
    }


Từ C# 3.0, các kỹ sư Microsoft định nghĩa sẵn 3 loại generic delegate Action, Func Predicate. Cách khai báo và sử dụng của 3 generic delegate trên sẽ được mô tả như bảng so sánh dưới đây (so với delegate):

Delegate Action Func Predicate
delegate void Name(T1 x1, T2 x2, …, Tn xn); Action<T1, T2, .., Tn> Name;
delegate Tn Name(T1 x1, T2 x2, …, Tn-1 xn-1); Func< T1, T2, .., Tn> Name;
delegate bool Name(T1 x1); Predicate<T1, bool>
Giải thích Tương đương với delegate nhưng giá trị trả về là void. Tương đương với delegate, giá trị trả về là kiểu Tn. Tương đương với delegate chỉ có 1 tham số có kiểu là T1 và kiểu trả về là bool.
Lưu ý Số lượng tham số truyền vào tối thiểu là 0 và tối đa là 16. Số lượng tham số truyền vào tối thiểu là 0 và tối đa là 16. Chỉ có 1 tham số truyền vào.


Ví dụ về Action

public class MyDelegateTest
    {
        public void Run()
        {
            Action<int> display = OnDisplay;

            display(100);
        }

        private void OnDisplay(int obj)
        {
            Console.WriteLine(obj);
        }
    }


Ví dụ về Func

    public class MyDelegateTest
    {
        public void Run()
        {
            Func<int, int, bool> check = OnPass;

            var ok = check(10, 20);
            Console.WriteLine(ok);  //  Output: False

            ok = check(20, 10);
            Console.WriteLine(ok);  //  Output: True
        }

        private bool OnPass(int x, int y)
        {
            return x >= y;
        }
    }


Ví dụ về Predicate

    public class MyDelegateTest
    {
        public void Run()
        {
            Predicate<int> check = OnCheck;

            var ok = check(10);
            Console.WriteLine(ok);  //  Output: True

            ok = check(15);
            Console.WriteLine(ok);  //  Output: False
        }

        private bool OnCheck(int x)
        {
            return x % 2 == 0;
        }
    }


Phương thức vô danh (Anonymous method) và Biểu thức lambda (Lamda Expression)

1. Phương thức vô danh (Anonymous method)

Trong các ví dụ mình đưa ra ở trên, các bạn đều thấy rằng mình phải khai báo tường minh hàm để delegate tham chiếu tới (hàm Add và Validate) – nhằm tái sử dụng lại hàm đó và giúp code dễ đọc hơn. Trong nhiều trường hợp, hàm đó chỉ được sử dụng 1 lần, nếu viết tường minh thì code quá dài dòng, vì vậy các kỹ sư của Microsoft đã đưa cách viết ngắn gọn hơn vào sử dụng với cái tên là phương thức vô danh (hàm không tên).

public class MyDelegateTest
    {
        private delegate void Display(string message);

        public void Run()
        {
            Display check = delegate(string message) {
                Console.WriteLine(message);
            };

            check("Anonymous method."); //  Output: Anonymous method.
        }
    }


2. Biểu thức lambda

Mặc dù đã có phương thức nặc danh để rút ngắn số lần gõ phím nhưng các kỹ sư Microsoft vẫn tiếp tục cải tiến và thêm vào biểu thức lambda (từ C# 3.0). Biểu thức lambda sử dụng toán tử “=>” để phân chia các tham số truyền vào và hàm tham chiếu.

Quay lại ví dụ trên, chúng ta sửa lại code như sau:

public class MyDelegateTest
    {
        private delegate void Display(string message);

        public void Run()
        {
            Display check = (message) => {
                Console.WriteLine(message);
            };

            check("Anonymous method."); //  Output: Anonymous method.
        }
    }


Kết luận

Trên đây là những hiểu biết của mình về Delegate, Anonymous method và Lambda expression. Rất mong nó sẽ có ích cho các bạn và sự ủng hộ từ các bạn. Nếu có bất kỳ ý kiến thắc mắc hoặc không hiểu rõ, hãy để lại comment để cùng nhau thảo luận nhé.