C#基礎(chǔ)提升系列——C#委托

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)用 DelegateMulticastDelegate 類的成員的方法調(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.MultiplyByTwoMathOperations_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、gotocontinue)調(diào)到該匿名方法的外部,也不能在匿名方法的外部使用跳轉(zhuǎn)語句調(diào)到匿名方法的內(nèi)部。并且不能訪問在匿名方法外部使用的refout參數(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.DelegateSystem.MulticastDelegate 類本身不是委托類型。 它們?yōu)樗刑囟ㄎ蓄愋吞峁┗A(chǔ)。 相同的語言設(shè)計過程要求不能聲明派生自 DelegateMulticastDelegate 的類。 C# 語言規(guī)則禁止這樣做。

相反,C# 編譯器會在你使用 C# 語言關(guān)鍵字聲明委托類型時,創(chuàng)建派生自 MulticastDelegate 的類的實例。

要記住的首要且最重要的事實是,使用的每個委托都派生自 MulticastDelegate。 多播委托意味著通過委托進行調(diào)用時,可以調(diào)用多個方法目標。

參考資源

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容