Delegate trong C#

Một phương thức có thể có một hoặc nhiều tham số có kiểu dữ liệu khác nhau, nhưng nếu bạn muốn truyền một phương thức làm tham số thì sao? Làm thế nào để C# xử lý các chức năng gọi lại (callback) hoặc xử lý sự kiện? Câu trả lời là sử dụng delegate.

Delegate trong C#

Một delegate giống như một con trỏ đến một phương thức (gọi là con trỏ hàm trong C và C++). Delegate là kiểu tham chiếu và nó chứa tham chiếu tới một phương thức. Tất cả các delegate đều bắt nguồn từ lớp System.Delegate.

Một delegate có thể được khai báo bằng cách sử dụng từ khóa delegate, theo sau là chữ ký phương thức như dưới đây.

<access modifier> delegate <return type> <delegate_name>(<parameters>)
Chữ ký phương thức gồm tên phương thức, số lượng và kiểu dữ liệu của các tham số nhưng không bao gồm kiểu dữ liệu trả về. Tuy nhiên đối với delegate thì chứ ký phương thức bao gồm cả kiểu dữ liệu trả về.

Ví dụ sau khai báo một delegate Print.

public delegate void Print(int value);

Delegate Print có thể được sử dụng để trỏ đến bất kỳ phương thức nào có cùng kiểu dữ liệu trả về và số lượng/kiểu dữ liệu của tham số được khai báo với Print. Ví dụ sau minh họa cách sử dụng delegate Print.

class Program
{
    // declare delegate
    public delegate void Print(int value);

    static void Main(string[] args)
    {
        // Print delegate points to PrintNumber
        Print printDel = PrintNumber;
        
        // or
        // Print printDel = new Print(PrintNumber);
            
        printDel(100000);
        printDel(200);

        // Print delegate points to PrintMoney
        printDel = PrintMoney;

        printDel(10000);
        printDel(200);
    }

    public static void PrintNumber(int num)
    {
        Console.WriteLine("Number: {0,-12:N0}",num);
    }

    public static void PrintMoney(int money)
    {
        Console.WriteLine("Money: {0:C}", money);
    }
}

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

Number: 10,000
Number: 200
Money: $ 10,000.00
Money: $ 200.00

Trong ví dụ trên, chúng tôi đã khai báo delegate Print chấp nhận tham số kiểu int và trả về kiểu void. Trong phương thức Main(), một biến printDel kiểu Print được khai báo và gán tên phương thức PrintNumber.

Bây giờ, mỗi khi gọi thực thi biến printDel sẽ gọi tới phương thức PrintNumber. Tương tự, nếu biến printDel được gán cho phương thức PrintMoney, thì nó sẽ gọi phương thức PrintMoney.

Hình ảnh sau đây minh họa cho delegate.

Delegate trong C#

Tùy chọn, một đối tượng delegate có thể được khởi tạo bằng từ khóa new và chỉ định tên phương thức, như ví dụ dưới đây:

Print printDel = new Print(PrintNumber);

Gọi delegate

Delegate có thể được gọi như một phương thức vì nó là một tham chiếu đến một phương thức.

Khi gọi một delegate sẽ gọi tới phương thức mà delegate tham chiếu tới. Delegate có thể được gọi bằng hai cách: sử dụng toán tử () hoặc sử dụng phương thức ủy nhiệm Invoke() như dưới đây:

Print printDel = PrintNumber;
printDel.Invoke(10000);

//or
printDel(10000);

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

Number: 10000
Number: 10000

Truyền delegate như một tham số

Một phương thức có thể có một tham số kiểu delegate. Phương thức có thể gọi thực thi tham số delegate này. Ví dụ sau minh họa truyền delegate như một tham số của phương thức:

public static void PrintHelper(Print delegateFunc, int numToPrint)
{
    delegateFunc(numToPrint);
}

Trong ví dụ trên, phương thức PrintHelper có một tham số delegateFunc kiểu Print là một delegate. Nó gọi thực thi delegate này: delegateFunc(numToPrint).

Ví dụ sau đây cho thấy cách sử dụng phương thức PrintHelper:

class Program
{
    public delegate void Print(int value);


    static void Main(string[] args)
    {
        PrintHelper(PrintNumber, 10000);
        PrintHelper(PrintMoney, 10000);
    }

    public static void PrintHelper(Print delegateFunc, int numToPrint)
    {
        delegateFunc(numToPrint);
    }

    public static void PrintNumber(int num)
    {
        Console.WriteLine("Number: {0,-12:N0}",num);
    }

    public static void PrintMoney(int money)
    {
        Console.WriteLine("Money: {0:C}", money);
    }
}

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

Number: 10,000
Money: $ 10,000.00

Multicast delegate

Delegate có thể tham chiếu tới nhiều phương thức một lúc. Một delegate tham chiếu tới nhiều phương thức được gọi là multicast delegate. Toán tử + thêm một phương thức vào đối tượng delegate và toán tử - loại bỏ một phương thức hiện có ra khỏi một đối tượng delegate.

Ví dụ dưới đây sẽ minh họa về multicast delegate:

public delegate void Print(int value);

static void Main(string[] args)
{       
    Print printDel = PrintNumber;
    printDel += PrintHexadecimal;
    printDel += PrintMoney;

    printDel(1000);

    printDel -= PrintHexadecimal;
    printDel(2000);
}

public static void PrintNumber(int num)
{
    Console.WriteLine("Number: {0,-12:N0}",num);
}

public static void PrintMoney(int money)
{
    Console.WriteLine("Money: {0:C}", money);
}

public static void PrintHexadecimal(int dec)
{
    Console.WriteLine("Hexadecimal: {0:X}", dec);
}

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

Number: 1,000
Hexadecimal: 3EB
Money: $ 1,000.00
Number: 2,000
Money: $2,000.00

Như bạn có thể thấy trong ví dụ trên, delegate Print trở thành một multicast delegate vì nó tham chiếu đến ba phương thức: PrintNumber, PrintMoneyPrintHexadecimal. Vì vậy, khi gọi thực thi printDel sẽ gọi tuần tự tất cả các phương thức trên.

Delegate cũng được sử dụng với Event, anonymous method (phương thức ẩn danh), delegate Func, delegate Action.

Những điểm cần nhớ về delegate trong C#

  • Delegate là một con trỏ phương thức (nó tham chiếu đến phương thức). Nó là kiểu dữ liệu tham chiếu.
  • Một phương thức được gán cho delegate phải có cùng chữ ký với delegate.
  • Delegate có thể được gọi như một phương thức bình thường sử dụng toán tử () hoặc sử dụng phương thức Invoke().
  • Nhiều phương thức có thể được gán cho một delegate bằng toán tử +. Nó được gọi là multicast delegate.

Delegate Func trong C#

C# 3.0 có các kiểu generic delegate tích hợp sẵn như FuncAction do đó bạn không cần phải định nghĩa các delegate tùy chỉnh như ở trên.

Func là một generic delegate thuộc namespace System. Nó không có hoặc có nhiều tham số đầu vào và một tham số out. Tham số cuối cùng được coi là một tham số out.

Ví dụ: delegate Func có một tham số đầu vào và một tham số out được định nghĩa trong namespace System như dưới đây:

namespace System
{    
    public delegate TResult Func<in T, out TResult>(T arg);
}

Tham số cuối cùng trong ngoặc <> được xem là kiểu trả về và các tham số còn lại được coi là loại tham số đầu vào như trong hình dưới đây.

Func delegate trong C#

Delegate Func có hai tham số đầu vào và một tham số out sẽ được trình bày như dưới đây.

Func delegate trong C#

Biến sum dưới đây có kiểu delegate Func với hai tham số đầu vào của kiểu int và trả về giá trị của kiểu int:

Func<int, int, int> sum;

Bạn có thể gán bất kỳ phương thức nào có hai tham số int và trả về kiểu int  cho biến sum ở trên. Hãy xem ví dụ sau:

class Program
{
    static int Sum(int x, int y)
    {
        return x + y;
    }

    static void Main(string[] args)
    {
        Func<int,int, int> add = Sum;

        int result = add(10, 10);

        Console.WriteLine(result); 
    }
}

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

20

Một kiểu delegate Func có thể không có hoặc có đến 16 tham số đầu vào của các kiểu dữ liệu khác nhau. Tuy nhiên, nó phải bao gồm một tham số out cho kết quả. Ví dụ, delegate Func sau đây không có bất kỳ tham số đầu vào nào, nó chỉ có một tham số out.

Func<int> getRandomNumber;

Delegate Func với phương thức ẩn danh trong C#

Bạn có thể chỉ định một phương thức ẩn danh cho delegate Func bằng cách sử dụng từ khóa delegate.

Chúng ta sửa đổi lại ví dụ trên một chút, sử dụng phương thức ẩn danh thay thế cho phương thức Sum:

class Program
{
    static void Main(string[] args)
    {
        Func<int,int, int> add = delegate(int x, int y)
        {
            return x + y;
        };

        int result = add(10, 10);

        Console.WriteLine(result); 
    }
}

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

20

Delegate Func với biểu thức Lambda trong C#

Một delegate Func cũng có thể được sử dụng với biểu thức lambda, chúng ta lại tiếp tục sửa ví dụ trên như sau:

class Program
{
    static void Main(string[] args)
    {
        Func<int,int, int> add = (x, y) => x + y;

        int result = add(10, 10);

        Console.WriteLine(result); 
    }
}

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

20

Những điểm cần nhớ về delegate Func trong C#

  • Func là kiểu delegate được tích hợp sẵn trong C#.
  • Delegate Func phải trả về một giá trị.
  • Delegate Func có thể không có hoặc có đến 16 tham số đầu vào.
  • Delegate Func không cho phép tham số refout.
  • Delegate Func có thể được sử dụng với một phương thức ẩn danh hoặc biểu thức lambda .

Delegate Action trong C#

Action cũng là một loại delegate được định nghĩa trong namespace System. Delegate Action cũng tương tự như delegate Func ngoại trừ delegate Action không trả về giá trị. Nói cách khác, một delegate Action có thể được sử dụng với một phương thức có kiểu trả về là void.

Ví dụ minh họa sử dụng delegate Action:

static void ConsolePrint(int i)
{
    Console.WriteLine(i);
}

static void Main(string[] args)
{
    Action<int> print = ConsolePrint;
    print(10);
}

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

10

Bạn có thể khởi tạo một delegate Action bằng cách sử dụng từ khóa new hoặc bằng cách chỉ định trực tiếp một phương thức như dưới đây:

Action<int> printActionDel = ConsolePrint;

//Or

Action<int> printActionDel = new Action<int>(ConsolePrint);

Một đại biểu hành động có thể mất tới 16 tham số đầu vào thuộc các loại khác nhau.

Delegate Action với phương thức ẩn danh trong C#

Một phương thức ẩn danh cũng có thể được chỉ định cho một delegate Action:

static void Main(string[] args)
{
    Action<int> print = delegate(int i)
    {
        Console.WriteLine(i);
    };

    print(10);
}

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

10

Delegate Action với biểu thức Lambda trong C#

Biểu thức Lambda cũng có thể được sử dụng với delegate Action:

static void Main(string[] args)
{

    Action<int> print = i => Console.WriteLine(i);
       
    print(10);
}

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

10

Do đó, bạn có thể sử dụng bất kỳ phương thức nào không trả về giá trị cho delegate Action.

Những điểm cần nhớ về delegate Action trong C#

  • Delegate Action cũng giống như delegate Func ngoại trừ việc nó không trả về giá trị. Kiểu trả về phải là void.
  • Delegate Action có thể không có hoặc có đến 16 tham số đầu vào.
  • Delegate Action có thể được sử dụng với các phương thức ẩn danh hoặc biểu thức Lambda.

Ưu điểm của delegate Func và Action

  • Dễ dàng và nhanh chóng để định nghĩa các delegate.
  • Làm cho mã ngắn gọn và dễ đọc.
  • Kiểu dữ liệu tương thích trong suốt ứng dụng.

Delegate Predicate trong C#

Predicate cũng là một delegate như delegate FuncAction. Nó đại diện cho một phương thức chứa một tập hợp các tiêu chí và kiểm tra xem tham số đã truyền có đáp ứng các tiêu chí đó hay không. Một phương thức được gán cho delegate Predicate phải có một tham số đầu vào và trả về kiểu boolean.

Delegate Predicate được định nghĩa trong namespace System.

Cũng giống như các loại delegate khác, delegate Predicate cũng có thể được sử dụng với bất kỳ phương thức, phương thức ẩn danh hoặc biểu thức lambda nào.

static bool IsUpperCase(string str)
{
    return str.Equals(str.ToUpper());
}

static void Main(string[] args)
{
    Predicate<string> isUpper = IsUpperCase;

    bool result = isUpper("hello world!!");

    Console.WriteLine(result);
}

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

False

Delegate Predicate với phương thức ẩn danh trong C#

Một phương thức ẩn danh cũng có thể được gán cho loại đại biểu Dự đoán như dưới đây.

static void Main(string[] args)
{
    Predicate<string> isUpper = delegate(string s) 
    { 
        return s.Equals(s.ToUpper());
    };
    
    bool result = isUpper("hello world!!");
}

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

False

Delegate Predicate với biểu thức Lambda trong C#

Biểu thức lambda cũng có thể được gán cho kiểu delegate Predicate như dưới đây.

static void Main(string[] args)
{
    Predicate<string> isUpper = s => s.Equals(s.ToUpper());
    bool result = isUpper("hello world!!");
}

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

False

Những điểm cần nhớ về delegate Predicate trong C#:

  • Delegate Predicate có một tham số đầu vào và kiểu trả về boolean.
  • Phương thức ẩn danh và biểu thức Lambda có thể được gán cho delegate Predicate.
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.