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。因此定义委托类型基本上是定义一个新类,所以可以在定义类的任何相同地方定义委托类型。(可以在类的内部定义委托类型,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象)。

使用委托

定义委托类型之后,可以创建该类型的实例。 为了便于说明委托是如何将方法进行传递的,针对上述的三个委托类型,分别定义三个方法:

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());
    }
}

多播委托

一个委托包含多个方法的调用,这种委托称为多播委托。多播委托可以识别运算符“+”和“+=“(在委托中添加方法的调用)以及”-“和”-=“(在委托中删除方法的调用)。

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 类

12-20 22:10