Tin tức mới

Delegates và Events trong C#

Có một thực tế đang xảy ra đó là nhiều người rất hay nhầm lẫn giữa hai khái niệm Delegate và Event, đặc biệt là đối với những người mới làm quen với ngôn ngữ C# nói riêng và các ngôn ngữ khác thuộc nền tảng .NET nói chung; Và ngay cả đối với những lập trình viên đã có thâm niên thì cũng không phải dễ dàng gì có thể phân biệt được rạch ròi sự khác nhau giữa hai khái niệm này. Bài viết này sẽ đưa ra một số phân tích, so sánh, qua đó mong muốn có thể làm rõ hơn bản chất của hai khái niệm Delegate và Event, và giúp chúng ta thấy rõ hơn được sự khác nhau cũng như mối quan hệ giữa chúng.

1. Delegate

Theo như định nghĩa được đưa ra trên MSDN thì delegate là một kiểu chứa tham chiếu đến một hoặc nhiều phương thức (method), có thể là phương thức của lớp (class’s method) hoặc là phương thức của đối tượng (object’s method). Đối với những ai đã làm việc với ngôn ngữ C hoặc C++ thì có thể thấy delegate gần giống với khái niệm Con trỏ hàm (funtion’s pointer). Tuy nhiên, hiện nay trong nhiều tài liệu vẫn hay dùng chung từ delegate để chỉ đến hai khái niệm khác nhau, gây ra sự mập mờ, đó là Kiểu delegate (delegate type) và Thực thể delegate (delegate instance, hay còn có thể gọi là Đối tượng delegate); Do đó, trong bài viết này chúng ta sẽ không sử dụng từ delegate với nghĩa chung như vậy mà sẽ phân biệt rạch ròi giữa kiểu delegate và đối tượng delegate.

Một kiểu delegate được khai báo bằng cách sử dụng từ khóa delegate đi kèm theo với kiểu dữ liệu trả về, tên và danh sách tham số của nó. Để một delegate có thể chứa tham chiếu đến một phương thức khác (hoặc đôi khi ta có thể nói là: delegate đại diện cho một phương thức) thì cả delegate và phương thức mà nó tham chiếu đến phải có cùng nguyên mẫu (signature), có nghĩa là cả hai đều phải có chung kiểu dữ liệu trả về và danh sách tham số.

Ta sẽ xét một ví dụ mà ở đó có một delegate với tên là Calculation được khai báo, kiểu delegate này có kiểu dữ liệu trả về là int, nhận vào hai tham số thuộc kiểu int; Một đối tương của delegate Calculation được tạo ra và tham chiếu đến phương thức Add() của lớp Program, chúng ta có thể để ý rằng cả kiểu delegate Calculation và phương thức Add() có kiểu dữ liệu trả về và danh sách tham số giống nhau.

namespace Test

{
    public delegate int Calculation(int a, int b);
    class Program
    {
        static int Add(int a, int b){
            return a + b;
        }
        static void Main(string[] args)
        {
            Calculation ca = new Calculation(Add);
            int c = ca(5, 4);
            Console.WriteLine(“c = {0}”, c);
            Console.ReadLine();
        }
    }
}
 Một cách dùng khác của delegate đó là kỹ thuật sử dụng delegate để truyền một phương thức vào như là tham số của một phương thức khác, ta cùng xem ví dụ sau:

using System; namespace Test {

public delegate int Calculation(int a, int b); class Program {

static int Add(int a, int b) {

return a + b;

} static int Sub(int a, int b) {

return a – b;

}

static int Calculate(int a, int b, Calculation cal) {

return cal(a, b);

} static void Main(string[] args) {

int c = Calculate(5, 4, Add); Console.WriteLine(“c = {0}”, c); int d = Calculate(5, 4, Sub); Console.WriteLine(“d = {0}”, d); Console.ReadLine();

}

}

}

Ở ví dụ trên thì ta đã tạo ra một phương thức Calculate() có hai tham số kiểu int và một tham số thuộc kiểu delegate Calculation, với cách làm này thì sau này, khi đem ra sử dụng hàm Calculate() thì ta hoàn toàn có thể tự do truyền vào bất cứ hàm nào để thực hiện tính toán, miễn sao hàm đó phải có cùng nguyên mẫu với kiểu delegate Calculation. Cách làm này còn được sử dụng để triển khai kỹ thuật callback, để tránh làm cho bài viết quá dài thì những ai quan tâm tới vấn đề này, xin vui lòng tìm hiểu thêm ở đây và ở đây.

2. Event

Điều đầu tiên mà chúng ta cần khẳng định đó là: Event không phải là đối tượng delegate. Để giúp chúng ta có thể thấy rõ được bản chất của  event cũng như sự khác nhau giữa event và delegate thì chúng ta sẽ mượn hai khái niệm khác cũng được sử dụng rất phổ biến trong ngôn ngữ C# đó là FieldProperty. Nhìn từ bên ngoài thì Property có vẻ rất giống với Field nhưng bản chất thì Property không phải là Field; Field là một biến, nó chứa dữ liệu, còn Property là một khối lệnh, nó giống với Method hơn. Trong một Property thì ta có hai khối lệnh có thể được khai báo đó là getset còn Field thì chỉ được khai báo trên một dòng. Thông thường thì người ta sử dụng các Property như là cách để truy xuất đến các Field của lớp đó, như trong ví dụ sau:

class Customer {

private string name;

public string Name {

get {

return this.name;

} set{

this.name = value;

}

}

}

Nhưng đôi lúc ta cũng có thể tạo ra các Property mà không dính dáng gì đến Field cả:

class TimeUtil {

public DateTime CurrentTime{

get {

return DateTime.Now;

}

}

}

 Và với sự cải tiến không ngừng của C# nói riêng và .NET nói chung thì bây giờ ta có thể khai báo Property với dạng như sau:

class Customer {

public string Name {

get;

set;

}

}

Nhìn khối lệnh trên thì thật đơn giản phải không, và hình như là không có bất cứ một Field nào được khai báo trong đó cả, nhưng thực tế thì nó lại khác. Trình biên dịch sẽ tự động tạo ra một field có kiểu string rồi sau đó sử dụng nó để triển khai property Name, và như vậy, đoạn code ở trên thực ra chỉ là một phiên bản rút gọn của lớp Customer mà chúng ta đã xem xét trước đó. Bây giờ thì chúng ta đã nắm rõ ràng hơn về bản chất của Field và Property, chúng ta quay lại trường hợp của Delegate và Event, có thể nói rằng mối liên quan giữa Delegate và Event không khác với mối liên quan giữa Field và Property là mấy. Thông thường, để xây dựng một Event thì ta thực hiện như ví dụ sau:

public delegate void ErrorNotification(string message);

class MyMachine{

public event ErrorNotification Notify;

public void ReportError(string error) {

if (Notify != null)

Notify(error);

}

}

và sử dụng:

static void Main(string[] args) {

MyMachine machine = new MyMachine();

machine.Notify += new ErrorNotification(PrintString);

machine.ReportError(“Some bug ocurred”);

Console.ReadLine();

}

static void PrintString(string msg){

Console.WriteLine(msg);

}

Ở ví dụ trên thì ta đã khai báo một kiểu delegate có tên là ErrorNotification, sau đó sử dụng nó để tạo một event cho lớp MyMachine với tên là Notify. Nhìn vào đoạn code đó thì sẽ có nhiều người cho rằng event được tạo ra bằng cách tạo một đối tượng mới của kiểu delegate ErrorNotification, nhưng thực tế thì không hề đơn giản như vậy. Một Event thực chất là một khối lệnh, tương tự như Property, nó cũng có hai khối lệnh con được khai báo trong đó là addremove; khối lệnh add được dùng để đăng ký một phương thức với event, còn khối lệnh remove được dùng để gỡ bỏ một phương thức ra khỏi event đó. Đây chính là cách khai báo một event ngắn gọn, và dễ gây ra nhầm lẫn, ta thử viết lại đoạn code của lớp MyMachineở dạng nguyên thủy của nó:

class MyMachine{

private ErrorNotification notify;//Delegate instance

//Event declaration

public event ErrorNotification Notify{

add{

this.notify += value;

}

remove{

this.notify -= value;

}

}

public void ReportError(string error) {

if (notify != null)

notify(error);

}

}

và đem ra sử dụng:

static void Main(string[] args) {

MyMachine machine = new MyMachine();

machine.Notify += new ErrorNotification(PrintString);

machine.ReportError(“Some bug ocurred”);

Console.ReadLine();

}

static void PrintString(string msg){

Console.WriteLine(msg);

}

Hãy chú ý đến cái cách mà chúng ta đã khai báo event Notify của lớp MyMachine với hai khối lệnh addremove bên trong. Và với cách triển khai cụ thể như vậy, ta có thể dễ dàng thấy rằng Event thực chất là cái cách mà chúng ta sử dụng để truy xuất đến một private delegate ở bên trong một lớp; Ở đây thì value có kiểu là ErrorNotification . Bây giờ thì bạn đã tin cái câu khẳng định trước đó chưa? Event không phải là đối tượng delegate. Và chắc bạn cũng đã thông cảm được với việc tại sao chúng ta lại dựa vào Field và Property để có dẫn dắt vào vấn đề tương quan giữa Delegate và Event, bởi vì thực sự nó có sự tương đồng nhau mà. Tóm lại thì cách khai báo event có dạng:

public event EventHandler MyEvent

chỉ là một cách viết ngắn gọn, và nó còn có tên gọi là field-like event.

3. Mấu chốt của vấn đề

Tại sao lại cần thiết phải có cả hai khái niệm là delegate và event? Câu trả lời ở đây chính là Encapsulation (tính bao gói).

Đâu là cách để đăng ký một event cho một đối tượng nào đó? Có ba sự lựa chọn:

– Một là, tạo một đối tượng delegate với mức truy cập là public, cách này thì chắc chắn là bị loại đầu tiên, vì nó không đảm bảo được sự đóng gói của đối tượng.

– Cách thứ 2, tạo một đối tượng delegate có mức truy cập là private và đồng thời tạo ra một property để truy xuất đến đối tượng delegate đó; cách này thì khá hơn cách trước, nhưng nhược điểm của nó là ta có thể sẽ có khó khăn khi muốn đăng ký nhiều phương thức với sự kiện này, bởi vì nó chỉ có thể nhận vào một phương thức mà thôi, ví dụ: someInstance.MyEvent = eventHandler, như vậy thì nó sẽ xóa mất phương thức mà trước đó MyEvent đã tham chiếu đến, chú ý đến toán tử “=” chứ không phải là  “+=” (Propety thì không thể dùng với toán tử +=).

– Cách thứ 3, và cũng là cách mà ta đang sử dụng, đó là tạo một đối tượng delegate rồi sau đó tạo hai khối lệnh dùng để add và remove các đối tượng delegate. Với cách thứ 3 thì ta đã bảo đảm được tính bao gói một cách toàn diện. Và cách thứ 3 khi được viết ngắn gọn thì chính là cái cách mà chúng ta vẫn thường dùng, kể từ khi bắt đầu làm quen với events.

4. Một vài sự khác nhau về cách sử dụng của Delegate và Event

Event có thể được khai báo trong interface, Delegate thì không.

Event chỉ có thể được gọi (invoked) ở bên trong class chứa nó, Delegate thì có thể được gọi ở bất cứ đâu (Tùy thuộc vào access modifier).

5. Kết luận

Delegate cung cấp cho ta một cách đơn giản để có thể đại diện cho các lời gọi của phương thức, đặc biệt là các phương thức của đối tượng. Delegate được sử dụng để triển khai các event.

Trong khi đó, event là cách để một lớp có thể đưa ra các thông báo (notification) đến các lớp khác khi có một sự kiện nào đó xảy ra với bản thân nó. Lớp tung ra event được gọi là publisher, còn lớp nhận và xử lý các event này được gọi là Subcriber.

EventDelegate là một cách mà C# sử dụng để triển khai cơ chế Observer Pattern (cũng còn được gọi là mô hình Publisher/Subcriber), điều mà trong Java thì được thực hiện bởi  các ActionListener.

Tham khảo:

Hãy tham gia nhóm Học lập trình để thảo luận thêm về các vấn đề cùng quan tâm.

17 thoughts on “Delegates và Events trong C#

  1. Thầy (cô)ơi cho em hỏi làm thế nào để lưu thông tin nhiều sinh viên… vào 1 file.dat sau đó lại đọc thông tin ra.thầy(cô)viết về chủ đề này đi ạ!

  2. Cho em bổ xung thêm nữa là: yêu cầu lưu thông tin với dạng mã hóa trong file.dat,sau đó lại đọc thông tin từ file đó ra dưới dạng đã giãi mã.em search trên mạng thấy có bài viết mã hóa rất hay nhung ko hiểu và áp dụng được.hy vọng thầy cô có bài hướng dẫn dễ hiểu về chủ đề này!

    1. Giải pháp lưu trữ tốt hơn file.dat của em là dùng SQLite + ADO.NET, như vậy thông tin vừa được quản lý và truy xuất hiệu quả, vừa có khả năng mã hóa cao.

  3. Bài viết hay quá thầy ơi 😀
    Thầy dẫn dắt đi từ Field và Property –> Delegate và Event đúng là hay quá.
    Mong thầy sẽ có nhiều bài viết như thế này nữa ạ 😀
    Em cảm ơn thầy rất nhiều.

  4. Cám ơn thầy, bài viết hay quá. Những kiến thức này không phải ai cũng nhớ và phân biệt rõ ràng như vậy.

  5. Bai viet hay nhat ve delegate minh tung doc vi minh con khong hieu nhieu delegate lam, cam on tac gia, hy vong tac gia co the co 1 bai viet khac ve phuong thuc bat dong do su dung delegate, sorry vi khong dung unicode

  6. Trang web có nhiều bài chia sẻ technical hay như thế này lại ít người biết đến vậy nhỉ? @@ Để mình giới thiệu cho các bác đồng nghiệp IT. Không biết web mình có nhận bài viết đóng góp từ đọc giả chúng tôi không nhỉ?

  7. Em có vấn đề này thắc mắc ạ. Theo như cách thứ 2 như thầy đã đề cập, tại sao mình phải cần bind nhiều phương thức cho cùng 1 sự kiện?, ví dụ như mình có 1 phương thức nữa là Warning(string mes) thì mình sẽ có 2 use case, ví dụ :
    if(true)–>machine.Notify += new ErrorNotification(Warning),
    else –>machine.Notify += new ErrorNotification(PrintString);
    Vậy thì ứng với từng trường hợp mình chỉ cần 1 phương thức xử lí , tại sao mình phải cần bind nhiều phương thức?, thầy có thể cho em ví dụ cụ thể được ko ạ.

  8. Chào Thầy (Cô). Cám ơn Thầy (Cô) về bài viết khá rõ ràng về delegate và event. Nhưng với cách thứ 2 (khai báo dạng private và tạo property) theo như Thầy(Cô) đề cập thì không thể sử dụng += mà chỉ sử dụng =. Nhưng em code thử thì thấy vẫn dùng += và vẫn chạy được tốt. Nhờ Thầy (Cô) giải đáp giúp. Em xin cám ơn.
    Em gửi kèm link tới file code em làm.
    https://docs.google.com/document/d/1cU5L7OGzs5FSlAiV9aVfGUfzwjTpkOeoAkF6uVkLSLI/edit?usp=sharing

  9. Hi bạn,
    Bài của bạn có chỗ không ổn:
    – Bạn dùng Property chứ không dùng event, do đó
    – Bạn dùng get/set chứ không dùng add/remove
    Mình đang nói đến khối lệnh của “public DelegateFunction CalculatorHandler”

    1. Em đọc phía trên bài viết trên thì anh đang nói đến dùng delegate. Em đang nói đến việc dùng delegate chứ chưa đề cập đến Event.

      “- Cách thứ 2, tạo một đối tượng delegate có mức truy cập là private và đồng thời tạo ra một property để truy xuất đến đối tượng delegate đó; cách này thì khá hơn cách trước, nhưng nhược điểm của nó là ta có thể sẽ có khó khăn khi muốn đăng ký nhiều phương thức với sự kiện này, bởi vì nó chỉ có thể nhận vào một phương thức mà thôi”

      1. Thank bạn,
        Bạn đã nói đúng ở dòng đó. Có nghĩa là, với Property thì ta vẫn có thể dùng “+=” để gán delegate. Tuy nhiên, nhược điểm của nó là, bởi vì ta có thể dùng dấu “=” để gán cho nên có thểxảy ra hiện tượng mất hết các tham chiếu trước đó. (Dấu = sẽ bỏ hết các tham chiếu trước đó, và chỉ giữ lại tham chiếu đến phương thức cuối cùng đang được gán). Còn đối với event, thì mình không dùng được dấu “=” mà bắt buộc phải dùng “+=”, đo đó không xảy ra hiện tượng bị mất các tham chiếu như trên.
        Cảm ơn bạn đã bắt lỗi chính xác

Leave a Reply

Your email address will not be published. Required fields are marked *