C# 委托
委托是類型安全的類捉超,它定義了返回類型和參數(shù)的類型晤愧,委托類可以包含一個(gè)或多個(gè)方法的引用武翎。可以使用lambda表達(dá)式實(shí)現(xiàn)參數(shù)是委托類型的方法榜掌。
委托
當(dāng)需要把一個(gè)方法作為參數(shù)傳遞給另一個(gè)方法時(shí)优妙,就需要使用委托。委托是一種特殊類型的對(duì)象憎账,其特殊之處在于套硼,我們以前定義的所有對(duì)象都包含數(shù)據(jù),而委托包含的只是一個(gè)或多個(gè)方法的地址胞皱。
聲明委托類型
聲明委托類型就是告訴編譯器邪意,這種類型的委托表示的是哪種類型的方法。語(yǔ)法如下:
delegate void delegateTypeName[<T>]([參數(shù)列表]);
聲明委托類型時(shí)指定的參數(shù)反砌,就是該委托類型引用的方法對(duì)應(yīng)的參數(shù)雾鬼。
//聲明一個(gè)委托類型
private delegate void IntMethodInvoker(int x);
//該委托表示的方法有兩個(gè)long型參數(shù),返回類型為double
protected delegate double TwoLongsOp(double first, double second);
//方法不帶參數(shù)的委托宴树,返回string
public delegate string GetString();
public delegate int Comparison<in T>(T left, T right);
(注:我們把上述定義的Comparison<in T>
呆贿、IntMethodInvoker
等統(tǒng)稱為委托類型。)
在定義委托類型時(shí)森渐,必須給出它要引用的方法的參數(shù)信息和返回類型等全部細(xì)節(jié)做入。聲明委托類型的語(yǔ)法和聲明方法的語(yǔ)法類似,但沒(méi)有方法體同衣,并且需要指定delegate
關(guān)鍵字竟块。
委托實(shí)現(xiàn)為派生自基類System.MulticastDelegate
的類,System.MulticastDelegate
有派生自基類System.Delegate
耐齐。因此定義委托類型基本上是定義一個(gè)新類浪秘,所以可以在定義類的任何相同地方定義委托類型。(可以在類的內(nèi)部定義委托類型埠况,也可以在任何類的外部定義耸携,還可以在命名空間中把委托定義為頂層對(duì)象)。
我們從“delegate”關(guān)鍵字開始辕翰,因?yàn)檫@是你在使用委托時(shí)會(huì)使用的主要方法夺衍。 編譯器在你使用
delegate
關(guān)鍵字時(shí)生成的代碼會(huì)映射到調(diào)用 Delegate 和 MulticastDelegate 類的成員的方法調(diào)用。可以在類中喜命、直接在命名空間中沟沙、甚至是在全局命名空間中定義委托類型河劝。
建議不要直接在全局命名空間中聲明委托類型(或其他類型)。
使用委托
定義委托類型之后矛紫,可以創(chuàng)建該類型的實(shí)例赎瞎。 為了便于說(shuō)明委托是如何將方法進(jìn)行傳遞的,針對(duì)上述的三個(gè)委托類型颊咬,分別定義三個(gè)方法:
static void ShowInt(int x)
{
Console.WriteLine("這是一個(gè)數(shù)字:"+x);
}
static double ShowSum(double first,double second)
{
return first + second;
}
//最后一個(gè)委托务甥,直接可以使用int.ToString()方法,所以此處不再定義
調(diào)用委托有兩種形式喳篇,一種形式是實(shí)例化委托敞临,并在委托的構(gòu)造函數(shù)中傳入要引用的方法名(注意僅僅是方法名,不需要帶參數(shù))杭隙,另一種形式是使用委托推斷,即不需要顯式的實(shí)例化委托因妙,而是直接指向要引用的方法名即可痰憎,編譯器將會(huì)自動(dòng)把委托實(shí)例解析為特定的類型。具體示例如下:
public static void Run()
{
int a = 10;
//調(diào)用委托形式一
IntMethodInvoker showIntMethod = new IntMethodInvoker(ShowInt);
showIntMethod(a);
//調(diào)用委托形式二
TwoLongsOp showSumMethod = ShowSum;
double sum= showSumMethod.Invoke(1.23, 2.33);
Console.WriteLine("兩數(shù)之和:"+sum);
//由于int.Tostring()不是靜態(tài)方法攀涵,所以需要指定實(shí)例a和方法名ToString
GetString showString = a.ToString;
string str=showString();
Console.WriteLine("使用委托調(diào)用a.ToString()方法:"+str);
}
在使用委托調(diào)用引用的方法時(shí)铣耘,委托實(shí)例名稱后面的小括號(hào)需要傳入要調(diào)用的方法的參數(shù)信息。實(shí)際上以故,給委托實(shí)例提供圓括號(hào)的調(diào)用和使用委托類的Invoke()
方法完全相同蜗细。委托實(shí)例showSumMethod
最終會(huì)被解析為委托類型的一個(gè)變量,所以C#編譯器會(huì)用showSumMethod.Invoke()
代替showSumMethod()
怒详。
委托實(shí)例可以引用任何類型的任何對(duì)象上的實(shí)例方法或靜態(tài)方法炉媒,只要方法的簽名匹配委托的簽名即可。(所謂簽名昆烁,指的是定義方法或委托時(shí)吊骤,指定的參數(shù)列表和返回類型)
簡(jiǎn)單的委托示例
后面的內(nèi)容將會(huì)基于此示例進(jìn)行擴(kuò)展,首先定義一個(gè)簡(jiǎn)單的數(shù)字操作類MathOperations
静尼,代碼如下:
internal class MathOperations
{
//顯示數(shù)值的2倍結(jié)果
public static double MultiplyByTwo(double value)
{
double result = value * 2;
Console.WriteLine($"{value}*2={result}");
return result;
}
//顯示數(shù)值的乘方結(jié)果
public static double Square(double value)
{
double result = value * value;
Console.WriteLine($"{value}*{value}={result}");
return result;
}
}
然后定義一個(gè)引用上述方法的委托:
delegate double DoubleOp(double x);
如果要使用該委托的話白粉,對(duì)應(yīng)的代碼為:
DoubleOp op = MathOperations.MultiplyByTwo;
op(double_num);// 假設(shè)double_num為一個(gè)double類型的變量
但是很多時(shí)候,我們并不是直接這樣使用鼠渺,而是將委托實(shí)例作為一個(gè)方法(假設(shè)該方法為A)的參數(shù)進(jìn)行傳入鸭巴,并且將委托實(shí)例引用的方法的參數(shù) 作為另一個(gè)參數(shù)傳遞給該方法A。將上述代碼進(jìn)行封裝轉(zhuǎn)換:
static void ShowDouble(DoubleOp op, double double_num)
{
double result = op(double_num);
Console.WriteLine("值為:"+result);
}
調(diào)用該方法:
ShowDouble(MathOperations.MultiplyByTwo, 3);
使用委托一個(gè)好的思路就是拦盹,先定義普通方法鹃祖,然后針對(duì)該方法定義一個(gè)引用該方法的委托,然后寫出對(duì)應(yīng)的委托使用代碼普舆,接著再將使用的代碼用一個(gè)新定義的方法進(jìn)行封裝轉(zhuǎn)換惯豆,在新的方法參數(shù)中池磁,需要指明委托實(shí)例和將要為委托實(shí)例引用的方法傳入的參數(shù)(也就是上述示例中的op和double_num),接著就可以在其他地方調(diào)用該方法了楷兽。
完整的實(shí)例代碼如下:
delegate double DoubleOp(double x);
static void ProcessAndDisplayNumber(DoubleOp action, double value)
{
double result = action(value);
Console.WriteLine($"Value is {value },result of operation is {result}");
}
public static void Run()
{
DoubleOp[] operations = {
MathOperations.MultiplyByTwo,
MathOperations.Square
};
for (int i = 0; i < operations.Length; i++)
{
Console.WriteLine($"Using operations[{i}]:");
ProcessAndDisplayNumber(operations[i], 2);
ProcessAndDisplayNumber(operations[i], 3);
ProcessAndDisplayNumber(operations[i], 4);
}
}
Action<T>
地熄、Func<T>
、Predicate<T>
委托
泛型Action<T>
委托表示引用一個(gè)void
返回類型的方法芯杀。 該委托類最多可以為將要引用的方法傳遞16
種不同的參數(shù)類型端考。
泛型Func<T>
委托表示引用一個(gè)帶有返回值類型的方法。該委托類最多可以為將要引用的方法傳遞16
中不同的參數(shù)類型揭厚,其中最后一個(gè)參數(shù)代表的是將要引用的方法的返回值類型却特。
泛型Predicate<T>
用于需要確定參數(shù)是否滿足委托條件的情況。 也可將其寫作 Func<T, bool>
筛圆。例如:
Predicate<int> pre = b => b > 5;
此處只對(duì)Action<T>
和Func<T>
做詳細(xì)說(shuō)明裂明。
有了這兩個(gè)委托類,在定義委托時(shí)太援,就可以省略delegate
關(guān)鍵字闽晦,采用新的形式聲明委托。
Func<double,double> operations = MathOperations.MultiplyByTwo;
Func<double, double>[] operations2 ={
MathOperations.MultiplyByTwo,
MathOperations.Square
};
static void ProcessAndDisplayNumber(Func<double, double> action, double value)
{
double result = action(value);
Console.WriteLine($"Value is {value },result of operation is {result}");
}
下面使用一個(gè)示例對(duì)委托的用途進(jìn)行說(shuō)明提岔,首先定義一個(gè)普通的方法仙蛉,該方法是冒泡排序的另一種寫法:
public static void Sort(int[] sortArray)
{
bool swapped = true;
do
{
swapped = false;
for (int i = 0; i < sortArray.Length - 1; i++)
{
if (sortArray[i] > sortArray[i + 1])
{
int temp = sortArray[i];
sortArray[i] = sortArray[i + 1];
sortArray[i + 1] = temp;
swapped = true;
}
}
} while (swapped);
}
上述方法中,接收的參數(shù)局限于數(shù)值碱蒙,為了擴(kuò)展 使其支持對(duì)其他類型的排序荠瘪,并且不僅僅是升序,對(duì)該方法進(jìn)行泛型改寫赛惩,并使用泛型委托哀墓。
internal class BubbleSorter
{
public static 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 temp = sortArray[i];
sortArray[i] = sortArray[i + 1];
sortArray[i + 1] = temp;
swapped = true;
}
}
} while (swapped);
}
}
上述方法中的參數(shù)comparison
是一個(gè)泛型委托,將要引用的方法帶有兩個(gè)參數(shù)喷兼,類型和T
相同麸祷,值可以來(lái)自于sortArray
,并返回bool
類型值褒搔,因此實(shí)際調(diào)用該委托時(shí)阶牍,不用單獨(dú)的為泛型類型傳入?yún)?shù),直接使用sortArray
中的項(xiàng)即可星瘾。
為了更好的調(diào)用該方法走孽,定義如下類:
internal class Employee
{
public string Name { get; set; }
public decimal Salary { get; private set; }
public override string ToString() => $"{Name},{Salary:C}";
public Employee(string name, decimal salary)
{
this.Name = name;
this.Salary = salary;
}
//為了匹配Func<T,T,bool>委托,定義如下方法
public static bool CompareSalary(Employee e1, Employee e2) => e1.Salary < e2.Salary;
}
使用該類:
Employee[] employees = {
new Employee("小明",8000),
new Employee("小芳",9800),
new Employee("小黑",4000),
new Employee("小米",13000),
new Employee("小馬",12000)
};
//調(diào)用排序
BubbleSorter.Sort(employees, Employee.CompareSalary);
ForeachWrite(employees); //輸出結(jié)果琳状,該方法的定義如下:
public static void ForeachWrite<T>(T[] list)
{
foreach (T item in list)
{
Console.WriteLine(item.ToString());
}
}
多播委托
一個(gè)委托包含多個(gè)方法的調(diào)用磕瓷,這種委托稱為多播委托。多播委托可以識(shí)別運(yùn)算符“+
”和“+=
“(在委托中添加方法的調(diào)用)以及”-
“和”-=
“(在委托中刪除方法的調(diào)用)。
多播委托實(shí)際上是一個(gè)派生自
System.MulticastDelegate
的類困食,而System.MulticastDelegate
又派生自基類System.Delegate
边翁。System.MulticastDelegate
的其他成員允許把多個(gè)方法調(diào)用鏈接為一個(gè)列表。
internal class MathOperations_V2
{
public static void MultiplyByTwo(double value)
{
double result = value * 2;
Console.WriteLine($"{value}*2={result}");
}
public static void Square(double value)
{
double result = value * value;
Console.WriteLine($"{value}*{value}={result}");
}
}
針對(duì)上述方法定義一個(gè)帶有泛型委托的方法:
private static void ProcessAndDisplayNumber(Action<double> action, double value)
{
Console.WriteLine("調(diào)用ProcessAndDisplayNumber方法:value=" + value);
action(value);
}
使用多播委托的形式進(jìn)行調(diào)用:
Action<double> operations = MathOperations_V2.MultiplyByTwo;
operations += MathOperations_V2.Square;
ProcessAndDisplayNumber(operations, 3);
ProcessAndDisplayNumber(operations, 4);
ProcessAndDisplayNumber(operations, 5);
上述在調(diào)用方法時(shí)硕盹,會(huì)依次執(zhí)行MathOperations_V2.MultiplyByTwo
和MathOperations_V2.Square
符匾。
注意:在使用多播委托時(shí),多播委托包含一個(gè)逐個(gè)調(diào)用的委托集合瘩例,一旦通過(guò)委托調(diào)用的其中一個(gè)方法拋出一個(gè)異常啊胶,整個(gè)迭代就會(huì)停止。
private static void One()
{
Console.WriteLine("調(diào)用One()方法");
throw new Exception("Error in one");
}
static void Two()
{
Console.WriteLine("調(diào)用Two()方法");
}
public static void Run()
{
Action d1 = One;
d1 += Two;
try
{
d1();
}
catch (Exception)
{
Console.WriteLine("調(diào)用d1出錯(cuò)了");
}
}
上述使用了多播委托垛贤,一旦One
出現(xiàn)了異常焰坪,Two并不能夠繼續(xù)執(zhí)行。因?yàn)榈谝粋€(gè)方法拋出了一個(gè)異常聘惦,委托迭代就會(huì)停止某饰,不再調(diào)用Two()
方法。為了避免這個(gè)問(wèn)題善绎,應(yīng)自己迭代方法列表黔漂。Delegate
類定義GetInvocationList()
方法,返回Delegate
對(duì)象數(shù)組涂邀,可以迭代這個(gè)數(shù)組進(jìn)行方法的執(zhí)行:
public static void Run2()
{
Action d1 = One;
d1 += Two;
Delegate[] delegates = d1.GetInvocationList();
foreach (Action d in delegates)
{
try
{
d();
}
catch (Exception)
{
Console.WriteLine("調(diào)用出錯(cuò)了N练隆箱锐!");
}
}
}
上述迭代比勉,即使第一個(gè)方法出錯(cuò),依然就執(zhí)行第二個(gè)方法驹止。
匿名方法和Lambda表達(dá)式
匿名方法是用作委托的參數(shù)的一段代碼浩聋。
string start = "厲害了,";
Func<string, string> print = delegate (string param)
{
return start + param;
};
Console.WriteLine(print("我的國(guó)臊恋!"));
在該示例中衣洁,Func<string,string>
委托接受一個(gè)字符串參數(shù),返回一個(gè)字符串抖仅。print
是這種委托類型的變量坊夫。不要把方法名賦予這個(gè)變量,而是使用一段簡(jiǎn)單的代碼:前面是關(guān)鍵字delegate
撤卢,后面是一個(gè)字符串參數(shù)环凿。
匿名方法的優(yōu)點(diǎn)是減少了要編寫的代碼,但代碼的執(zhí)行速度并沒(méi)有加快放吩。
使用匿名方法時(shí)智听,在匿名方法中不能使用跳轉(zhuǎn)語(yǔ)句(break
、goto
或continue
)調(diào)到該匿名方法的外部,也不能在匿名方法的外部使用跳轉(zhuǎn)語(yǔ)句調(diào)到匿名方法的內(nèi)部到推。并且不能訪問(wèn)在匿名方法外部使用的ref
和out
參數(shù)考赛。
實(shí)際使用中,不建議使用上述的方式定義匿名方法莉测,而是使用lambda表達(dá)式颜骤。
只要有委托參數(shù)類型的地方,就可以使用lambda表達(dá)式悔雹,將上述示例改為lambda表達(dá)式复哆,代碼如下:
//使用Lambda表達(dá)式進(jìn)行匿名方法的定義
string start = "厲害了,";
Func<string, string> lambda = param => start + param;
Console.WriteLine(lambda("我的C#!!!"));
使用lambda表達(dá)式規(guī)則:
參數(shù)
只有一個(gè)參數(shù)時(shí)腌零,可以省略小括號(hào)
Func<string, string> oneParam = s => $"將{s}轉(zhuǎn)換為大寫:" + s.ToUpper();
//調(diào)用
Console.WriteLine(oneParam("abc"));
沒(méi)有參數(shù)或者有多個(gè)參數(shù)時(shí)必須使用小括號(hào)
//無(wú)參數(shù)
Action a = () => Console.WriteLine("無(wú)參數(shù)");
a();
//多個(gè)參數(shù)梯找,在小括號(hào)中指定參數(shù)類型
Func<double, double, double> twoParamsWithTypes = (double x, double y) => x + y;
//調(diào)用
Console.WriteLine("2.3+1.3=" + twoParamsWithTypes(2.3, 1.3));
多行代碼
如果lambda表達(dá)式只有一條語(yǔ)句,在方法塊內(nèi)就不需要花括號(hào)({}
)和return
語(yǔ)句益涧,因?yàn)榫幾g器會(huì)添加一條隱式的return
語(yǔ)句锈锤。如果lambda表達(dá)式有多條語(yǔ)句,必須顯式的添加花括號(hào)或return語(yǔ)句闲询。例如:
Func<string, string, string> joinString = (str1, str2) =>
{
str1 += str2;
return str1.ToUpper();
};
Console.WriteLine(joinString("abc", "def"));
閉包
在lambda表達(dá)式的內(nèi)部使用表達(dá)式外部的變量久免,稱為閉包。使用閉包需要注意的一點(diǎn)就是 扭弧,如果在表達(dá)式中修改了閉包的值阎姥,可以在表達(dá)式的外部訪問(wèn)已修改的值 。
委托和 MulticastDelegate 類
System.Delegate
類及其單個(gè)直接子類System.MulticastDelegate
可提供框架支持鸽捻,以便創(chuàng)建委托呼巴、將方法注冊(cè)為委托目標(biāo)以及調(diào)用注冊(cè)為委托目標(biāo)的所有方法。有趣的是御蒲,
System.Delegate
和System.MulticastDelegate
類本身不是委托類型衣赶。 它們?yōu)樗刑囟ㄎ蓄愋吞峁┗A(chǔ)。 相同的語(yǔ)言設(shè)計(jì)過(guò)程要求不能聲明派生自Delegate
或MulticastDelegate
的類厚满。 C# 語(yǔ)言規(guī)則禁止這樣做府瞄。相反,C# 編譯器會(huì)在你使用 C# 語(yǔ)言關(guān)鍵字聲明委托類型時(shí)碘箍,創(chuàng)建派生自
MulticastDelegate
的類的實(shí)例遵馆。要記住的首要且最重要的事實(shí)是,使用的每個(gè)委托都派生自
MulticastDelegate
丰榴。 多播委托意味著通過(guò)委托進(jìn)行調(diào)用時(shí)货邓,可以調(diào)用多個(gè)方法目標(biāo)。
參考資源
- 《C#高級(jí)編程(第10版)》
- 委托概述
- System.Delegate 和
delegate
關(guān)鍵字 - 委托和事件
- 委托和 lambda