First words: tình cờ lượn lờ trên code project thì phát hiện thấy một series 4 bài viết về delegate trong C#, đọc dòng đầu tiên thấy người ta nói quá đúng (đại loại là ngại sử dụng delegate đấy) nên quyết định ngồi đọc. Đọc một chặp mình quyết định dịch ra lại cho nhớ. Tuy nhiên mình vẫn chưa có xin phép bác tác giả bài này để dịch lại thành tiếng Việt (tại làm biến đăng kí acc ^^), hi vọng bác ấy không biết😀. Cái này mình dịch lại thôi, cho nên đại từ nhân xưng “Tôi” ở đây là của bác ấy ấy, không phải em, hì hì. Vậy là xong phần giới thiệu, sau đây sẽ là nội dung chính.

 

Introduction

Có rất nhiều lập trình viên thường tìm cách tránh sử dụng delegate trong công việc của mình. Tất cả chúng ta đều biết rằng chúng ta có thể phát triển bất cứ thứ gì chúng ta muốn bằng rất nhiều các công cụ cũng như các phương thức lập trình rất quen thuộc. Vậy tại sao chúng ta lại phải mất thời gian tìm hiểu sử dụng delegate trong khi có nhiều cách khác cũng có thể thực hiện được chức năng tương tự.

Theo tôi thì đây chỉ đơn giản là lời ngụy biện mà thôi – nó chỉ ra một thực tế rằng đôi khi nhiều (hoặc thậm chí là rất nhiều) lập trình viên không hiểu rõ ràng về delegate.

Challenge

Có hàng ngàn các chủ đề, tutorials và blog giải thích về delegates. Đó quả thực là một thách thức không nhỏ cho những ai đang cố gắng giải thích lại một cái gì đó đã được giải thích rất nhiều lần. Tuy vậy, tôi vẫn quyết định viết một vài bài viết với mục tiêu giúp cho programmer có thể hiểu hơn về nó, nhờ vậy bỏ đi cảm giác ngại ngùng khi sử dụng delegate trong các dự án của họ.

The Essence of Delegates

Trước hết, chúng ta sẽ bắt đầu từ nghĩa tiếng anh của từ Delegate. Theo từ điển của Google, delegate được giải thích như sau:

Delegate: a person sent or authorized to represent others, in particular an elected. Representative sent to a conference.”

Ở đây, tôi muốn nhấn mạnh cụm từ “sent or authorized to represent others”. (tạm hiểu là được gởi gắm hoặc có quyền đại diện cho một người khác). So, delegate trong ngôn ngữ lập trình C# là một thực thể có quyền đại diện cho một đối tượng khác nào đó. Nghĩa là chúng ta đang có một thứ gì đó và nó cần được mô tả lại (hoặc đại diện). Vậy thì một câu hỏi được đặt ra đó là: What is “thứ gì đó”?

Trong trường hợp của chúng ta thì nó chính là các phương thức (method), hoặc hàm (function). Các method được đề cập ở đây có thể nằm ở bất kì đâu. Nó có thể thuộc sở hữu của một đối tượng, hoặc chỉ đơn giản là một static method. Nếu một method cần phải trình diễn từ xa (remote representation), nó sẽ phải dùng delegate. Delegate có thể gọi một phương thức dễ dàng mà không cần quan tâm đến khoảng cách xa hay gần giữa nó và method. Vậy thì tại sao Microsoft lại dùng cái tên delegate để đặt tên cho loại thực thể này? Đơn giản bởi vì chức năng chủ yếu của nó là gọi và thực thi bất kì phương thức nào từ xa, tương tự với định nghĩa tiếng Anh của nó. Ngoài ra, có thể hiểu một cách đại khái rằng nó giúp chúng ta thực hiện việc truyền method như một tham số. Chúng ta đã quá quen thuộc với việc truyền một đối tượng vào hàm với vai trò là tham số, nhưng còn đối với method thì sao? Liệu chúng ta có được phép truyền method/function vào hàm như một parameter? Câu trả lời là KHÔNG. Nhưng (và đây chính là sức mạnh thần kì của delegate), đại diện hợp pháp của nó (ở đây là delegate) thì có thể. Khá là dông dài phải không? Cho đến thời điểm này thì chúng ta đã biết rằng một delegate đại diện cho một method (function). Bây giờ chúng ta sẽ đi vào phân tích cách sử dụng delegate, step by step.

Step 1: Declare a delegate
Public delegate void RemoteOperation();

Microsoft tạo ra delegate theo một cách “rất hướng đối tượng”. Khác với con trỏ hàm trong C++, C# sẽ tạo ra một class khi compiler bắt gặp đoạn code trên. Bạn đừng quá ngạc nhiên. Khi bạn khai báo một delegate thì một class đang được tạo ra bởi compiler mà bạn không biết đó thôi. Tên của class được tạo ra sẽ là RemoteOperation. Tùy thuộc vào compiler version mà nó có thể kế thừa từ class MulticastDelegate (với các phiên bản mới nhất) hoặc từ class Delegate. Class MulticastDelegate kế thừa từ class Delegate. Class này cũng có các contructor, điều này có nghĩa là bạn cần phải khởi tạo nó bằng từ khóa new(). Giữa Delegate và MulticastDelegate cũng có những mối quan hệ nhất định. Nếu bạn là một lập trình viên chuyên nghiệp về OOP, hẳn bạn đã từng nghe qua về programming patterns lần đầu tiên được giới thiệu bởi Gang of Four (GOF). MulticastDelegate -> Delegate là một ví dụ hoàn hảo của Structural Composite Pattern (thiệt tình là chưa có biết cái GOF với cái Structural Composite là gì ^^). Nói chung là khái niệm về nó thì rất đơn giản, Delegate class giới thiệu component. MulticastDelegate giới thiệu composite. MulticastDelegate có cài đặt một mảng có kiểu Delegate dùng để lưu trữ các đối tượng delegate (private array). Bạn có thể thêm hoặc bớt các đối tượng của MulticastDelegate từ mảng này theo ý thích. Base class Delegate có một số các properties và methods. Trong đó nó có một method gọi là Invoke(), method này có các dấu hiệu đặc trưng của delegate (đó là kiểu trả về, danh sách các tham số truyền vào, các thành phần này gọi chung là signature). Signature này được lấy từ câu lệnh khai báo delegate của bạn. Trong ví dụ của chúng ta, phương thức Invoke không nhận bất cứ tham số nào và có kiểu trả về là kiểu void. Base class delegate có 2 property đặc biệt quan trọng:

  1. Target – dùng để lưu trữ tham chiếu đến đối tượng mà Method được cài đặt
  2. Method – lưu trữ MethodInfo của đối tượng này

Chúng ta sẽ đi sâu vào các properties này sau.


Note: nếu chúng ta khai báo delegate khác (nhìn bên dưới) thì Invoke method cũng phải có kiểu trả về là kiểu int và nhận 2 tham số, một kiểu integer và một kiểu string. Nói cách khác, Invoke method bắc chước định nghĩa delegate của bạn.

public delegate int DifferentSignatureDelegate (int id , string name );

Bây giờ chúng ta sẽ chuẩn bị một method mà chúng ta muốn delegate mô tả lại. Đây chỉ đơn giản là một static function với tên là DoNothing(), function này không có tham số và return void:

public static void DoNothing()
{
       Console.WriteLine( "Nothing in nothing out, no action taken");
}  
Step 2: Instantiate the delegate
RemoteOperation operationVoid = new RemoteOperation(DoNothing);

Lúc này, compiler sẽ tạo ra một instance hoặc class RemoteOperation. Như các bạn thấy, chúng ta sử dụng từ khóa new và một tham số truyền vào contructor của nó, tham số này chính là tên của function, không phải là một string. Bạn còn nhớ Invoke method chứ? Trước khi bắt đầu tạo ra instance, compiler sẽ kiểm tra xem signature của hàm DoNothing có giống với signature của Invoke method của class RemoteOperation hay không. Nếu sai, compiler sẽ trả về một lỗi với nội dung là không tìm thấy cái hàm “somename” nào trùng với delegate “delegatename”. Còn nếu đúng, một đối tượng delegate thuộc kiểu RemoteOperation được tạo ra. Sau đó, compiler tạo một thể hiện mới của class Delegate. Thể hiện này sẽ set value của Target property thành null bởi vì delegate đang là đại diện của một static function. Nếu function mà delegate đại diện không phải là static thì lúc này, chúng ta sẽ đưa vào đối tượng delegate này một hàm thuộc thể hiện của object sở hữu hàm đó. Property còn lại là Method sẽ có giá trị là Void DoNothing(). Sau đó, nó sẽ thêm thể hiện của class Delegate mà ta nói ở trên vào mảng Delegates được cài đặt bên trong đối tượng kiểu RemoteOperation

 

Hình ảnh này không thực sự là một UML diagram, nó chỉ mô tả những gì xảy ra bên trong khi chúng ta thực hiện một delegate. Đối tượng vừa mới tạo ra sẽ có đầy đủ các thông tin cần thiết để mô tả lại chính xác công việc của function DoNothing(), đó là:

  1. Có signature của function
  2. Biết chính xác function này được đặt ở đâu
  3. Nó có thể gọi function này được.
    Và, bây giờ chúng ta có thể sử dụng đối tượng này làm tham số để truyền vào hàm.
    Step 3: Execute the Delegate

    Việc thực thi delegate (gọi hàm mà nó đại diện) rất đơn giản:

    operatorVoid();

    Câu lệnh này cũng tương đương như việc gọi Invoke method của đối tượng operatorVoid:

    operatorVoid.Invoke();

    Lúc này, chương trình sẽ tìm hàm được đưa vào delegate và thực thi nó, bất kể dù nó nằm ở đâu. Một câu hỏi đặt ra là: một delegate có thể làm đại diện cho nhiều hàm/phương thức khác? Có trả lời là có, hiển nhiên!! Một đối tượng delegate có thể đại diện cho bao nhiêu methods (functions) tùy ý. Chỉ có duy nhất một yêu cầu đó là tất cả các function này đều phải có signature giống tuyệt đối với delegate. Và như chúng ta đã biết, việc này thực hiện được là nhờ mảng lưu trữ các đối tượng có kiểu là Delagate. Bạn có thể xem mảng này trong lúc debug nhờ vào phương thức GetInvocationList(). Bây giờ, chúng ta có thêm một class nữa:

    public class SomeClass
    {
        public void AnotherFunction() {}
    }  

    Sau đó chúng ta sẽ tạo ra một thể hiện của class này:

    SomeClass o1 = new SomeClass (); 

    Thêm tham chiếu đến method AnotherFunction() vào delegate:

    operationVoid += new RemoteOperation(o1.AnotherFunction); 

    Hình ảnh số 2 dưới đây sẽ mô tả những gì có bên trong đối tượng operatorVoid.

Khi bạn gọi method Invoke, tất cả các function trong delegate sẽ được gọi thực thi theo trình tự mà bạn add vào. Bạn cũng có thể remove một function ra khỏi delegate:

operationVoid -= new RemoteOperation(o1.AnotherFunction);

Example

Lí thuyết như vậy là đủ rồi ha, bây giờ chúng ta sẽ code thử vài dòng để demo những gì chúng ta vừa được biết ở trên. Ý tưởng của đoạn code này là dựa trên remote control của TV để bật, tắt.

Trước hết chúng ta khai báo một public delegate:

public delegate void RemoteOperation();

Class Television có 2 method dùng để bật và tắt TV:

public delegate void RemoteOperation()   public class Television
{
	private string model;
	public string Model { get {return model; } }
	// Constructor
	public Television (string model)
	{
		this.model = model;
	}
	// Turn TV on
	public void TurnOn()
	{
		Console.Writeline(this.model + " is on");
	}
	// Turn TV off
	public void TurnOff()
	{	
		Console.WriteLine(this.model + " is off");
	}
}  

Class TVOwner có 2 readonly property: TV và Name

public class TVOwner
{
	private Television myTV;
	private string name;
	public string Name
	{
		get { return name; }
	}
	public Television TV 
	{
		get { return myTV; }
	}
	// Constructor
	public TVOwner (string name, string tvmodel)
	{
		this.name = name;
		myTV = new Television(tvmode);
	}
}

Class Friend có một biến private thuộc kiểu RemoteOperation – tên của delegate đã được khai báo từ từ trước. Ngoài ra còn có 2 methods:

  1. SetRemoteOperation
  2. RemoveRemoteOperation

Các method này nhận một trong các tham số có kiểu RemoteOperation và add tham số này vào delegate (biến private). Người sử dụng TV có thể gọi trực tiếp phương thức TurnOn và TurnOff, nhưng họ cũng có thể gọi các phương thức này thông qua điều khiển từ xa. Hơn nữa, họ cũng có thể chuyển remote control sang cho người khác sử dụng, và những người này tất nhiên cũng có thể bật/tắt TV bằng remote control. Đây chính là công dụng của delegate.

public class Friend
{
    private RemoteOperation remote; // variable of delegate type
    private string name;
    // Constructor
    public Friend (string name)
    {
         this.name = name;
    }   // Set quyen dieu khien cho thiet bi
    public void SetRemoteOperation (RemoteOperation remote, string deviceName)
    {
         this.remote += remote;
         Console.WriteLine("Remote operation for " + deviceName + " set");
    }   // Remove quyen dieu khien cua thiet bi
    public void RemoveRemoteOperation (RemoteOperation remote, string deviceName)
    {
         this.remote -= remote;
         Console.WriteLine("RemoteOperation " + deviceName + " removed");
    }   // Thuc thi lenh dieu khien tu xa
    public void UserRemote()
    {
         if (remote != null)
         {
              Console.WriteLine(name + " clicked remote '" + 
                                            remote.GetType().FullName + " " 
                                             + Remote.Method.Name +  "'");
              remote();
         }
    }	
}    

Đoạn chương trình chính:

/// <summary>
/// Execution program class
/// </summary>
public class Program
{
    /// <summary>
    /// Main procedure
    /// </summary>
    /// <param name="args"></param>
    static void Main( string[] args)
    {
	//create tv owner:
	TvOwner owner = new TvOwner( "Ed", "Samsung");   //create tv owner:
	TvOwner nowner = new TvOwner( "Nick", "Sony");   //create friend:
	Friend friend = new Friend( "Alex");   //instantiate delegate for turning tv on:
	RemoteOperation setTvOn = new RemoteOperation(owner.TV.TurnOn);   //instantiate delegate for turning tv on:
	RemoteOperation aaa = new RemoteOperation(nowner.TV.TurnOn);   //instantiate delegate for turnting tv off:
	RemoteOperation setTvOff = new RemoteOperation(owner.TV.TurnOff);   //pass function turnOnTv to the friend:
	friend.SetRemoteOperation(setTvOn, owner.TV.Model);   //pass function turnOnTv to the friend:
	friend.SetRemoteOperation(aaa, nowner.TV.Model);   //friend is using the remote to turn TV
	friend.UserRemote();   //remove remote operation from friend:
	friend.RemoveRemoteOperation(setTvOn, owner.TV.Model);   //pass function turnOffTv to the friend:
	friend.SetRemoteOperation(setTvOff, owner.TV.Model);   //friend is using the remote to turn TV
	friend.UserRemote();   //instantiate delegate for static function:
	RemoteOperation operationVoid = new RemoteOperation(DoNothing);   friend.RemoveRemoteOperation(setTvOff, "none");   friend.SetRemoteOperation(operationVoid, "none");   friend.UserRemote();   //finish:
	Console.Read();
    }   public static void DoNothing()
    {
	Console.WriteLine( "Nothing in nothing out, no action");
    }
}	

Kết quả khi chạy đoạn chương trình trên:

Tóm tắt

Dễ thấy là một delegate khi thể hiện được instantiated sẽ có kiểu MulticastDelegate. Nhiệm vụ của đối tượng này là để giữ tham chiếu đến một hoặc nhiều phương thức có cùng signature. Signature này được set trong lúc khai báo delegate. Bản thân bên trong mỗi delegate có một mảng lưu trữ các tham chiếu đến mỗi method được add vào delegate, và khi delegate thực thi methode Invoke(), tất cả các method được delegate tham chiếu tới sẽ được gọi thực thi ngay lập tức. Delegate cũng là một đối tượng, do đó chúng ta cũng có thể xem nó như các loại đối tượng khác, nghĩa là chúng ta có thể đưa nó vào hàm với vai trò tham số.

Bạn có thể xem bài viết nguyên mẫu tại: http://www.codeproject.com/KB/cs/delegatespart1.aspx

[To be continued…]