C# 委托
委托是類型安全的類,它定義了返回類型和參數(shù)的類型,委托類可以包含一個或多個方法的引用??梢允褂胠ambda表達式實現(xiàn)參數(shù)是委托類型的方法。
委托
當需要把一個方法作為參數(shù)傳遞給另一個方法時,就需要使用委托。委托是一種特殊類型的對象,其特殊之處在于,我們以前定義的所有對象都包含數(shù)據(jù),而委托包含的只是一個或多個方法的地址。
聲明委托類型
聲明委托類型就是告訴編譯器,這種類型的委托表示的是哪種類型的方法。語法如下:
delegate void delegateTypeName[<T>]([參數(shù)列表]);
聲明委托類型時指定的參數(shù),就是該委托類型引用的方法對應(yīng)的參數(shù)。
//聲明一個委托類型
private delegate void IntMethodInvoker(int x);
//該委托表示的方法有兩個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ù)信息和返回類型等全部細節(jié)。聲明委托類型的語法和聲明方法的語法類似,但沒有方法體,并且需要指定delegate關(guān)鍵字。
委托實現(xiàn)為派生自基類System.MulticastDelegate的類,System.MulticastDelegate有派生自基類System.Delegate。因此定義委托類型基本上是定義一個新類,所以可以在定義類的任何相同地方定義委托類型。(可以在類的內(nèi)部定義委托類型,也可以在任何類的外部定義,還可以在命名空間中把委托定義為頂層對象)。
我們從“delegate”關(guān)鍵字開始,因為這是你在使用委托時會使用的主要方法。 編譯器在你使用
delegate關(guān)鍵字時生成的代碼會映射到調(diào)用 Delegate 和 MulticastDelegate 類的成員的方法調(diào)用。可以在類中、直接在命名空間中、甚至是在全局命名空間中定義委托類型。
建議不要直接在全局命名空間中聲明委托類型(或其他類型)。
使用委托
定義委托類型之后,可以創(chuàng)建該類型的實例。 為了便于說明委托是如何將方法進行傳遞的,針對上述的三個委托類型,分別定義三個方法:
static void ShowInt(int x)
{
Console.WriteLine("這是一個數(shù)字:"+x);
}
static double ShowSum(double first,double second)
{
return first + second;
}
//最后一個委托,直接可以使用int.ToString()方法,所以此處不再定義
調(diào)用委托有兩種形式,一種形式是實例化委托,并在委托的構(gòu)造函數(shù)中傳入要引用的方法名(注意僅僅是方法名,不需要帶參數(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)方法,所以需要指定實例a和方法名ToString
GetString showString = a.ToString;
string str=showString();
Console.WriteLine("使用委托調(diào)用a.ToString()方法:"+str);
}
在使用委托調(diào)用引用的方法時,委托實例名稱后面的小括號需要傳入要調(diào)用的方法的參數(shù)信息。實際上,給委托實例提供圓括號的調(diào)用和使用委托類的Invoke()方法完全相同。委托實例showSumMethod最終會被解析為委托類型的一個變量,所以C#編譯器會用showSumMethod.Invoke()代替showSumMethod()。
委托實例可以引用任何類型的任何對象上的實例方法或靜態(tài)方法,只要方法的簽名匹配委托的簽名即可。(所謂簽名,指的是定義方法或委托時,指定的參數(shù)列表和返回類型)
簡單的委托示例
后面的內(nèi)容將會基于此示例進行擴展,首先定義一個簡單的數(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;
}
}
然后定義一個引用上述方法的委托:
delegate double DoubleOp(double x);
如果要使用該委托的話,對應(yīng)的代碼為:
DoubleOp op = MathOperations.MultiplyByTwo;
op(double_num);// 假設(shè)double_num為一個double類型的變量
但是很多時候,我們并不是直接這樣使用,而是將委托實例作為一個方法(假設(shè)該方法為A)的參數(shù)進行傳入,并且將委托實例引用的方法的參數(shù) 作為另一個參數(shù)傳遞給該方法A。將上述代碼進行封裝轉(zhuǎn)換:
static void ShowDouble(DoubleOp op, double double_num)
{
double result = op(double_num);
Console.WriteLine("值為:"+result);
}
調(diào)用該方法:
ShowDouble(MathOperations.MultiplyByTwo, 3);
使用委托一個好的思路就是,先定義普通方法,然后針對該方法定義一個引用該方法的委托,然后寫出對應(yīng)的委托使用代碼,接著再將使用的代碼用一個新定義的方法進行封裝轉(zhuǎn)換,在新的方法參數(shù)中,需要指明委托實例和將要為委托實例引用的方法傳入的參數(shù)(也就是上述示例中的op和double_num),接著就可以在其他地方調(diào)用該方法了。
完整的實例代碼如下:
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>委托表示引用一個void返回類型的方法。 該委托類最多可以為將要引用的方法傳遞16種不同的參數(shù)類型。
泛型Func<T>委托表示引用一個帶有返回值類型的方法。該委托類最多可以為將要引用的方法傳遞16中不同的參數(shù)類型,其中最后一個參數(shù)代表的是將要引用的方法的返回值類型。
泛型Predicate<T> 用于需要確定參數(shù)是否滿足委托條件的情況。 也可將其寫作 Func<T, bool> 。例如:
Predicate<int> pre = b => b > 5;
此處只對Action<T>和Func<T>做詳細說明。
有了這兩個委托類,在定義委托時,就可以省略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}");
}
下面使用一個示例對委托的用途進行說明,首先定義一個普通的方法,該方法是冒泡排序的另一種寫法:
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ù)值,為了擴展 使其支持對其他類型的排序,并且不僅僅是升序,對該方法進行泛型改寫,并使用泛型委托。
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是一個泛型委托,將要引用的方法帶有兩個參數(shù),類型和T相同,值可以來自于sortArray,并返回bool類型值,因此實際調(diào)用該委托時,不用單獨的為泛型類型傳入?yún)?shù),直接使用sortArray中的項即可。
為了更好的調(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());
}
}
多播委托
一個委托包含多個方法的調(diào)用,這種委托稱為多播委托。多播委托可以識別運算符“+”和“+=“(在委托中添加方法的調(diào)用)以及”-“和”-=“(在委托中刪除方法的調(diào)用)。
多播委托實際上是一個派生自
System.MulticastDelegate的類,而System.MulticastDelegate又派生自基類System.Delegate。System.MulticastDelegate的其他成員允許把多個方法調(diào)用鏈接為一個列表。
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}");
}
}
針對上述方法定義一個帶有泛型委托的方法:
private static void ProcessAndDisplayNumber(Action<double> action, double value)
{
Console.WriteLine("調(diào)用ProcessAndDisplayNumber方法:value=" + value);
action(value);
}
使用多播委托的形式進行調(diào)用:
Action<double> operations = MathOperations_V2.MultiplyByTwo;
operations += MathOperations_V2.Square;
ProcessAndDisplayNumber(operations, 3);
ProcessAndDisplayNumber(operations, 4);
ProcessAndDisplayNumber(operations, 5);
上述在調(diào)用方法時,會依次執(zhí)行MathOperations_V2.MultiplyByTwo和MathOperations_V2.Square。
注意:在使用多播委托時,多播委托包含一個逐個調(diào)用的委托集合,一旦通過委托調(diào)用的其中一個方法拋出一個異常,整個迭代就會停止。
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出錯了");
}
}
上述使用了多播委托,一旦One出現(xiàn)了異常,Two并不能夠繼續(xù)執(zhí)行。因為第一個方法拋出了一個異常,委托迭代就會停止,不再調(diào)用Two()方法。為了避免這個問題,應(yīng)自己迭代方法列表。Delegate類定義GetInvocationList()方法,返回Delegate對象數(shù)組,可以迭代這個數(shù)組進行方法的執(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)用出錯了!!");
}
}
}
上述迭代,即使第一個方法出錯,依然就執(zhí)行第二個方法。
匿名方法和Lambda表達式
匿名方法是用作委托的參數(shù)的一段代碼。
string start = "厲害了,";
Func<string, string> print = delegate (string param)
{
return start + param;
};
Console.WriteLine(print("我的國!"));
在該示例中,Func<string,string>委托接受一個字符串參數(shù),返回一個字符串。print是這種委托類型的變量。不要把方法名賦予這個變量,而是使用一段簡單的代碼:前面是關(guān)鍵字delegate,后面是一個字符串參數(shù)。
匿名方法的優(yōu)點是減少了要編寫的代碼,但代碼的執(zhí)行速度并沒有加快。
使用匿名方法時,在匿名方法中不能使用跳轉(zhuǎn)語句(break、goto或continue)調(diào)到該匿名方法的外部,也不能在匿名方法的外部使用跳轉(zhuǎn)語句調(diào)到匿名方法的內(nèi)部。并且不能訪問在匿名方法外部使用的ref和out參數(shù)。
實際使用中,不建議使用上述的方式定義匿名方法,而是使用lambda表達式。
只要有委托參數(shù)類型的地方,就可以使用lambda表達式,將上述示例改為lambda表達式,代碼如下:
//使用Lambda表達式進行匿名方法的定義
string start = "厲害了,";
Func<string, string> lambda = param => start + param;
Console.WriteLine(lambda("我的C#!!!"));
使用lambda表達式規(guī)則:
參數(shù)
只有一個參數(shù)時,可以省略小括號
Func<string, string> oneParam = s => $"將{s}轉(zhuǎn)換為大寫:" + s.ToUpper();
//調(diào)用
Console.WriteLine(oneParam("abc"));
沒有參數(shù)或者有多個參數(shù)時必須使用小括號
//無參數(shù)
Action a = () => Console.WriteLine("無參數(shù)");
a();
//多個參數(shù),在小括號中指定參數(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表達式只有一條語句,在方法塊內(nèi)就不需要花括號({})和return語句,因為編譯器會添加一條隱式的return語句。如果lambda表達式有多條語句,必須顯式的添加花括號或return語句。例如:
Func<string, string, string> joinString = (str1, str2) =>
{
str1 += str2;
return str1.ToUpper();
};
Console.WriteLine(joinString("abc", "def"));
閉包
在lambda表達式的內(nèi)部使用表達式外部的變量,稱為閉包。使用閉包需要注意的一點就是 ,如果在表達式中修改了閉包的值,可以在表達式的外部訪問已修改的值 。
委托和 MulticastDelegate 類
System.Delegate類及其單個直接子類System.MulticastDelegate可提供框架支持,以便創(chuàng)建委托、將方法注冊為委托目標以及調(diào)用注冊為委托目標的所有方法。有趣的是,
System.Delegate和System.MulticastDelegate類本身不是委托類型。 它們?yōu)樗刑囟ㄎ蓄愋吞峁┗A(chǔ)。 相同的語言設(shè)計過程要求不能聲明派生自Delegate或MulticastDelegate的類。 C# 語言規(guī)則禁止這樣做。相反,C# 編譯器會在你使用 C# 語言關(guān)鍵字聲明委托類型時,創(chuàng)建派生自
MulticastDelegate的類的實例。要記住的首要且最重要的事實是,使用的每個委托都派生自
MulticastDelegate。 多播委托意味著通過委托進行調(diào)用時,可以調(diào)用多個方法目標。
參考資源
- 《C#高級編程(第10版)》
- 委托概述
- System.Delegate 和
delegate關(guān)鍵字 - 委托和事件
- 委托和 lambda