最近在學委託,經過摘錄、整理,總結如下:
回調(Callback)函數是windows編程的一個重要部分。回調函數實際上是方法調用的指針,也成爲函數指針,是一個非常強大的編程特性。.NET以委託的形式實現了函數的指針的概念。與C/C++的函數指針不同的是.NET委託是類型安全的。也就是說C/C++的函數指針只不過是一個指向內存單元的指針,我們無法知道這個指針實際指向什麼,像參數和返回類型等就無從知曉了。
當把方法傳送給其他方法時,需要用到委託。如考慮以下的函數:
C++:
#include <iostream>
#include <string>
using namespace std;
int fun(int);
int fun_call(int (*f)(int),int);
void main(int argc,char* argv[])
{
typedef int (*fp)(int);
fp fpt;
fpt=fun;
count<<fun_call(fpt,1);
}
int fun(int a)
{
return a-1;
}
int fun_call(int (*fp)(int),int b)
{
return (fp(10)+b);
}
上述程序的“ftp=fun”實現函數指針的初始化,直接將fun的地址賦給函數指針ftp,然後傳送給fun_call,fun_call可以根據這兩個參數計算出結果:fp(10)=9,9+1=10。實現了把方法傳送給其他方法。
函數指針最常用的是使用函數指針數組來批量調用函數:
int f1(){return 1;}
int f2(){return 2;}
int f3(){return 3;}
void main(int argc,char* argv[])
{
tpyedef int (* fp)();
fp fps[3]={f1,f2,f3};
for(int 0;i<2;i++)
{
cout<<fps[i]<<endl; //實現按數組序列號調用函數
}
}
在編譯時我們不知道第二個方法會是什麼,這個信息只能在運行時得到,所以需要把第二個方法作爲參數傳遞給第一個方法。在C/C++,只能提取函數的地址,並傳送爲一個參數。c是沒有類型安全性的,可以把任何函數傳送給需要函數指針的方法。這種直接的方法會導致一些問題,例如類型安全性,在面向對象編程中,方法很少是孤立存在的,在調用前通常需要與類實例相關聯。而這種指針的方法沒考慮這種情況。所以.NET在語法上不允許使用這種直接的方法。如果要傳遞方法,就必須把方法的細節封裝在一種新的類型的對象中,這種新的對象就是委託。
委託,實際上只是一種特殊的對象類型,其特別之處在於,我們之前定義的所有對象都包含數據,而委託包含的只是函數的地址。
1、在c#中聲明委託
delegate void Method(int x);
定義了委託就意味着告訴編譯器這種類型的委託代表了哪種類型的方法,然後創建該委託的一個或多個實例。編譯器在後臺將創建表示該委託的一個類。也就是說,定義一個委託基本上是定義一個新類,所以可以在定義類的任何地方定義委託,既可以在類的內部定義,也可以在類的外部定義。注意,委託是類型安全性非常高的,因此定義委託時,必須給出它所代表的方法簽名和返回類型等全部細節。
2、在C#中使用委託
using System;
namespace DelegateSpace
{
class DelegateTest
{
private delegate string GetString();
static void Main()
{
Test test=new Test();
GetString method=new GetString(test.Add);
Console.WriteLine(method());
}
}
class Test
{
public string Add(int x,int y)
{
return (x+y).ToString();
}
}
}
上述程序中聲明瞭類型爲GetString的委託,並對它初始化,使它指向對象test的方法Add(int x,int y)。在C#中,委託在語法上總是帶有一個參數的構造函數,這個參數就是委託指向的方法,這個方法必須匹配最初定義委託時的簽名。如上例中委託是這樣定義的:“delegate string GetString();”要求被委託的函數的返回類型是string,如果test.Add(int x,int y)返回的是int,則編譯器就會報錯。還要注意賦值的語句:“GetString method=new GetString(test.Add);“不能寫成"GetString method=new GetString(test.Add(3,2));"因爲test.Add(2,3)返回的是string。而委託的構造函數需要把傳進的是函數的地址,這很像C/C++的函數指針。
3、多播委託
調用委託的次數與調用方法的次數相同,如果要調用多個方法,就需要多次顯式調用這個委託。委託也可以包含多個方法。這種委託稱爲多播委託。如果調用多播委託,就可以按順序連續調用多個方法。所以,委託的簽名必須返回void,否則,就只能得到委託調用的最後一個方法的結果。
如:
delegate void DoubleOp(double value);
class MainEntry
{
static void Main()
{
DoubleOp operations=MathOperation.MultiplyByTwo;
operations+=MathOperation.Square;
}
}
class MathOperation
{
public static double MultiplyByTwo(double value)
{
return value*2;
}
public static double Square(double value)
{
return value*value;
}
}
上面的“DoubleOp operation=MathOperation.MultiplyByTwo;operation+=MathOperation.Square;” 等價於“DoubleOp operation1=MathOperation.MultiplyByTwo;DoubleOp operation2=MathOperation.Square;DoubleOP operations=operation1+operation2;”,多播委託還可以識別運算符-和-=,用於從委託中刪除方法調用。
通過一個多播委託調用多個方法還有一個大問題。多播委託包含一個逐個調用委託的集合。如果通過委託調用一個方法拋出異常,整個迭代就會終止。在這種情況下,爲了避免這個問題,應手動迭代方法列表。可以使用Delegate類定義的方法GetInvocationList(),它返回一個Delegate對象數組。
如考慮以下代碼:
public delegate void DemoDelegate();
class Program
{
static void One()
{
Console.WriteLine("One");
throw new Exception("Test!");
}
static void Two()
{
Console.WriteLine("Two");
}
static void Main()
{
DemoDelegate dl=One;
dl+=Two;
try
{
dl();
}
catch(Exception)
{
Console.WriteLine("Exception caught!");
}
}
}
運行結果:
One
Exception caught!
修改後的代碼如下:
static void Main()
{
DemoDelegate dl=One;
dl+=Two;
Delegate[] delegates=dl.GetInvocationList();
foreach(DemoDelegate d in delegates)
{
try
{
d();
}
}
catch(Exception)
{
Console.WriteLine("Exception caught");
}
}
運行結果如下:
One
Exception caught
Two
同樣地,如果委託簽名不是返回void,但希望得到所有的經委託調用後的結果,也可以用GetInvocationList()得到Delegate對象數組,再用上面的迭代方式獲得返回結果。
4、匿名方法
使用委託還有另外一種方式:通過匿名方法。匿名方法是用作委託參數的一個代碼塊。
如下代碼:
using System;
namespace DelegateTest
{
class Program
{
delegate string delegateString(string val);
static void Main()
{
string mid=",middle part";
delegateString anonDel=delegate(string param)
{
param+=mid;
param+=" and end";
return param;
};
Console.WriteLine(anonDel("Strat of string"));
}
}
}
該代碼塊使用方法級的字符串變量mid,該變量是在匿名方法的外部定義的,並添加到要傳送的參數中,接着代碼返回該字符串值。匿名方法的優點是減少要編寫的代碼。不必定義僅由委託使用的方法。在爲事件定義委託時,這是很顯然的。這有助於減低代碼的複雜性,尤其是定義了好幾個事件時,代碼會顯得比較簡單。使用匿名方法時,代碼執行得不太快。
在使用匿名方法時,必須遵循一些規則:
1)在匿名方法中不能使用跳轉語句跳到該匿名方法的外部;
2)匿名方法外部的跳轉語句不能跳到該匿名方法的內部;
3)在匿名方法內部不能訪問不安全代碼,也不能訪問在匿名方法外部使用的ref和out參數,但可以使用在匿名方法外部定義的其他變量。
5、λ表達式
這是C# 3.0爲匿名方法提供的一個新方法。如前面的語句:
...
static void Main()
{
string mid=...;
delegateString anonDel=param=>
{
param+=mid;
param+=" and end";
return param;
};
...
}
...
λ表達式=>的左邊列出了匿名方法需要的參數,右邊列出了實現代碼,實現代碼放在花括號中,類似於前面的匿名方法,如果實現代碼只有一行,可以刪除花括號和return語句,編譯器會自動添加該語句。
如:public delegate bool Predicate(int val);
Predicate pl=x=>x>5;
在上面的λ表達式中,左邊定義了變量x,這個變量的類型自動設置爲int,因爲這是通過委託定義的,實現代碼返回比較x>5布爾結果。如果x大於5,則返回true,否則返回false。
6、協變和抗變
委託調用的方法不需要與委託聲明的定義類型相同。由此出現協變和抗變。
1)返回類型協變
方法的返回類型可以派生於委託定義的類型。如下代碼:
public class DelegateReturn
{
}
public class DelegateReturn2:DelegateReturn
{
}
public delegate DelegaReturn MyDelegate1();
class Program
{
static void Main()
{
MyDelegate1 d1=Method1;
d1();
}
static DelegateReturn2 Method1()
{
DelegateReturn2 d2=new DelegateReturn2();
return d2;
}
}
上述代碼中,委託MyDelegate定義爲返回DelegateReturn類型。賦予委託實例d1的方法返回DelegateReturn2類型,DelegateReturn2派生自Delegate,根據子類“是”父類的這種關係,滿足了委託的需求。這稱爲返回類型的協變。
2)參數類型的抗變
委託定義的參數可能不同於委託調用的方法,這裏是返回類型不同,因爲方法使用的參數類型可能派生自委託定義的類型。如下代碼:
public class DelegateParam
{
}
public class DelegateParam2:DelegateParam
{
}
public delegate void MyDelegate2(DelegateParam2 p);
class Program
{
static void Main()
{
MyDelegate2 d2=Method2;
DelegateParam2 p=new DelegateParam2();
d2(p);
}
static void Method2(DelegateParam p)
{
}
}
上述代碼中,委託使用的參數類型是DelegateParam2,而賦予委託實例d2的方法使用的參數類型是DelegateParam,DelegateParam是DelegateParam2的基類。