-
#1. 委托
- 1.1 聲明委托
- 1.2 使用委托
- 1.3 簡單委托示例
- 1.4 Action<T>和Func<T>委托
- 1.5 BubbleSorter示例
- 1.6 多播委托
- 1.7 匿名方法
-
#2. Lambda 表達式
- 2.1 參數(shù)
- 2.2 多行代碼
- 2.3 Lambda表達式外部變量
-
#3. 事件
- 3.1 事件發(fā)布程序
- 3.2 事件偵聽器
- 3.3 弱事件
#1. 委托
當要把方法傳遞給其他方法時歹河,需要使用委托掩浙。把方法的細節(jié)封裝在一種新類型的對象中,即委托秸歧。委托只是一種特殊類型的對象厨姚,其特殊之處在于,我們以前定義的所有對象都包含數(shù)據(jù)键菱,而委托包含的只是一個或多個方法的地址谬墙。
1.1 聲明委托
在C#中使用一個類時,分兩個階段。首先拭抬,需要定義這個類部默,即告訴編譯器這個類由什么字段和方法組成。然后(除非只使用靜態(tài)方法)造虎,實例化類的一個對象傅蹂。使用委托時,也需要經(jīng)過這兩個步驟算凿。
- 首先必須定義要使用的委托份蝴,對于委托,定義它就是告訴編譯器這種類型的委托表示哪種類型的方法氓轰。
- 然后搞乏,必須創(chuàng)建該委托的一個或多個實例。編譯器在后臺將創(chuàng)建表示該委托的一個類戒努。
定義委托的語法如下:
delegate void IntMethodInvoker(int x);
在定義委托時,必須給出它所表示的方法的簽名和返回類型等全部細節(jié)镐躲。
==理解委托的一種好方式是把委托當作這樣一件事情储玫,它給方法的簽名和返回類型指定名稱。==
1.2 使用委托
下面的代碼說明了如何使用委托萤皂。
private delegate string GetAString();
static void Main(string[] args)
{
int x = 40;
GetAString firstStringMethod = new GetAString(x.ToString);
Console.WriteLine("String is {0}", firstStringMethod());
//With firstStringMethod initialized to x.ToString(),
//the above statement is equivalent to saying
//Console.WriteLine("String is {0}",x.ToString());
}
在這段代碼中撒穷,實例化了類型GetAString的一個委托,并對它進行初始化裆熙,使它引用整形變量x的ToString()。在C#中,委托在語法上總是接受一個參數(shù)的構(gòu)造函數(shù)恕洲,這個參數(shù)就是委托引用的方法晒夹。這個方法必須匹配最初定義委托時的簽名。所以在這個示例中僚稿,如果不帶參數(shù)并返回一個字符串的方法來初始化firstStringMethod變量凡桥,就會產(chǎn)生一個編譯錯誤。注意蚀同,因為int.ToString()是一個實例方法(不是靜態(tài)方法)缅刽,所以需要指定實例(x)和方法名來正確地初始化委托。
實際上蠢络,給委托實例提供圓括號與調(diào)用委托類的Invoke()方法完全相同衰猛。因為firstStringMethod是委托類型的一個變量,所以c#編譯器會用firstStringMethod.Invoke()代替firstStringMethod()刹孔。
firstStringMethod();
firstStringMethod.Invoke();
為了減少輸入量啡省,只要需要委托實例,就可以只傳送地址的名稱。這稱為委托推斷冕杠。只要編譯器可以把委托實例解析為特定的類型微姊,這個C#特性就是有效的。下面用GetAString委托的一個新實例初始化GetAString類型的firstStringMethod變量:
GetAString firstStringMethod = new GetAString(x.ToString);
只要用變量x把方法名傳送給變量firstStringMethod分预,就可以編寫出作用相同的代碼:
GetAString firstStringMethod = x.ToString;
==調(diào)用上述方法時輸入形式不能為x.ToString()(不要輸入圓括號)兢交,也不能把它傳送給委托變量,輸入圓括號調(diào)用一個方法笼痹,調(diào)用x.ToString()方法會返回一個不能賦予委托變量的字符串對象配喳,只能把方法的地址賦予委托變量。==
委托推斷可以在需要委托實例的任何地方使用凳干。委托推斷也可以用于事件晴裹,因為事件基于委托。
==給委托的實例可以引用任何類型的任何對象上的實例方法或靜態(tài)方法——只要方法的簽名匹配于類型的簽名即可救赐。==
1.3 簡單的委托示例
在這個示例中涧团,定義一個類MathsOperations,它有兩個靜態(tài)方法经磅,對double類型的值執(zhí)行兩個操作泌绣,然后使用該委托調(diào)用這些方法。
class MathOperations
{
public static double MultiplyByTwo(double value)
{
return value * 2;
}
public static double Square(double value)
{
return value * value;
}
}
下面調(diào)用方法:
class DelegateTest
{
private delegate double DoubleOp(double x);
static void Main(string[] args)
{
DoubleOp[] operations = {
MathOperations.MultiplyByTwo,
MathOperations.Square
};
for (int i = 0; i < operations.Length; i++)
{
Console.WriteLine("Using operations[{0}]:", i);
ProcessAndDisplayName(operations[i], 2.0);
ProcessAndDisplayName(operations[i], 7.94);
ProcessAndDisplayName(operations[i], 1.414);
Console.WriteLine();
}
}
static void ProcessAndDisplayName(DoubleOp action, double value)
{
double result = action(value);
Console.WriteLine("Value is {0},result of operation is {1}", value, result);
}
}
在這段代碼中预厌,實例化了一個委托數(shù)組DoubleOp(記住阿迈,一旦定義了委托類,基本上就可以實例化它的實例轧叽,就像處理一般的類那樣——所以把一些委托的實例放在數(shù)組中是可以的)苗沧。該數(shù)組的每個元素都初始化為由MathsOperations類實現(xiàn)的不同操作。
1.4 Action<T>和Func<T>委托
除了為每個參數(shù)和返回類型定義一個新委托類型之外炭晒,還可以使用Action<T>和Func<T>委托待逞。泛型Action<T>委托表示引用一個void返回類型的方法。
Func<double, double>[] operations = {
MathOperations.MultiplyByTwo,
MathOperations.Square
};
static void ProcessAndDisplayName(Func<double,double> action, double value)
{
double result = action(value);
Console.WriteLine("Value is {0},result of operation is {1}", value, result);
}
1.5 BubbleSorter示例
下面的示例將說明委托的真正用途网严。我們要編寫一個類BubbleSorter飒焦,它實現(xiàn)一個靜態(tài)方法Sort(),這個方法的第一個參數(shù)是一個對象數(shù)組屿笼,把該數(shù)組按照升序重新排列牺荠。
對于接受類型T的泛型方法Sort<T>,需要一個比較方法驴一,其兩個參數(shù)的類型是T休雌,if比較的返回類型是布爾類型。這個方法可以從Func<T1,T2,TResult>委托中引用肝断,其中T1和T2的類型相同:Func<T,bool>杈曲。給Sort<T>方法指定下述簽名:
static public void Sort<T>(IList<T> sortArray,Func<T,T,bool> comparison);
這個方法的文檔說明驰凛,comparison必須引用一個方法,該方法帶有兩個參數(shù)担扑,如果第一個參數(shù)的值“小于”第二個參數(shù)恰响,就返回true。
設置完畢后涌献,下面定義BubbleSorter類:
class BubbleSorter
{
static public void Sort<T>(IList<T> sortArray, Func<T, T, bool> comparison)
{
bool swapped = true;
do
{
swapped = false;
for (int i = 0; i < sortArray.Count - 1; i++)
{
if (comparison(sortArray[i + 1], sortArray[i]))
{
T tmp = sortArray[i];
sortArray[i] = sortArray[i + 1];
sortArray[i + 1] = tmp;
swapped = true;
}
}
} while (swapped);
}
}
為了使用這個類胚宦,需要定義另一個類,從而建立要排序的數(shù)組燕垃。
class Employee
{
public string Name { get; private set; }
public decimal Salary { get; private set; }
public Employee(string name, decimal salary)
{
this.Name = name;
this.Salary = salary;
}
public override string ToString()
{
return string.Format("{0}, {1:C}", Name, Salary);
}
public static bool CompareSalary(Employee l, Employee r)
{
return l.Salary < r.Salary;
}
}
注意枢劝,為了匹配Func<T,T,bool>委托的簽名,在這個類中必須定義CompareSalary卜壕,它的參數(shù)是兩個Employee引用您旁,并返回一個布爾值,在實現(xiàn)比較代碼中轴捎,根據(jù)薪水進行比較鹤盒。
下面編寫一些客戶端代碼,完成排序:
class DelegateTest
{
static void Main(string[] args)
{
Employee[] employees = {
new Employee("Bugs Bunny",20000),
new Employee("Elmer Fudd",10000),
new Employee("Daffy Duck",25000),
new Employee("Will Coyote",1000000.38m),
new Employee("Foghorn Leghorn",23000),
new Employee("RoadRunner",50000),
};
BubbleSorter.Sort(employees, Employee.CompareSalary);
foreach(var employee in employees)
{
Console.WriteLine(employee);
}
}
}
1.6 多播委托
前面使用的每個委托都只包含一個方法調(diào)用侦副。調(diào)用委托的次數(shù)與調(diào)用方法的次數(shù)相同昨悼。如果要調(diào)用多個方法,就需要多次顯式調(diào)用這個委托跃洛。但是,委托也可以包含多個方法终议。這種委托稱為多播委托汇竭。如果調(diào)用多播委托,就可以按順序連續(xù)調(diào)用多個方法穴张。為此细燎,委托的簽名就必須返回void;否則皂甘,就只能得到委托調(diào)用的最后一個方法的結(jié)果玻驻。
可以使用返回類型為void的Action<double>委托:
class DelegateTest
{
static void Main(string[] args)
{
Action<double> operations = MathOperations.MultiplyByTwo;
operations += MathOperations.Square;
}
}
在前面的示例中,因為要存儲對兩個方法的引用偿枕,所以實例化了一個委托數(shù)組璧瞬。而這里只是在同一個多播委托中添加兩個操作。多播委托可以識別運算符"+"和"+="渐夸。
要委托引用返回void的方法嗤锉,就應重寫MathOperations類中的方法,從而讓它們顯示其結(jié)果墓塌,而不是返回它們:
class MathOperations
{
public static void MultiplyByTwo(double value)
{
double result = value * 2;
Console.WriteLine("Multiplying by 2: {0} gives {1}", value, result);
}
public static void Square(double value)
{
double result = value * value;
Console.WriteLine("Squaring: {0} gives {1}", value, result);
}
}
為了適應這個改變瘟忱,也必須重寫ProcessAndDisplayName()方法:
static void ProcessAndDisplayName(Action<double> action, double value)
{
Console.WriteLine();
Console.WriteLine("ProcessAndDisplayName called with value = {0}", value);
action(value);
}
下面測試多播委托奥额,其代碼如下:
class DelegateTest
{
static void Main(string[] args)
{
Action<double> operations = MathOperations.MultiplyByTwo;
operations += MathOperations.Square;
ProcessAndDisplayName(operations, 2.0);
ProcessAndDisplayName(operations, 7.94);
ProcessAndDisplayName(operations, 1.414);
Console.WriteLine();
}
}
如果正在使用多播委托,就應知道對同一個委托調(diào)用方法鏈的順序并未正式定義访诱。因此應避免編寫依賴于以特定順序調(diào)用方法的代碼垫挨。
通過一個委托調(diào)用多個方法還可能導致一個大問題。多播委托包含一個逐個調(diào)用的委托集合触菜。如果通過委托調(diào)用的其中一個方法拋出異常九榔,整個迭代就會停止。
class DelegateTest
{
static void Main(string[] args)
{
Action d1 = One;
d1 += Two;
try
{
d1();
}
catch (Exception e)
{
Console.WriteLine("Exception caught with msg:{0}",e.Message);
}
}
static void One()
{
Console.WriteLine("One");
throw new Exception("Error in one");
}
static void Two()
{
Console.WriteLine("Two");
}
}
委托只調(diào)用了第一個方法玫氢。因為第一個方法拋出了異常帚屉,所以委托的迭代會停止,不再調(diào)用Two()漾峡。
在這種情況下攻旦,為了避免這個問題,應自己迭代方法列表生逸。Delegate類定義GetInvocationList()牢屋,它返回一個Delegate數(shù)組。現(xiàn)在可以使用這個委托調(diào)用與委托直接相關(guān)的方法槽袄,捕獲異常烙无,并繼續(xù)下一次迭代。
class DelegateTest
{
static void Main(string[] args)
{
Action d1 = One;
d1 += Two;
Delegate[] delegates = d1.GetInvocationList();
foreach(Action d in delegates)
{
try
{
d();
}
catch (Exception e)
{
Console.WriteLine("Exception caught with msg:{0}", e.Message);
}
}
}
}
1.7 匿名方法
到目前為止遍尺,要想使委托工作截酷,方法必須已經(jīng)存在(即委托是用它將調(diào)用的方法的相同的簽名定義的)。但還有另外一種使用委托的方式:即通過匿名方法乾戏。匿名方法是用作委托的參數(shù)的一段代碼迂苛。
用匿名方法定義委托的語法與前面的定義并沒有區(qū)別。但在實例化委托時鼓择,就有區(qū)別了三幻。
class DelegateTest
{
static void Main(string[] args)
{
string mid = ", middle part,";
Func<string, string> anonDel = delegate (string param)
{
param += mid;
param += " and this was added to the string.";
return param;
};
Console.WriteLine(anonDel("Start of string"));
}
}
Func<string,string>委托接受一個字符串參數(shù),返回一個字符串呐能。anonDel是這種委托類型的變量念搬。不是把方法名賦予這個變量,而是使用一段簡單的代碼:它前面是關(guān)鍵字delegate摆出,后面是一個字符串參數(shù)朗徊。
在使用匿名方法時,必須遵循兩條規(guī)則偎漫。在匿名方法中不能使用跳轉(zhuǎn)語句(break荣倾、goto或continue)跳到該匿名方法的外部,反之亦然:匿名方法外部的跳轉(zhuǎn)語句不能跳到該匿名方法的內(nèi)部骑丸。
在匿名方法內(nèi)部不能訪問不安全的代碼舌仍。另外妒貌,也不能訪問在匿名方法外部使用的ref或out參數(shù)。但可以使用在匿名外部定義的其他變量铸豁。
#2. Lambda表達式
自C#3.0開始灌曙,就可以使用一種新語法把實現(xiàn)代碼賦予委托:Lambda表達式。只要有委托參數(shù)類型的地方节芥,就可以使用Lambda表達式在刺。前面使用匿名方法的例子可以改為使用Lambda表達式:
class LambdaTest
{
static void Main(string[] args)
{
string mid = ", middle part,";
Func<string, string> lambda = param =>
{
param += mid;
param += " and this was added to the string.";
return param;
};
Console.WriteLine(lambda("Start of string"));
}
}
Lambda運算符"=>"的左邊列出了需要的參數(shù)。Lambda運算符的右邊定義了賦予lambda變量的方法的實現(xiàn)代碼头镊。
2.1 參數(shù)
Lambda表達式有幾種定義參數(shù)的方式蚣驼。如果只有一個參數(shù),只寫出參數(shù)名字就足夠了相艇。下面的Lambda表達式使用了參數(shù)s颖杏。因為委托類型定義了一個string參數(shù),所以s的類型就是string坛芽。
Func<string,string> oneParam = s => String.Format(
"change uppercase {0}",s.ToUpper());
Console.WriteLine(oneParam("test"));
如果委托使用多個參數(shù)留储,就把參數(shù)名放在花括號中。這里參數(shù)x和y的類型是double咙轩,由Func<double,double,double>委托定義:
Func<double,double,double> twoParams = (x,y) => x * y;
Console.WriteLine(twoParams(3,2));
為了方便获讳,可以在花括號中給變量名添加參數(shù)類型:
Func<double, double, double> twoParamsWithTypes = (double x, double y) => x * y;
Console.WriteLine(twoParamsWithTypes(4, 2));
2.2 多行代碼
如果Lambda表達式只有一條語句,在方法塊內(nèi)就不需要花括號和return語句活喊,因為編譯器會添加一條隱式的return語句丐膝。
Func<double,double> square = x => x * x;
添加花括號、return語句和分號是完全合法的钾菊,通常這比不添加這些符號更容易閱讀:
Func<double,double> square = x => {
return x * x;
}
但是帅矗,如果在Lambda表達式的實現(xiàn)代碼中需要多條語句,就必須添加花括號和return語句:
Func<string,string> lambda = param => {
param += mid;
param += " and this was added to the string";
return param;
}
2.3 Lambda表達式外部的變量
通過Lambda表達式可以訪問Lambda表達式塊外部的變量结缚。這是一個非常好的功能,但如果為正確使用软棺,也會非常危險红竭。
在下面的示例中,F(xiàn)unc<int,int>類型的Lambda表達式需要一個int參數(shù)喘落,返回一個int茵宪。該Lambda表達式的參數(shù)用變量x定義。實現(xiàn)代碼還訪問了Lambda表達式外部的變量someVal瘦棋。只要不認為在調(diào)用f時稀火,Lambda表達式創(chuàng)建了一個以后使用的新方法,這似乎沒有什么問題赌朋。
int someVal = 5;
Func<int, int> f = x => x + someVal;
Console.WriteLine(f(10));
對于Lambda表達式x => x + someVal,編譯器會創(chuàng)建一個匿名類凰狞,它有一個構(gòu)造函數(shù)來傳遞外部變量篇裁。該構(gòu)造函數(shù)取決于從外部傳遞進來的變量個數(shù)。
public class AnonymousClass
{
private int someVal;
public AnonymousClass(int someVal)
{
this.someVal = someVal;
}
public int AnonymouseMethod(int x) {
return x + someVal;
}
}
使用Lambda表達式并調(diào)用該方法赡若,會創(chuàng)建匿名類的一個實例达布,并傳遞調(diào)用該方法時變量的值。
==Lambda表達式可以用于類型是一個委托的任意地方逾冬。類型是Expression或Expression<T>時黍聂,也可以使用Lambda表達式。此時編譯器會創(chuàng)建一個表達式樹身腻。==
#3. 事件
事件基于委托产还,為委托提供了一種發(fā)布/訂閱機制。
3.1 事件發(fā)布程序
從CarDealer類開始嘀趟,它基于事件提供一個訂閱脐区。CarDealer類用event關(guān)鍵字定義了類型為EventHandler<CarInfoEventArgs>的NewCarInfo事件。在NewCar()方法中去件,觸發(fā)NewCarInfo事件:
public class CarIInfoEventArgs : EventArgs
{
public string Car { get; private set; }
public CarIInfoEventArgs(string car)
{
this.Car = car;
}
}
public class CarDealer
{
public event EventHandler<CarIInfoEventArgs> NewCarInfo;
public void NewCar(string car)
{
Console.WriteLine("CarDealer, new car {0}", car);
NewCarInfo?.Invoke(this, new CarIInfoEventArgs(car));
}
}
CarDealer類提供了EventHandler<CarIInfoEventArgs>類型的NewCarInfo事件坡椒。作為一個約定,事件一般使用帶兩個參數(shù)的方法尤溜,其中第一個參數(shù)是一個對象倔叼,包含事件的發(fā)送者,第二個參數(shù)提供了事件的相關(guān)信息宫莱。
委托EventHandler<TEventArgs>的定義如下:
public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e)
where TEventArgs : EventArgs
3.2 事件偵聽器
Consumer類用作事件偵聽器丈攒。這個類訂閱了CarDealer類的事件,并定義了NewCarIsHere方法授霸,該方法滿足EventHandler<CarInfoEventArgs>委托的要求巡验,其參數(shù)類型是object和CarInfoEventArgs:
public class Consumer
{
private string name;
public Consumer(string name)
{
this.name = name;
}
public void NewCarIsHere(object sender, CarIInfoEventArgs e)
{
Console.WriteLine("{0}: car {1} is new", name, e.Car);
}
}
現(xiàn)在需要連接事件發(fā)布程序和訂閱器。為此使用CarDealer類的NewCarInfo事件碘耳,通過“+=”創(chuàng)建一個訂閱显设。消費者訂閱了事件,接著消費者也訂閱了事件辛辨,然后通過“-=”取消了訂閱捕捂。
static void Main(string[] args)
{
ar dealer = new CarDealer();
var michael = new Consumer("Michael");
dealer.NewCarInfo += michael.NewCarIsHere;
dealer.NewCar("Mercedes");
var nick = new Consumer("Nick");
dealer.NewCarInfo += nick.NewCarIsHere;
dealer.NewCar("Ferrari");
dealer.NewCarInfo -= michael.NewCarIsHere;
dealer.NewCar("Toyota");
}
3.3 弱事件
通過事件,直接連接到發(fā)布程序和偵聽器斗搞。但垃圾回收有一個問題指攒。例如,如果偵聽器不再直接飲用僻焚,發(fā)布程序就仍有一個引用允悦。垃圾回收器不能清空偵聽器占用的內(nèi)存,因為發(fā)布程序仍保有一個引用虑啤,會針對偵聽器觸發(fā)事件隙弛。
這種強連接可以通過弱事件模式來解決架馋,即使用WeekEventManager作為發(fā)布程序和偵聽器之間的中介。
1. 弱事件管理器
要使用弱事件驶鹉,需要創(chuàng)建一個派生自WeekEventManager類的類绩蜻。WeekEventManager類
public class WeakCarInfoEventManager : WeakEventManager
{
public static WeakCarInfoEventManager CurrentManager
{
get
{
WeakCarInfoEventManager manager = GetCurrentManager(typeof(WeakCarInfoEventManager))
as WeakCarInfoEventManager;
if (manager == null)
{
manager = new WeakCarInfoEventManager();
SetCurrentManager(typeof(WeakCarInfoEventManager), manager);
}
return manager;
}
}
public static void AddListener(object source, IWeakEventListener listener)
{
CurrentManager.ProtectedAddListener(source, listener);
}
protected override void StartListening(object source)
{
(source as CarDealer).NewCarInfo += CarDealer_NewCarInfo;
}
void CarDealer_NewCarInfo(object sender, CarIInfoEventArgs e)
{
DeliverEvent(sender, e);
}
protected override void StopListening(object source)
{
(source as CarDealer).NewCarInfo -= CarDealer_NewCarInfo;
}
}
對于發(fā)布程序類CarDealer,不需要做任何修改室埋,其實現(xiàn)代碼與前面相同办绝。
2. 事件偵聽器
偵聽器需要改為實現(xiàn)IWeekEventListener接口。這個接口定義了ReceiveWeekEvent()姚淆,觸發(fā)事件時孕蝉,從弱事件管理器中調(diào)用這個方法。在該方法的實現(xiàn)代碼中腌逢,應從觸發(fā)的事件中調(diào)用NewCarIsHere()方法降淮。
public class Consumer : IWeakEventListener
{
private string name;
public Consumer(string name)
{
this.name = name;
}
public void NewCarIsHere(object sender, CarIInfoEventArgs e)
{
Console.WriteLine("{0}: car {1} is new", name, e.Car);
}
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
NewCarIsHere(sender, e as CarIInfoEventArgs);
return true;
}
}
在Main方法中,連接發(fā)布程序和偵聽器搏讶,該連接現(xiàn)在使用WeekCarInfoEventManager類的AddListener()和RemoveListener()靜態(tài)方法佳鳖。
static void Main(string[] args)
{
var dealer = new CarDealer();
var michael = new Consumer("Michael");
WeakCarInfoEventManager.AddListener(dealer, michael);
dealer.NewCar("Mercedes");
var nick = new Consumer("Nick");
WeakCarInfoEventManager.AddListener(dealer, nick);
dealer.NewCar("Ferrari");
WeakCarInfoEventManager.AddListener(dealer, michael);
}