C#基礎提升系列——C#委托


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.DelegateSystem.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.MultiplyByTwoMathOperations_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,后面是一個字符串參數。

匿名方法的優點是減少了要編寫的代碼,但代碼的執行速度並沒有加快。

使用匿名方法時,在匿名方法中不能使用跳轉語句(breakgotocontinue)調到該匿名方法的外部,也不能在匿名方法的外部使用跳轉語句調到匿名方法的內部。並且不能訪問在匿名方法外部使用的refout參數。

實際使用中,不建議使用上述的方式定義匿名方法,而是使用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。 多播委托意味着通過委托進行調用時,可以調用多個方法目標。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM