02/10/2018, 00:07

[C#] Hướng dẫn sử dụng Delegate và Event trong lập trình Csharp

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ữ lập trình 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 ...

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ữ lập trình 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 là gì?

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. 

2. Event là gì?

Đ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à Field và Property. 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à get và set 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à add và remove; 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 add và remove 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.

Event và Delegate là một cách mà lập trình 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.

Tags: eventlập trình
0