[.NET大牛之路 022] C# 高级:详解委托(一)

一个程序按照人们的预期工作,在代码层面来看就是在执行我们编写的一条条语句,很多语句是以方法调用的方式向一个给定的对象发送请求。然而,很多情况要求一个对象能够使用回调机制与创建它的实体进行通信,尤其在图形用户界面应用程序中,比如按钮控件需要在被点击的情况下调用外部方法。

01 理解委托

使用回调机制,程序员能够配置一个函数来向另一个函数报告状态。通过这种回调机制,Windows 开发者能够处理按钮点击、鼠标移动、菜单选择以及内存中两个实体之间的双向通信等。

在 .NET 中,回调是以一种类型安全和面向对象的方式使用委托实现的。委托是一个类型安全的对象,它指向应用程序中的一个方法或多个方法,该方法可以在以后恰当的时机被调用。

具体来说,一个委托包含下面三个重要的信息:

  • 它所调用的方法的引用(地址)
  • 该方法的参数(如果有)
  • 该方法的返回类型(如果有)

当一个委托对象被创建并获得必要的信息后,它可以在运行时动态地调用它所指向的方法。

注意,.NET 的委托可以指向静态方法或实例方法。

02 定义委托类型

在 C# 中通过使用 delegate 关键字创建一个委托类型,委托的定义必须与它要指向的方法签名相匹配。例如,下面这个委托类型可以指向任何返回一个整数并接受两个整数作为输入参数的方法:

public delegate int BinaryOp(int x, int y);

当 C# 编译器处理委托类型时,它会自动生成一个继承自 System.MulticastDelegate 的密封类。这个类和它的基类 System.Delegate 一起为委托提供了必要的基础结构,使其能够维护以后要调用的方法列表。例如,如果你使用 ildasm.exe 检查上面定义的 BinaryOp 委托,你会发现如下细节:

// -------------------------------------------------------
// TypDefName: SimpleDelegate.BinaryOp
// Extends   : System.MulticastDelegate
// Method #1
// -------------------------------------------------------
//         MethodName: .ctor
//         ReturnType: Void
//         2 Arguments
//                 Argument #1:  Object
//                 Argument #2:  I
// Method #2
// -------------------------------------------------------
//         MethodName: Invoke
//         ReturnType: I4
//         2 Arguments
//                 Argument #1:  I4
//                 Argument #2:  I4
//         2 Parameters
//                 (1) ParamToken : Name : x flags: [none]
//                 (2) ParamToken : Name : y flags: [none] //
// Method #3
// -------------------------------------------------------
//         MethodName: BeginInvoke
//         ReturnType: Class System.IAsyncResult
//         4 Arguments
//                 Argument #1:  I4
//                 Argument #2:  I4
//                 Argument #3:  Class System.AsyncCallback
//                 Argument #4:  Object
//         4 Parameters
//                 (1) ParamToken : Name : x flags: [none]
//                 (2) ParamToken : Name : y flags: [none]
//                 (3) ParamToken : Name : callback flags: [none]
//                 (4) ParamToken : Name : object flags: [none]
//
// Method #4
// -------------------------------------------------------
//         MethodName: EndInvoke
//         ReturnType: I4 (int32)
//         1 Arguments
//                 Argument #1:  Class System.IAsyncResult
//         1 Parameters
//                 (1) ParamToken : Name : result flags: [none]

正如你所看到的,编译器生成的 BinaryOp 类定义了三个公共方法。 Invoke() 是 .NET Core 用来以同步的方式调用委托对象所维护的每个方法,这意味着调用者必须等待调用完成后才能继续。但,我们在代码中不需要显式调用 Invoke() 方法。

注:虽然生成 BeginInvoke() EndInvoke() 方法,但在 .NET Core 中已经被弃用了,它是 .NET Framwork 中的方法。

下面是编译器生成的 BinaryOp 委托类的关键代码:

sealed class BinaryOp : System.MulticastDelegate
{
  public int Invoke(int x, int y);
  ...
}

首先,注意到为 Invoke() 方法定义的参数和返回类型与 BinaryOp 委托的定义是完全一致的。

总而言之,C# 委托类型定义的是一个密封的类,它有一个编译器生成的方法,其参数和返回类型是基于委托声明的。

03 MulticastDelegate 和 Delegate 类

当你使用 C# 的 delegate 关键字声明一个委托类型时,就间接地声明了一个派生自 System.MulticastDelegate 类的类型。这个类为子类提供了对一个列表的访问方法,该列表包含了由委托对象维护的方法的引用。还提供了一些额外的方法,如一些操作符重载等。下面是 System.MulticastDelegate 的一些主要成员。

public abstract class MulticastDelegate : Delegate
{
  public sealed override Delegate[] GetInvocationList();
  public static bool operator == (MulticastDelegate d1, MulticastDelegate d2);
  public static bool operator != (MulticastDelegate d1, MulticastDelegate d2);
  ...
}

System.MulticastDelegate 又继承了 System.Delegate 类,下面是这个类的部分成员:

public abstract class Delegate : ICloneable, ISerializable
{
  public MethodInfo Method { get; }
  public object? Target { get; }
  public static Delegate Combine(params Delegate[] delegates);
  public static Delegate Combine(Delegate a, Delegate b);
  public static Delegate Remove(Delegate source, Delegate value);
  public static Delegate RemoveAll(Delegate source, Delegate value);
  public static bool operator ==(Delegate d1, Delegate d2);
  public static bool operator !=(Delegate d1, Delegate d2);
  ...
}

其中 Method 代表委托指向的方法; Target 代表委托指向的方法所在的实体对象,如果为空表明是静态方法;当对委托进行 += -= 操作时,会分别自动调用 Combine Remove 方法。

我们不能在代码中直接继承这两个基类,当我们使用 delegate 关键字创建一个委托类型时,就已经间接地创建了一个继承自 MulticastDelegate 的类。

04 简单委托示例

前面说了这么多,下面我们来写一个简单完整的委托示例:

// SimpleMath.cs
namespace SimpleDelegate
{
  // 这个类定义两个签名相同的方法,后面的 BinaryOp 委托对象将指向它们
  public class SimpleMath
  {
    public static int Add(int x, int y) => x + y;
    public static int Subtract(int x, int y) => x - y;
  }
}

// Program.cs
using System;
using SimpleDelegate;
// 创建 BinaryOp 委托对象并指向 SimpleMath.Add 方法
BinaryOp b = new BinaryOp(SimpleMath.Add);
// 使用委托对象间接调用 Add() 方法
Console.WriteLine("10 + 10 is {0}", b(10, 10));
Console.ReadLine();

// 定义一个委托类型
public delegate int BinaryOp(int x, int y);

在这里,我们创建了一个名为 SimpleMath 的类,它定义了两个静态方法,与 BinaryOp 委托定义的参数及返回类型相匹配。当你想把目标方法分配给一个给定的委托对象时,只需把方法的名称传给委托的构造函数:

BinaryOp b = new BinaryOp(SimpleMath.Add);

然后,我们可以使用一种看起来像直接调用的语法来调用其指向的方法:

Console.WriteLine("10 + 10 is {0}", b(10, 10));

在背后,运行时调用的是编译器生成的一个继承自 MulticastDelegate 基类的 Invoke() 方法。查看 IL 代码,我们可以验证这一点:

.method private hidebysig static void Main(string[] args) cil managed
{
  ...
  callvirt instance int32 BinaryOp::Invoke(int32, int32)
}

C# 并不要求你在你的代码库中明确地调用 Invoke(),尽管下面这样写也是允许的。

Console.WriteLine("10 + 10 is {0}", b.Invoke(10, 10));

另外,.NET 的委托是类型安全的。因此,如果你试图创建一个与指向方法签名不匹配的委托对象,则会有编译时错误。

05 使用委托实现消息订阅

显然,前面的例子纯粹是为了介绍委托,并不能说明委托的实际用途。因此,让我们写一个更实际的委托示例,使用委托实现消息订阅功能。

下面定义一个 Car 类,它可以将其当前的引擎状态告知外部实体:

public class Car
{
    public int CurrentSpeed { get; set; }
    public int MaxSpeed { get; set; } = 100;

    private bool _carIsDead;

    // 1) 定义一个委托类型
    public delegate void CarEngineHandler(string msgForCaller);

    // 2) 定义一个委托类型的成员变量
    private CarEngineHandler _listOfHandlers;

    // 3) 定义一个用于注册调用方法的方法
    public void RegisterWithCarEngine(CarEngineHandler methodToCall)
    {
        _listOfHandlers = methodToCall;
    }
}

在这个例子中,我们把委托类型 CarEngineHandler 的定义放在了 Car 类的内部,语法上这不是必需的。

接下来,请注意我们声明了一个委托类型的私有成员变量 _listOfHandlers 和一个辅助函数 RegisterWithCarEngine(),它允许调用者将一个方法分配到委托的调用列表中。

严格来说,你可以将你的委托成员变量定义为公共的,避免创建额外的注册方法。然而,过度开放的设计可能会造成一些不可控的风险。通过将委托成员变量定义为私有,你就可以强制执行封装服务,并提供一个更加安全的调用方式。

下面实现一个 Accelerate() 方法,使 Car 对象能够向任何订阅的监听器发送与引擎有关的消息:

// 4)  实现 Accelerate() 方法,在特定情况下调用委托的调用列表
public void Accelerate(int delta)
{
    if (_carIsDead)
    {
        _listOfHandlers?.Invoke("对不起,汽车坏了...");
        return;
    }

    CurrentSpeed += delta;
    if (10 == (MaxSpeed - CurrentSpeed))
    {
        _listOfHandlers?.Invoke("小心!要爆炸了!");
    }
    if (CurrentSpeed >= MaxSpeed)
    {
        _carIsDead = true;
    }
    else
    {
        Console.WriteLine("当前速度:{0}", CurrentSpeed);
    }
}

到这,委托相关的的代码就写好了。现在我们在 Program 类中来测试一下:

using System;

Car c1 = new() { MaxSpeed = 100, CurrentSpeed = 10 };

// 告诉汽车当它想要发送消息时要调用哪个方法
c1.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));

// 汽车开始加速
Console.WriteLine("汽车开始加速...");
for (int i = 0; i < 6; i++)
{
    c1.Accelerate(20);
}

Console.ReadLine();

static void OnCarEngineEvent(string msg)
{
    Console.WriteLine("来自汽车的消息 => {0}", msg);
}

在这个示例中,先是向汽车对象注册( RegisterWithCarEngine)了一个监听方法(通过传递一个委托对象),当汽车的速度达到一定的值时,调用委托对象成员变量 _listOfHandlers 以触发外部方法的 OnCarEngineEvent 调用,实现汽车向外部发送消息的功能。

来看看示例的输出结果:

汽车开始加速...
当前速度:30
当前速度:50
当前速度:70
来自汽车的消息 => 小心!要爆炸了!
当前速度:90
来自汽车的消息 => 对不起,汽车坏了...

通过这个示例,我们使用委托实现了一个初步的订阅/通知模型。

本文来自http://cnblogs.com/willick,经授权后发布,本文观点不代表个人技术分享立场,转载请联系原作者。

chaoqiangchaoqiang
上一篇 2021-09-05 10:43
下一篇 2021-10-23 08:29

相关推荐

近期个人博客正在迁移中,原博客请移步此处,抱歉!