C# 委托
委托是類型安全的類,它定義了返回類型和參數的類型,委托類可以包含一個或多個方法的引用。可以使用lambda表達式實現參數是委托類型的方法。
委托
當需要把一個方法作為參數傳遞給另一個方法時,就需要使用委托。委托是一種特殊類型的對象,其特殊之處在於,我們以前定義的所有對象都包含數據,而委托包含的只是一個或多個方法的地址。
聲明委托類型
聲明委托類型就是告訴編譯器,這種類型的委托表示的是哪種類型的方法。語法如下:
delegate void delegateTypeName[<T>]([參數列表]);
聲明委托類型時指定的參數,就是該委托類型引用的方法對應的參數。
//聲明一個委托類型 private delegate void IntMethodInvoker(int x); //該委托表示的方法有兩個long型參數,返回類型為double protected delegate double TwoLongsOp(double first, double second); //方法不帶參數的委托,返回string public delegate string GetString(); public delegate int Comparison<in T>(T left, T right);
(注:我們把上述定義的Comparison<in T>
、IntMethodInvoker
等統稱為委托類型。)
在定義委托類型時,必須給出它要引用的方法的參數信息和返回類型等全部細節。聲明委托類型的語法和聲明方法的語法類似,但沒有方法體,並且需要指定delegate
關鍵字。
委托實現為派生自基類System.MulticastDelegate
的類,System.MulticastDelegate
有派生自基類System.Delegate
。因此定義委托類型基本上是定義一個新類,所以可以在定義類的任何相同地方定義委托類型。(可以在類的內部定義委托類型,也可以在任何類的外部定義,還可以在命名空間中把委托定義為頂層對象)。
我們從“delegate”關鍵字開始,因為這是你在使用委托時會使用的主要方法。 編譯器在你使用
delegate
關鍵字時生成的代碼會映射到調用 Delegate 和 MulticastDelegate 類的成員的方法調用。可以在類中、直接在命名空間中、甚至是在全局命名空間中定義委托類型。
建議不要直接在全局命名空間中聲明委托類型(或其他類型)。
使用委托
定義委托類型之后,可以創建該類型的實例。 為了便於說明委托是如何將方法進行傳遞的,針對上述的三個委托類型,分別定義三個方法:
static void ShowInt(int x) { Console.WriteLine("這是一個數字:"+x); } static double ShowSum(double first,double second) { return first + second; } //最后一個委托,直接可以使用int.ToString()方法,所以此處不再定義
調用委托有兩種形式,一種形式是實例化委托,並在委托的構造函數中傳入要引用的方法名(注意僅僅是方法名,不需要帶參數),另一種形式是使用委托推斷,即不需要顯式的實例化委托,而是直接指向要引用的方法名即可,編譯器將會自動把委托實例解析為特定的類型。具體示例如下:
public static void Run() { int a = 10; //調用委托形式一 IntMethodInvoker showIntMethod = new IntMethodInvoker(ShowInt); showIntMethod(a); //調用委托形式二 TwoLongsOp showSumMethod = ShowSum; double sum= showSumMethod.Invoke(1.23, 2.33); Console.WriteLine("兩數之和:"+sum); //由於int.Tostring()不是靜態方法,所以需要指定實例a和方法名ToString GetString showString = a.ToString; string str=showString(); Console.WriteLine("使用委托調用a.ToString()方法:"+str); }
在使用委托調用引用的方法時,委托實例名稱后面的小括號需要傳入要調用的方法的參數信息。實際上,給委托實例提供圓括號的調用和使用委托類的Invoke()
方法完全相同。委托實例showSumMethod
最終會被解析為委托類型的一個變量,所以C#編譯器會用showSumMethod.Invoke()
代替showSumMethod()
。
委托實例可以引用任何類型的任何對象上的實例方法或靜態方法,只要方法的簽名匹配委托的簽名即可。(所謂簽名,指的是定義方法或委托時,指定的參數列表和返回類型)
簡單的委托示例
后面的內容將會基於此示例進行擴展,首先定義一個簡單的數字操作類MathOperations
,代碼如下:
internal class MathOperations { //顯示數值的2倍結果 public static double MultiplyByTwo(double value) { double result = value * 2; Console.WriteLine($"{value}*2={result}"); return result; } //顯示數值的乘方結果 public static double Square(double value) { double result = value * value; Console.WriteLine($"{value}*{value}={result}"); return result; } }
然后定義一個引用上述方法的委托:
delegate double DoubleOp(double x);
如果要使用該委托的話,對應的代碼為:
DoubleOp op = MathOperations.MultiplyByTwo; op(double_num);// 假設double_num為一個double類型的變量
但是很多時候,我們並不是直接這樣使用,而是將委托實例作為一個方法(假設該方法為A)的參數進行傳入,並且將委托實例引用的方法的參數 作為另一個參數傳遞給該方法A。將上述代碼進行封裝轉換:
static void ShowDouble(DoubleOp op, double double_num) { double result = op(double_num); Console.WriteLine("值為:"+result); }
調用該方法:
ShowDouble(MathOperations.MultiplyByTwo, 3);
使用委托一個好的思路就是,先定義普通方法,然后針對該方法定義一個引用該方法的委托,然后寫出對應的委托使用代碼,接着再將使用的代碼用一個新定義的方法進行封裝轉換,在新的方法參數中,需要指明委托實例和將要為委托實例引用的方法傳入的參數(也就是上述示例中的op和double_num),接着就可以在其他地方調用該方法了。
完整的實例代碼如下:
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
種不同的參數類型。
泛型Func<T>
委托表示引用一個帶有返回值類型的方法。該委托類最多可以為將要引用的方法傳遞16
中不同的參數類型,其中最后一個參數代表的是將要引用的方法的返回值類型。
泛型Predicate<T>
用於需要確定參數是否滿足委托條件的情況。 也可將其寫作 Func<T, bool>
。例如:
Predicate<int> pre = b => b > 5;
此處只對Action<T>
和Func<T>
做詳細說明。
有了這兩個委托類,在定義委托時,就可以省略delegate
關鍵字,采用新的形式聲明委托。
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); }
上述方法中,接收的參數局限於數值,為了擴展 使其支持對其他類型的排序,並且不僅僅是升序,對該方法進行泛型改寫,並使用泛型委托。
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); } }
上述方法中的參數comparison
是一個泛型委托,將要引用的方法帶有兩個參數,類型和T
相同,值可以來自於sortArray
,並返回bool
類型值,因此實際調用該委托時,不用單獨的為泛型類型傳入參數,直接使用sortArray
中的項即可。
為了更好的調用該方法,定義如下類:
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) }; //調用排序 BubbleSorter.Sort(employees, Employee.CompareSalary); ForeachWrite(employees); //輸出結果,該方法的定義如下: public static void ForeachWrite<T>(T[] list) { foreach (T item in list) { Console.WriteLine(item.ToString()); } }
多播委托
一個委托包含多個方法的調用,這種委托稱為多播委托。多播委托可以識別運算符“+
”和“+=
“(在委托中添加方法的調用)以及”-
“和”-=
“(在委托中刪除方法的調用)。
多播委托實際上是一個派生自
System.MulticastDelegate
的類,而System.MulticastDelegate
又派生自基類System.Delegate
。System.MulticastDelegate
的其他成員允許把多個方法調用鏈接為一個列表。
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("調用ProcessAndDisplayNumber方法:value=" + value); action(value); }
使用多播委托的形式進行調用:
Action<double> operations = MathOperations_V2.MultiplyByTwo; operations += MathOperations_V2.Square; ProcessAndDisplayNumber(operations, 3); ProcessAndDisplayNumber(operations, 4); ProcessAndDisplayNumber(operations, 5);
上述在調用方法時,會依次執行MathOperations_V2.MultiplyByTwo
和MathOperations_V2.Square
。
注意:在使用多播委托時,多播委托包含一個逐個調用的委托集合,一旦通過委托調用的其中一個方法拋出一個異常,整個迭代就會停止。
private static void One() { Console.WriteLine("調用One()方法"); throw new Exception("Error in one"); } static void Two() { Console.WriteLine("調用Two()方法"); } public static void Run() { Action d1 = One; d1 += Two; try { d1(); } catch (Exception) { Console.WriteLine("調用d1出錯了"); } }
上述使用了多播委托,一旦One
出現了異常,Two並不能夠繼續執行。因為第一個方法拋出了一個異常,委托迭代就會停止,不再調用Two()
方法。為了避免這個問題,應自己迭代方法列表。Delegate
類定義GetInvocationList()
方法,返回Delegate
對象數組,可以迭代這個數組進行方法的執行:
public static void Run2() { Action d1 = One; d1 += Two; Delegate[] delegates = d1.GetInvocationList(); foreach (Action d in delegates) { try { d(); } catch (Exception) { Console.WriteLine("調用出錯了!!"); } } }
上述迭代,即使第一個方法出錯,依然就執行第二個方法。
匿名方法和Lambda表達式
匿名方法是用作委托的參數的一段代碼。
string start = "厲害了,"; Func<string, string> print = delegate (string param) { return start + param; }; Console.WriteLine(print("我的國!"));
在該示例中,Func<string,string>
委托接受一個字符串參數,返回一個字符串。print
是這種委托類型的變量。不要把方法名賦予這個變量,而是使用一段簡單的代碼:前面是關鍵字delegate
,后面是一個字符串參數。
匿名方法的優點是減少了要編寫的代碼,但代碼的執行速度並沒有加快。
使用匿名方法時,在匿名方法中不能使用跳轉語句(break
、goto
或continue
)調到該匿名方法的外部,也不能在匿名方法的外部使用跳轉語句調到匿名方法的內部。並且不能訪問在匿名方法外部使用的ref
和out
參數。
實際使用中,不建議使用上述的方式定義匿名方法,而是使用lambda表達式。
只要有委托參數類型的地方,就可以使用lambda表達式,將上述示例改為lambda表達式,代碼如下:
//使用Lambda表達式進行匿名方法的定義 string start = "厲害了,"; Func<string, string> lambda = param => start + param; Console.WriteLine(lambda("我的C#!!!"));
使用lambda表達式規則:
參數
只有一個參數時,可以省略小括號
Func<string, string> oneParam = s => $"將{s}轉換為大寫:" + s.ToUpper(); //調用 Console.WriteLine(oneParam("abc"));
沒有參數或者有多個參數時必須使用小括號
//無參數 Action a = () => Console.WriteLine("無參數"); a(); //多個參數,在小括號中指定參數類型 Func<double, double, double> twoParamsWithTypes = (double x, double y) => x + y; //調用 Console.WriteLine("2.3+1.3=" + twoParamsWithTypes(2.3, 1.3));
多行代碼
如果lambda表達式只有一條語句,在方法塊內就不需要花括號({}
)和return
語句,因為編譯器會添加一條隱式的return
語句。如果lambda表達式有多條語句,必須顯式的添加花括號或return語句。例如:
Func<string, string, string> joinString = (str1, str2) => { str1 += str2; return str1.ToUpper(); }; Console.WriteLine(joinString("abc", "def"));
閉包
在lambda表達式的內部使用表達式外部的變量,稱為閉包。使用閉包需要注意的一點就是 ,如果在表達式中修改了閉包的值,可以在表達式的外部訪問已修改的值 。
委托和 MulticastDelegate 類
System.Delegate
類及其單個直接子類System.MulticastDelegate
可提供框架支持,以便創建委托、將方法注冊為委托目標以及調用注冊為委托目標的所有方法。有趣的是,
System.Delegate
和System.MulticastDelegate
類本身不是委托類型。 它們為所有特定委托類型提供基礎。 相同的語言設計過程要求不能聲明派生自Delegate
或MulticastDelegate
的類。 C# 語言規則禁止這樣做。相反,C# 編譯器會在你使用 C# 語言關鍵字聲明委托類型時,創建派生自
MulticastDelegate
的類的實例。要記住的首要且最重要的事實是,使用的每個委托都派生自
MulticastDelegate
。 多播委托意味着通過委托進行調用時,可以調用多個方法目標。