The Delegates in C#

delegate是C#的關鍵保留字,用來宣告可裝載函式的型別

/黃忠成

 

 

What’s Delegate

 

  delegate是C#的關鍵保留字,用來宣告可裝載函式的型別,其地位與class類似,例如用class宣告一個類別。

 

class MyClass

 

相對於delegate就如下。

 

delegate void XXXX();

 

透過delegate宣告的是一個可裝載函式的型別,之後我們便可以此型別來宣告變數並建立實體,爾後以函式呼叫的方式來使用此變數。

public delegate void TestHandler(string s);

……………

TestHandler h = new TestHandler(…..);

…………..

h(“Hello”);

對有C/C++經驗的開發者而言,delegate其實就是function point(in C)或是function object(in C++/STL),只是C/C++的function point/function object通常是不具名的,而delegate是具名,

且化身為一個型別,若硬要找出相似點,Objective-C的Block更像delegate。

typedef int(^SumFunc)(int,int);

….

SumFunc x = …

當然,熟悉C/C++的人很快速地就可以找到如何做出類似的具名function point or function object(typedef)。

.NET實作function point/function object的手法相較於其它平台來說是比較特別的,當.NET相容的編譯器遭遇到function point/function object的宣告關鍵字時(通常是delegate),

會將其展開成為一個繼承自MultiCastDelegate的類別。

class public auto ansi sealed ActionFunction

    extends [mscorlib]System.MulticastDelegate

{

    .method public hidebysig specialname rtspecialname instance void .ctor(object 'object', native int 'method') runtime managed

    {

    }


    .method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke(string s, class [mscorlib]System.AsyncCallback callback, object 'object') runtime managed

    {

    }


    .method public hidebysig newslot virtual instance void EndInvoke(class [mscorlib]System.IAsyncResult result) runtime managed

    {

    }


    .method public hidebysig newslot virtual instance void Invoke(string s) runtime managed

    {

    }

}

查看MulticastDelegate,會發現其繼承自Delegate,而Delegate的建構子可傳入一個MethodInfo物件,這意味著Delegate底層與反射(Reflection)機制有關。

protected Delegate(Type target, string method);

MulticastDelegate與Delegate都是抽象類別,且建構子皆為protected或是private,所以我們無法直接建立MulticastDelegate或是Delegate類別的實體物件,

但透過其提供的靜態函式CreateDelegate,可以指定建構出何種Delegate及對應至哪一個MethodInfo物件。

static void TestDelegate0()

{

      Delegate d = Delegate.CreateDelegate(typeof(ActionFunction),typeof(Program).GetMethod("ActionFunctionHandle"));

      ((ActionFunction)d).Invoke("TEST");

}

因此,我們可以這樣理解,編譯器會為delegate產生出一個繼承自MultiCastDelegate的類別,以delegate宣告變數時,就是建立此類別的實體物件,

當透過delegate變數呼叫函式時,編譯器會對此呼叫產生出對<Delegate>.Invoke呼叫的程式碼。

有一點必須特別注意,雖然Delegate看起來是使用反射(Reflection)機制來呼叫指定的函式,但其效率遠比使用反射來得快,因為這個反射動作是由CLR底層及編譯器合作完成的。

 

PS: 這裡列出的CreateDelegate範例效能與反射(Reflection)大致相同,當循一般途徑宣告Delegate時,編譯器會進行最佳化,GetMethod動作是跳過Reflection而直接由CLR完成的。

 

 

Anonymous  Method

 

  在C# 2.0之前,使用delegate得循以下途徑。

public delegate void ActionFunction(string s);
………………..
static void TestDelegate1(ActionFunction func)
{
    func("Hello World");
}

static void ActionFunctionHandle(string s)
{
     Console.WriteLine(s);
}
………………………
TestDelegate1(new ActionFunction(ActionFunctionHandle));

C# 2.0開始提供Anonymous Method,透過此機制的協助可以讓寫法更簡潔。

TestDelegate1(new ActionFunction(
            //anonymous method  
delegate(string s)
            {
                Console.WriteLine("---anonymous delegate---");
                Console.WriteLine(s);
            }));

Anonymous Method的缺點就是其只能用一次,也就是說這個Anonymous Method在呼叫完TestDelegate1後就無法再使用了,事實上編譯器會為Anonymous Method產生

一個真正的函式,只是函式名稱無從得知,自然也無法呼叫,下面是反組譯後的情況。

[CompilerGenerated]

private static void 
b__0(string s) { Console.WriteLine("---anonymous delegate---"); Console.WriteLine(s); }

C#編譯器很聰明,以下的簡化寫法也是被接受的。

TestDelegate1(delegate(strings)

{

     Console.WriteLine("---anonymous delegate---");

     Console.WriteLine(s);

});

前面提過,所有的delegate宣告都會被轉為繼承自MulticastDelegate的類別,而MulticastDelegate繼承自Delegate,因此下面的寫法也是合法的。

static void TestDelegate2(Delegate func)
{
     func.DynamicInvoke("Hello World");
}

這種寫法可做出允許任何delegate傳入的函式,不過傳遞時有點問題,簡化寫法的話是不被接受的。

//error

TestDelegate2(delegate(strings)

{

                Console.WriteLine(s);

});

 

必須明確地寫成下面這樣。

 

TestDelegate2(new ActionFunction(delegate(string s)
                {
                    Console.WriteLine("---general delegate parameter---");
                    Console.WriteLine(s);
                }));

這是因為當編譯器處理anonymous method時,會依據呼叫目的的參數型別來推論要產生出何種Delegate類別的實體建立程式碼,一旦接收Delegate的函式使用了Delegate作為參數型別,

編譯器便推論為使用Delegate類別來建立出Delegate實體,但Delegate類別是個抽象類別,所以最後會因無法建立出實體而拋出編譯錯誤。特別注意的是當使用Delegate為型別時,

只能透過DynamicInvoke呼叫而無法像前面例子般使用Invoke,後者是由編譯器產生的,差別在於Invoke是一種略過Reflection的呼叫動作,DynamicInvoke則是循正規Reflection方式呼叫,

所以DynamicInvoke會慢上許多,甚至比原生的Reflection更慢,差別有多大呢?,用一個例子來測試。

 

public static string PerformanceTest1()        
{
   return "Hello World";
}

static void TestDelegatePer1(Delegate d)
{            
     ((StringProvider)d).Invoke();
}

static void TestDelegatePer2(MethodInfo mi)
{
     mi.Invoke(null, null);
}

static void TestDelegatePer3(Delegate d)
{
    d.DynamicInvoke();
}
…………………………
Stopwatch sw = new Stopwatch();
Delegate d = Delegate.CreateDelegate(typeof(StringProvider), typeof(Program).GetMethod("PerformanceTest1"));
sw.Start();
for (int i = 0; i < 10000000; i++)
      TestDelegatePer1(d);
sw.Stop();
Console.WriteLine("Native Delegate:"+sw.ElapsedMilliseconds);

            
Delegate d2 = Delegate.CreateDelegate(typeof(StringProvider), typeof(Program).GetMethod("PerformanceTest1"));
sw.Restart();
for (int i = 0; i < 10000000; i++)
       TestDelegatePer3(d);
sw.Stop();
Console.WriteLine("Native Delegate with Dynamic Invoke:" + sw.ElapsedMilliseconds);

MethodInfo mi = typeof(Program).GetMethod("PerformanceTest1");
sw.Restart();
for (int i = 0; i < 10000000; i++)
    TestDelegatePer2(mi);
sw.Stop();
Console.WriteLine("Reflection:"+sw.ElapsedMilliseconds);
Console.ReadLine();

結果如下:

 

Native Delegate:186

Native Delegate with Dynamic Invoke:10846

Reflection:7153

 

 

關於Multi-casting

 

 MulticastDelegate類別本身具備Multi-casting機制,所以以下的寫法也是合法的。

 

ActionFunction func = delegate(string s)
    {
         Console.WriteLine("---multicast delegate 1---");
         Console.WriteLine(s);
    };

func += delegate(string s)
{
     Console.WriteLine("---multicast delegate 2---");
     Console.WriteLine(s);
};

TestDelegate1(func);

此例會先列出delegate 1,再列出delegate 2,簡單的說,就是把多個函式指定給同一個Delegate,當該Delegate被呼叫時,這些函式會依序被呼叫。如果需要,透過GetInvocationList函式

也可以取得此Delegate中所包含的所有函式,每一個函式都對應成一個Delgate物件。

 

foreach(ActionFunctiond infunc.GetInvocationList())
  d("Hello");

 

具回傳值的delegate

 

  Delegate也可以被宣告為一個有回傳值的delegate,如下所示。

public delegate string StringProvider();
………………….
TestFnDelegate(delegate()
            {
                return "Hello World";
            });

 

當透過anonymous method來傳遞時不需指明回傳值型別,C#編譯器會依據傳遞的目標函式參數來推論。

 

Lambda Expression

 

  C# 3.0新增了Lambda Expression支援,主要是簡化原本anonymous method的寫法。

 

TestDelegate1(s => Console.WriteLine(s));

相較於原本的anonymous method寫法,Lambda Expression簡化許多。

=>的左方是函式所需要的參數,右方是函式本體,當只有一個參數時,( 及)是可省略的,多個參數時會變成下面這樣。

 

TestDelegate12((s,s1) => Console.WriteLine(s + s1));

本體如果只有一行的話,{及}是可以省略的,多行會變成下面這樣。

 

TestDelegate1(s =>

{

      Console.WriteLine("---anonymous delegate---");

      Console.WriteLine(s);

});

應用於擁有回傳值的delegate則變成下面這樣。

 

TestFnDelegate(() => "Hello World");

注意,當沒有參數時,必須明確加上(),當有回傳值且本體是單行時,{及}與return都是可省略的,多行的話會變下面這樣。

 

TestFnDelegate(() =>

{

     …………..

     return"Hello World";

});

 

Async-Delegate

 

  Delegate提供兩種呼叫模式,一種是同步型態,也就是前面所用的方式。

 

static void TestLambda(Action func)
{
     //此為同步呼叫模式
     func("Hello World");
}

另一種則是非同步型態,當使用這種方式呼叫Delegate時,Delegate會開啟另外一個執行緒來執行指定的函式。

 

static void TestDelegate4(ActionFunction func)
{
     func.BeginInvoke("Hello World", (state) =>
     {
           Console.WriteLine("Complete");
     }, null);
}

…………….
Console.WriteLine("---test async invoke "+ Thread.CurrentThread.ManagedThreadId.ToString()+" ---");
TestDelegate4(s => 
      {
           Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
           Console.WriteLine(s);
      });

以上程式碼會輸出以下內容,明顯可看出Delegate執行指定函式時的執行緒與呼叫端的執行緒不同。

 

---test async invoke 9 ---

10

Hello World

Complete

 

 

Generics Delegates: Action<T>Func<T>

 

   Action<T>最早出現在.NET Framework 2.0,主要是提供Array.ForEach及List.ForEach兩個函式使用。

 

//prototype of Action
// public delegate void Action (T obj)
static void TestDelegate5()
{
     List data = new List() { "code6421", "david", "tom", "gray" };
    data.ForEach(delegate(string s)
    {
         Console.WriteLine(s);
    });
}

與Action<T>同期出生的還有Predicate<T>,用於Array.Find及List.Find等搜尋用函式。

 

//prototype of Predicate
// public delegate bool Predicate (T obj)
static void TestDelegate6()
{
    List data = new List() { "code6421", "david", "tom", "gray" };
    string b = data.Find(delegate(string s)
            {
                return s == "tom";
            });

    if (b != null)
       Console.WriteLine("item found.");
}

仔細思考ForEach及Find函式的實作及使用方式,你會發現它們符合了Design Patterns中所定義的Template Method設計模式,

泛型(Generics)加上泛型委派(Generics Delegate)則讓C#在實現Template Method設計模式時更加的簡單。

撇開Template Method不談,泛型委派還有另一層意義,當宣告一個delegate時,代表著編譯器必須產生出一個新的類別,舉例來說,我們可能會寫出以下的程式碼。

 

public delegate bool PredicateFuncHandler(T input);
public delegate bool CompareFuncHanlder(T first, T second);
public delegate bool CanCastToIntFuncHandler(T input);
………………
public static List Where(List source, PredicateFuncHandler predicateFunc)
{
     List result = new List();
     foreach (var item in source)
     {
          if (predicateFunc(item))
             result.Add(item);
     }
     return result;
}

public static List CastToInt(List source, CanCastToIntFuncHandler canCastToInt)
{
      List result = new List();
      foreach (var item in source)
      {
          if (canCastToInt(item))
              result.Add(item.ToString()[0]);
      }
      return result;
}



public static T Max(List source, CompareFuncHanlder compareFunc)
{
     if (source.Count == 0)
        throw new ArgumentException("source is empty.");
     if (source.Count == 1)
         return source[0];
     T current = source[0];
     for (int i = 1; i < source.Count; i++)
     {
        if (compareFunc(current, source[i]))
            current = source[i];
     }
     return current;
}

就編譯器而言,她會為這三個delegate產生出三個類別,但事實上PredicateFuncHandler與CanCastToIntFuncHandler是完全相同的,也就是這種寫法造成了多餘的delegate類別產生。

  為了不讓設計師產生多餘的delegate或是為了不產生多餘的delegate而耗費時間找尋已存在並適用的delegate,.NET Framework 3.5延展了Action<T>的設計,除了原有的

無回傳值及無傳入參數的Action委派外還另外提供無傳回值但有特定數量參數的八個常用的泛型委派,統一其名稱為Action<…>。

 

public delegate void Action()

public delegate void Action(T obj)

public delegate void Action(T1 arg1, T2 arg2)

public delegate void Action(T1 arg1, T2 arg2, T3 arg3)

public delegate void Action(T1 arg1, T2 arg2, T3 arg3, T4 arg4)

public delegate void Action(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)

public delegate void Action(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)

public delegate void Action(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)

public delegate void Action(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)

針對有傳回值的泛型委派,.NET Framework 3.5則提供了9個常用的,統一名稱為Func<…>,其最後一個型別參數則一律為函式回傳值型別。

 

public delegate TResult Func()

public delegate TResult Func(T arg)

public delegate TResult Func(T1 arg1, T2 arg2)

public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3)

public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4)

public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)

public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)

public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)

public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)

有了這些後,設計師自行宣告delegate的時機相對減少,上面的例子也可以改寫成無自行宣告delegate的版本。

 

public static List Where(List source, Func predicateFunc)
{
    List result = new List();
    foreach (var item in source)
    {
         if (predicateFunc(item))
             result.Add(item);
    }
    return result;
}

public static List CastToInt(List source, Func canCastToInt)
{
     List result = new List();
     foreach (var item in source)
     {
         if (canCastToInt(item))
             result.Add(item.ToString()[0]);
     }
     return result;
}

public static T Max(List source, Func compareFunc)
{
    if (source.Count == 0)
         throw new ArgumentException("source is empty.");
     if (source.Count == 1)
         return source[0];
     T current = source[0];
     for (int i = 1; i < source.Count; i++)
     {
           if (compareFunc(current, source[i]))
              current = source[i];
     }
     return current;
 }

.NET Framework 3.5會預設建立大量泛型委派的另一個原因則是LINQ,整個LINQ Framework中有大部分的函式都是以Template Method模式設計。

 

PS: 運用Lambda Expression這種技巧,要特別小心變數存取部分,可參考筆者另一篇文章。

The Closure and Lambda Programming Style