2012年3月14日 星期三

委派 (Delegate)

什麼是委派

話說委派

在設計一個類別的時候,我們可能必需在某些情況下去執行某些事情,但又不想/無法在類別裡寫死處理的方法,這時就要用到委派。 我們僅在類別裡定義一個特定的簽名(signature),指出要執行的方法的樣子(樣子指的就參數列與回傳值的型別),至於該方法叫什麼名字,內容如何,則由呼叫端於呼叫時自行決定。

舉個例好了:
假設你在設計一個警報器,你在這個類別上加上了一個"警鈴響了"事件,事件發生時,這個類別啟動自動處理模式,例如,撥打119或撥打110或通知老闆...。 但是你在設計時,根本不知道使用者要如何處理,所以這部分就可以留待使用者自行撰寫處理方法。

也就是說,在設計類別時,可能會碰到某個方法在執行時需要額外處理,但是你無法將這部分的處理寫死在類別裡,此時就得將這個部分「外包」給呼叫端自行處理。 而客戶端必須事先提供一個處理函式,等到類別的方法要執行的時候,就會回頭去呼叫(callback)那個客戶端事先指定的處理函式。這個處理函式就稱為「委派方法」。

從類別設計者的角度來說,這種設計方式可將那些變化不定的繁瑣細節從類別中移出去,使類別保持乾淨、穩定。 從呼叫端的角度來看,當你在使用某個類別時,該類別已經設計好一種模式,在你呼叫某個方法之前,它會要求你先提供一個符合特定簽名(signature;即參數與傳回值)的方法,才能達成你想要執行的工作。

這樣子的模式就有點類似C/C++ 的函式指標(function pointer),程式碼回到呼叫端的處理函式執行後,再回到自已的程序。 與C/C++ 函式指標不同的是,delegate 具備 type-safe、secure managed 的特性,以確保 delegate 指向的函式必定存在。若在 delegate 尚未存放任何方法即進行呼叫,則 CLR 會拋出一個例外狀況。

更精準來說:delegate 是 C# 的特殊型別,用來定義方法(method/function)的 signature,delegate 的實體(instance)可以存放一或多個符合該 signature 的方法。

講白一點:delegate 是設計一個機制,這個機制要讓程式碼可以去執行某個方法,但是方法名稱還無法得知,只知道方法的參數型別,和回傳參數的型別。

委派的一些特性

  • delegate 是一種參考型別,它可以 encapsulate 一個或多個方法。
  • delegate 的宣告和方法簽章類似。它有回傳值和參數。
  • delegate 在 CLR 中,扮演事件的底層機制。(function pointer)
  • delegate 可用來封裝具名方法或匿名方法。
  • delegate 大致類似 C++ 的函式指標,但是,delegate 是型別安全的。
  • 將委派與具名方法或匿名方法建立關聯,即可實體化 (Instantiated) 委派。
  • 實體化委派的那個方法,必須和委派的宣告有相同的簽名,也就是相同回傳型別和相同的參數列。
  • 若要搭配匿名方法使用,就要同時宣告委派以及其相關聯的程式碼。

委派的宣告方法

宣告委派和定義 method 的樣式很相似,它包含一個回傳值和任意數量的參數。 所以委派宣告最主要工作就是要明確定義回傳型別參數型別

[modifier] delegate 回傳值型別 delegateName ( [參數列] )
public delegate void Testdelegate3(object sender, EventArgs e);
public delegate void Testdelegate1(string message);
public delegate int  Testdelegate2(MyType m, long num);
public delegate void Testdelegate3(object sender, EventArgs e);

委派的使用方法

要使用委派,要先實體化一個委派(instantiated),通常會使用到要封裝的方法名稱,要不就是使用匿名方法建構。
只要叫用(invoke)委派執行個體執行,委派就會呼叫封裝的方法。
若呼叫端有傳遞參數給委派,委派也會 pass 參數給封裝的方法。
若封裝的方法有回傳值,委派也會將回傳值送給呼叫端。

實體化委派

delegateName var = new delegateName( FunctionName ) ;       //C#1.0 用法
delegateName var = FunctionName ;                           //C#2.0 用法
// Define a delegate
public delegate void Mydelegate(string message);

// Instantiate the delegate.
Mydelegate handler1 = new Mydelegate(DoWork);   //實體化一個委派,這是C#1.0的寫法
Mydelegate handler2 = DoWork;                //這是C#2.0精簡化的寫法,意思與上一行一模一樣

// Call the delegate.
handler1.Invoke("Hello World");      //看似是對委派呼叫,但實際上,委派會去呼叫 DoWork 方法,並 pass 參數"Hello World"給該方法
handler1("Hello World");         //這是C#2.0精簡化的寫法,同上

使用具名方法呼叫

前面提到過,委派方法要和委派具有相同的簽名。此外,使用具名方法呼叫還可以做到:

  • 可以同時封裝多個方法(Multicastdelegate)。
  • 可以封裝執行個體的方式或是靜態的方法

範例說明:

1.範例(1)-(3)都是封裝執行個體的方式
2.範例(4)是封裝靜態的方式
3.範例(3):多重委派 (Multidelegate),委派封裝的方法可以多個,也可以重複。

delegate void Mydelegate(int x, int y);     // 定義委派型別

class Program
{
    static void Main(string[] args)
    {
        MathClass1 mc1 = new MathClass1();
        MathClass2 mc2 = new MathClass2();

        //===========================================
        //ex:1) Invoke the delegate object (   MapTo: mc1.NumberAdd )
        //===========================================

        Mydelegate myDlegate1 = new Mydelegate(mc1.NumberAdd);      //步驟2: 建立委派物件
        for (int i = 1; i <= 3; i++)
        {
            myDlegate1.Invoke(i, i);                                // callback NumberAdd(i, i);
        }
        // output : 2 4 6

        //===========================================
        //ex:2) Invoke the delegate object (  MapTo: mc2.NumberAdd )
        //===========================================

        Mydelegate myDlegate2 = mc2.NumberAdd;                      //步驟2: 建立委派物件  (這是C#2.0精簡的寫法)
        for (int i = 1; i <= 3; i++)
        {
            myDlegate2(i, i);                                       //C#2.0精簡寫法,省略了 .Invoke 
        }
        // output : 4 8 12

        //===========================================
        //ex:3) Invoke the delegate object  (  MapTo: mc1.NumberAdd  and mc2.NumberAdd )
        //===========================================

        Mydelegate myDlegate3 = null;    // 委派變數在使用前必須先初始化 
        myDlegate3 += mc1.NumberAdd;     // 委派可以封裝一個或多個方法
        myDlegate3 += mc2.NumberAdd;     // 委派封裝的方法,是不用管它所參考的物件類別,只要該方法的 引數型別 和 傳回型別 與委派的型別相符即可。(就是簽名signature相同)
        myDlegate3 += mc1.NumberAdd;     // 若委派的引動過程清單中包含一個以上的項目,稱為多點傳送的委派(Multicastdelegate)。
        for (int i = 1; i <= 3; i++)
        {
            myDlegate3.Invoke(i, i);   
        }
        // output : 2 4 2   4 8 4   6 12 6

        //===========================================
        //ex:4) Invoke the delegate object  (  MapTo STATIC method :  MathClass1.NumberMinus )
        //===========================================

        Mydelegate myDlegate4 = MathClass1.NumberMinus;   
        for (int i = 1; i <= 3; i++)
        {
            myDlegate4(i, i);
        }
        // output : 0 0 0

    }
}
    
public class MathClass1
{
    public void NumberAdd(int m, int n)
    {
        int result = (m + n) * 1;
        Console.Write(result.ToString() + " ");
    }

    public static void NumberMinus(int m, int n)
    {
        int result = (m - n) * 1;
        Console.Write(result.ToString() + " ");
    }

}

public class MathClass2
{
    public void NumberAdd(int m, int n)
    {
        int result = (m + n) * 2;
        Console.Write(result.ToString() + " ");
    }
}

使用匿名方法呼叫

在 C# 2.0 以前的版本中,宣告委派的唯一方式是使用具名方法。 C# 2.0 引進了匿名方法 (Anonymous Method),在 C# 3.0 (含) 以後版本,則以 Lambda 運算式取代匿名方法來做為撰寫內嵌 (Inline) 程式碼的慣用方式。 匿名方法可以讓您省略參數清單,而這表示匿名方法可以轉換為具有各種簽章的委派。

在使用匿名方法時,就不需要定義那個方法名稱,所以就不須撰寫實體化這一段程式碼,直接把那個方法要做的事情內嵌到委派宣告的下方。

//===========================================
//ex:5) Invoke the delegate object  (  使用匿名方法 )
//===========================================
//使用匿名方法時,將原本要執行的程式碼內嵌進來,就不需像上面例子還得建立一個獨立的執行方法
//注意{}結尾有一個;
Mydelegate myDlegate5 = delegate(int m, int n)
                        {   
                            int result = (m + n) * 1;
                            Console.Write(result.ToString() + " ");
                        };  
for (int i = 1; i <= 3; i++)
{
    myDlegate5(i, i);
}
// output : 2 4 6

具名方法 VS. 匿名方法

//這個範例示範,分別使用具名方法與匿名方法,撰寫 button1.click 要執行的程式碼
public Form1()
{
    InitializeComponent();

    //使用具名方法
    button1.Click += new System.EventHandler(button1_Click);

    //使用匿名方法,將要執行的程式碼,內嵌進來,就不需要像上面例子還得建立個別的執行方法
    button2.Click += delegate(object o, EventArgs e)
                    {
                        MessageBox.Show("hello");
                    };
}

private void button1_Click(object sender, EventArgs e)
{
    MessageBox.Show("hello");
}
//================================
//當需要建立似乎不太有必要的方法時,就可以指定程式碼區塊來替代委派 (匿名方法)
//啟動新執行緒時就是個好例子
//================================
private void button3_Click(object sender, EventArgs e)
{
    //方法1) 透過具名方法,讓委派啟動新執緒要做的事
    ThreadStart thread_start = new ThreadStart(SimpleWork);  //ThreadStart
    Thread thread1 = new Thread(thread_start);
    thread1.Start();

    //方法2) 透過匿名方法,讓委派啟動新執緒要做的事
    Thread thread2 = new Thread(
            delegate()
            {
                Console.WriteLine("Thread: {0} Start: {1}", Thread.CurrentThread.ManagedThreadId, System.DateTime.Now.ToString());
                Thread.Sleep(3000);
            }
        );
    thread2.Start();
}

static void SimpleWork()
{
    Console.WriteLine("Thread: {0} Start: {1}", Thread.CurrentThread.ManagedThreadId, System.DateTime.Now.ToString());
    Thread.Sleep(3000);
}

C# 3.0 的寫法

「Lambda 運算式」(Lambda Expression) 是一種匿名函式,它可以包含運算式和陳述式 (Statement),而且可以用來建立委派 (delegate) 或運算式樹狀架構型別。

Lambda 運算子「 => 」,這個符號的意思是「移至(goes to)」;或者可以解讀成:「把左邊的參數傳入右邊的匿名方法」

//===========================================
//ex:6) Invoke the delegate object (  使用 C# 3.0 新增加的寫法 )
//===========================================

Mydelegate myDlegate6 = (int m, int n) =>
                        {
                            int result = (m + n) * 3;
                            Console.Write(result.ToString() + " ");
                        };
for (int i = 1; i <= 3; i++)
{
    myDlegate6(i, i);
}
// output : 6 12 18   
//================================
//delegate各種不同寫法的例子
//下面四種程式碼寫法,執行結果都相同
//================================
delegate int Mydelegate(int i);
private void button4_Click(object sender, EventArgs e)
{
    //C#1.0 寫法
    Mydelegate mydelegate1 = new Mydelegate(DoWork);
    int y1 = mydelegate1.Invoke(5);

    //C#2.0 具名寫法
    Mydelegate mydelegate2 = DoWork;
    int y2 = mydelegate1(5);

    //C#2.0 匿名寫法
    Mydelegate mydelegate3 = delegate(int x) { return x * x; };
    int y3 = mydelegate1(5);  

    //C#3.0 Lambda 寫法
    Mydelegate mydelegate4 = x => x * x;   
    int y4 = mydelegate4(5);  
}

static int DoWork(int x)
{
    return x * x;
}

搞懂了沒:委派

delegate double DE(double i);
private double DoWork(double i, DE de)
{
    return de(i + i);
}
private double Square(double i)
{
    return i * i;
}
private void button1_Click(object sender, EventArgs e)
{
    DE de = i => i * 2;
    int x = (int) DoWork(de(25), Square);
    int y = (int) DoWork(de(25), Math.Log10);
}

//x ?= 50 100 625 2500 10000(*)
//y ?= 2

委派的進階使用方法

Multicastdelegate

Multicastdelegate ,委派可以同時封裝多個方法,也就是它的引動過程清單中可以包含一個以上的項目。 所以,若我們 Invoke 一個含有多個項目的引動清單,則委派就依引動清單的順序,執行委派方法。如上面例子所示範的。

該例子因為委派方法沒有回傳值,Invoke 之後,就會依序執行所有的委派方法。 但是如果委派方法有回傳值,也如此直接 Invoke 的話,就只能取得最後一次的回傳值。如下面這個範例:

public class MathClass
    {
        public int Add(int m, int n)
        {
            return (m + n);
        }
        public int Sum(int m, int n)
        {
            return (m - n);
        }
        public int Mul(int m, int n)
        {
            return (m * n);
        }
        public int Div(int m, int n)
        {
            return (m / n);
        }
    }

delegate int MathHandler(int i, int j);

private void button12_Click(object sender, EventArgs e)
{
    MathClass mathclass = new MathClass();
    MathHandler handler = null;

    handler += mathclass.Add;
    handler += mathclass.Sum;
    handler += mathclass.Mul;

    int result = 0;
    result = result + handler.Invoke(4, 5);  //直接 invoke ,只會取得最後一次委派方法的值

    Console.Write(result); //20
}

所以,如果委派方法有回傳值,就必須利用 GetInvocationList 方法,取得委派的引動清單後,再分開 invoke 每一個方法,這樣才可以取得每一次的回傳值。如下面這個範例:

private void button13_Click(object sender, EventArgs e)
{
    MathClass mathclass = new MathClass();
    MathHandler handler = null;

    handler += mathclass.Add;
    handler += mathclass.Sum;
    handler += mathclass.Mul;

    int result = 0;
    foreach (MathHandler math in handler.GetInvocationList())
    {
        result = result + math.Invoke(4, 5);
    }
    Console.Write(result); //28
}

delegate.DynamicInvoke

DynamicInvoke 是動態叫用委派的方法,它是屬於晚期繫結 (late-bound) 的一種做法。 例如,委派的方法必須在執行階段才知道的話,就可以透過 reflection 物件取得組件的執行方法, 再使用靜態的 delegate.Createdelegate 方法,建立委派執行個體。 再使用 DynamicInvoke 以叫用委派方法。

private void button14_Click(object sender, EventArgs e)
{
    //======================================================================================
    // 使用 Reflection 方法,動態載入組件,並取得指定的執行方法物件 MethodInfo
    //======================================================================================

    //透過 reflection 取得組件的執行方法
    string className = "PraceticeBlog.DotNet.MathClass";
    string methodName = (rbMul.Checked ? "Mul" : "Div");
    int n1 = int.Parse(txtN1.Text);
    int n2 = int.Parse(txtN2.Text);

    //取得組件
    Assembly asm = Assembly.GetExecutingAssembly();
    //由組件取得型別物件
    Type MyType = asm.GetType(className);
    //由型別取得方法物件
    MethodInfo method = MyType.GetMethod(methodName, new Type[] { typeof(int), typeof(int) });

    //======================================================================================
    // 委派叫用
    //======================================================================================

    // 建立指定型別的委派
    delegate thedelegate = delegate.Createdelegate(typeof(MathHandler),null, method);

    // DynamicInvoke 動態叫用委派的方法
    int result = (int)thedelegate.DynamicInvoke(n1, n2);
    Console.Write(result);          
}

沒有留言:

張貼留言