C#中的委托和事件

76 阅读7分钟

先谈概念

委托:一种引用类型,表示具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联。 你可以通过委托实例调用方法。委托用于将方法作为参数传递给其他方法。 事件处理程序就是通过委托调用的方法。

事件:类或对象可以通过事件向其他类或对象通知发生的相关事情。 发送(或引发)事件的类称为“发布者”,接收(或处理)事件的类称为“订阅者”。

委托

我们需要先声明一个委托实例,在C#中,显示声明自定义委托采用delegate关键字,声明方式与声明普通方法相同,需要指定访问范围和返回类型,同时包含访问参数。

同时我们针对委托,声明对应的方法,方法的返回值和参数需要与委托保持一致,若不一致则会在委托传递方法时出现编译错误。

委托执行内部传递方法的方式是使用Invoke方法,此处需注意,C#中同时提供了BeginInvoke和EndInvoke的方法对,用于异步执行内部的方法,具体含义和用法可参考我之前的一篇文章:wacky:浅谈.Net异步编程的前世今生----APM篇

下面我们一起来看一下示例:

class Program
{
    public delegate void DelegateWithNoParams();

    public delegate int DelegateSum(int a, int b);

    static void Main(string[] args)
    {
        DelegateWithNoParams delegate1 = new DelegateWithNoParams(FunctionWithNoParams);
        delegate1.Invoke();
        DelegateSum delegate2 = new DelegateSum(FunctionSum);
        int c = delegate2.Invoke(10, 20);
        Console.WriteLine("带返回值和参数的方法,结果为:" + c);
        Console.Read();
    }

    public static void FunctionWithNoParams()
    {
        Console.WriteLine("无返回值无参数的方法");
    }

    public static int FunctionSum(int a, int b)
    {
        int c = a + b;
        return c;

    }
}

在此示例中,我们分别定义了一个无参数无返回值的委托和一个包含2个参数并返回int类型的委托,分别用于执行两种对应的方法。在两个委托执行对应的Invoke方法之后,会产生以下的结果:

image.png

结果和我们预期一致,程序同步顺序地执行了两个委托并打印出相应的结果。但是看到这里也许你会有一个疑问,既然委托执行时的结果与直接调用方法一致,那么我们为什么还需要使用委托来执行方法呢?

这时我们就要回到最初的定义:委托用于将方法作为参数传递给其他方法。

由于实例化的委托是一个对象,因此可以作为参数传递或分配给一个属性。 这允许方法接受委托作为参数并在稍后调用委托。 这被称为异步回调,是在长进程完成时通知调用方的常用方法。 当以这种方式使用委托时,使用委托的代码不需要知道要使用的实现方法。 功能类似于封装接口提供的功能。

我们一起使用一个比较直观的例子来验证:

class Program
{
    public delegate void Del(string message);
        
    static void Main(string[] args)
    {
        Del handler = new Del(DelegateMethod);
        MethodWithCallback(1,2,handler);
        Console.Read();
    }

    public static void DelegateMethod(string message)
    {
        Console.WriteLine(message);
    }

    public static void MethodWithCallback(int param1, int param2, Del callback)
    {
        callback(string.Format("当前的值为:{0}", (param1 + param2)));
    }
}

在这段代码中,我们声明了一个无返回值委托Del,用于接收传入的消息,并且该委托指向了一个调用控制台的方法DelegateMethod。而后续我们调用MethodWithCallback方法时,无需调用控制台相关方法,而是直接将Del委托的实例作为参数传入,就实现DelegateMethod方法的调用。这个实例就是我们上述提到的异步回调和委托对方法的引用。

image.png

事实上,委托是可以调用多个方法的,这种方式就叫做多播委托,在C#中,我们可以使用+=的运算符,将其他委托附加到当前委托之后,就可以实现多播委托,相关示例如下:

class Program
{
    public delegate void Del();
        
    static void Main(string[] args)
    {
        Del handler = new Del(DelegateMethod1);
        Del handlerNew = new Del(DelegateMethod2);
        handler += handlerNew;
        handler.Invoke();
        Console.Read();
    }

    public static void DelegateMethod1()
    {
        Console.WriteLine("天才第一步!");
    }

    public static void DelegateMethod2()
    {
        Console.WriteLine("天才第二步!");
    }
}

在这个示例中,我们重新编写了一个方法叫DelegateMethod2,同时我们又声明了一个新的委托对象handlerNew指向该方法。接着我们使用+=的方式将handlerNew添加至handler并执行该委托,得到的结果如下:

image.png

如我们先前所料,多播委托把多个方法依次进行了执行。此时如果某个方法发生异常,则不会调用列表中的后续方法,如果委托具有返回值和/或输出参数,它将返回上次调用方法的返回值和参数。与增加方法相对应,若要删除调用列表的方法,则可以使用-=运算符进行操作。

关于委托的理解与常用方式,我们就讲解到这里,事实上,多播委托常用于事件处理中,由此可见,事件与委托有着千丝万缕的联系,下面我们就拉开事件的序幕。

事件

如前文讲解时所说,事件是一种通知行为,因此要分为事件发布者和事件订阅者。而且在.Net中,事件基于EventHandler委托和EventArgs基类的,因此我们在声明事件时,需要先定义一个委托类型,然后使用event关键字进行事件的定义。

相关的示例如下:

public class PublishEvent
{
    public delegate void NoticeHandler(string message);

    public event NoticeHandler NoticeEvent;

    public void Works()
    {
        //触发事件
        OnNoticed();
    }

    protected virtual void OnNoticed()
    {
        if (NoticeEvent != null)
        {
            //传递事件及参数
            NoticeEvent("Notice发布的报警信息!");
        }
    }
}

public class SubscribEvent
{
    public SubscribEvent(PublishEvent pub)
    {
        //订阅事件
        pub.NoticeEvent += PrintResult;
    }
        
    /// <summary>
    /// 订阅事件后的响应函数
    /// </summary>
    /// <param name="message"></param>
    void PrintResult(string message)
    {
        Console.WriteLine(string.Format("已收到{0}!采取措施!",message));
    }
}

class Program
{
    static void Main(string[] args)
    {
        PublishEvent publish = new PublishEvent();
        SubscribEvent subscrib = new SubscribEvent(publish);
        //触发事件
        publish.Works();
        Console.Read();
    }
}

从事例中我们可以看出,我们分别定义了发布者和订阅者相关的类。

在发布者中,我们需要声明一个委托NoticeHandler,然后定义一个此类型的事件NoticeEvent。在定义对象之后,我们需要对事件进行执行,因此有了OnNoticed方法,此方法只用于事件本身的执行。那么什么时候才能执行事件呢?于是我们又有了触发该事件的方法Works,当Works方法被调用时,就会触发NoticeEvent事件。

而在订阅者中,我们需要对NoticeEvent事件进行订阅,因此我们需要发布者的对象PublishEvent,同时需要对它的事件进行订阅。正如我们前文所说,订阅使用+=的方式,与多播委托的使用是一致的,而+=后的对象正是我们需要响应后续处理的方法PrintResult。当事件被触发时,订阅者会接收到该事件,并自动执行响应函数PrintResult。

执行结果如下图所示:

image.png

从执行结果中我们可以看出,在事件被触发后,订阅者成功接收到了发布者发布的事件内容,并进行自动响应,而我们在此过程中从未显式调用订阅者的任何方法,这也是事件模型的本质意义:从发布到订阅。

在微软官方文档中提到,事件是一种特殊的多播委托,只能从声明它的类中进行调用。 客户端代码通过提供对应在引发事件时调用的方法的引用来订阅事件。 这些方法通过事件访问器添加到委托的调用列表中,这也是我们可以使用+=去订阅事件的原因所在,而取消事件则和多播委托一致,使用-=的方式。

关于事件的使用场景还有一种与多线程通知相关的典型用法,具体含义和用法可参考我之前的另一篇文章:wacky:浅谈.Net异步编程的前世今生----EAP篇