65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 课程 - 第5课:C# 示例中的事件、委托、委托链

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (30投票s)

2016年2月8日

CPOL

10分钟阅读

viewsIcon

53574

downloadIcon

1246

这是我系列讲座的第五讲,内容涉及委托和事件。

全部课程集


引言

在本文中,我将回顾一个相当敏感的话题,它既难以理解又难以解释。我们将讨论事件、委托和委托链。我不确定我对这个主题的解释是否对每个人都非常清楚。当我给我的学生讲这门课时,我们进行了很多讨论,并在这些讨论中澄清了许多事情。在这里,我们无法面对面交流,我只能用文字来分享我的想法。希望本文能帮助您更好地理解其主题。如果您觉得我的文章不够清晰或不完整,请尝试其他来源和示例。

事件

事件是您可以在类中定义的成员类型。有关C#中类及其成员的更多信息,您可以在我的文章此处阅读。定义事件的类型可以通知其他对象关于与其或其对象发生的一些特定情况。例如,KeyboardKey类可以定义一个PressedDown事件。现在,每个想要知道何时按下按键的对象都应该订阅PressedDown事件,并且每次事件发生时它都会收到通知。这个功能是通过事件类型成员实现的。事件功能广泛用于UI实现中,当一些图形控件由于用户操作或数据更改而发送通知时。现代UI库是基于事件概念构建的,但UI并不是唯一常用事件的地方。事件是类通知其他对象其状态变化以及其他对象需要知道这种变化的最简单和最方便的方式。事件模型是基于委托构建的。我们将在本文后面详细回顾委托,对于事件的解释,我们将在不深入细节的情况下使用委托。

您可以在下面看到解释C#如何处理事件的序列图。在我们的示例中,我们有3个类:CellPhone、CellPhoneCallsLog和CallHandler。CellPhone类有一个NewCallEvent事件,当有新呼叫从网络传入时会触发该事件。当我们创建CellPhoneCallsLog和CallHandler的对象时,它们订阅NewCallEvent,这是动作#1。当CellPhone收到NewCallEvent时,这是动作#2,它会通知所有订阅者,这是动作#3。

让我们进一步,将下面的示例开发为用C#语言编写的真实类。

步骤1 声明事件成员:要声明事件成员,您需要使用event关键字。大多数事件都是开放的,具有公共可见性。事件声明的格式如下:

可见性修饰符 + event 关键字 + 事件类型 + 事件名称

在我们的例子中,声明是

public event EventHandler<CallEventArgs> NewCallEvent;

让我们回顾一下事件声明的每个部分。

  • 可见性修饰符 - 与任何类型成员一样,您需要使用可见性修饰符为事件定义可见性。如前所述,事件通常使用公共可见性。
  • 事件关键字 - 这里一切都很清楚,只需在键盘上输入event即可。
  • 事件类型 - 这是事件声明中非常重要的类型。事件类型是事件的委托类型。此事件的订阅者应提供一个与EventType委托原型对应的方法。事件类型委托也有特定的格式。

可见性修饰符 + delegate 关键字 + void 关键字 + 类型名称 + 两个参数:第一个参数类型为 object,第二个参数类型为 EventArgs(关于 EventArgs 请参见下一步)。您可以定义自己的符合上述格式的委托类型,但有一个标准的通用 EventHandler 委托定义如下:

public delegate void EventHandler<TEventArgs> (Object sender, TEventArgs e) where TEventArgs: EventArgs;

在大多数情况下,使用它就足够了。

  • 事件名称 - 是您希望赋予事件的名称。

步骤2 EventArgs:当事件被触发时,包含事件的对象应将附加信息传递给接收事件通知的对象。无论您是否想传递一些附加信息,您都必须将派生自EventArgs类或EventArgs类的对象放入EventHandler委托中。如果您没有传递任何附加信息,则无需创建EventArgs类的新对象,而是使用EventArgs.Empty属性。如果您决定向事件订阅者传递一些附加信息,则需要实现自己的类。该类有一些规则:

  1. 它必须派生自EventArgs类。
  2. 其名称必须以EventArgs结尾。如我们的示例中所示,它为CallEventArgs。
  3. 您想要共享的所有数据都应该通过属性访问。
  4. 我建议对EventArgs类使用只读字段。这在我下面提供的示例中是如何实现的。

步骤3 定义负责通知订阅者事件的方法:您需要定义一个特定的方法来负责通知事件。这应该是一个受保护的虚拟方法,它带有一个参数,即包含事件附加信息的EventArgs对象(或您定义的类)。此方法的默认实现会检查事件是否有订阅者并调用它。根据Jeff Richter和其他来源的说法,最好将事件的引用复制到临时变量,然后再调用它。这将防止在您尝试从一个线程调用,而第二个线程在调用之前将引用设置为null时出现NullReferenceException。在我们的例子中,实现看起来像这样:

protected virtual void OnNewCallEvent(CallEventArgs e)
{
         EventHandler<CallEventArgs> temp = System.Threading.Volatile.Read(ref NewCallEvent);
         if (temp != null)
          temp(this, e);
}

步骤4 定义将触发事件的方法:一旦我们完成了事件定义,我们需要创建一个方法,将一些输入数据转换为我们想要触发的事件。这里没有特定的规则,您可以从类的任何方法中调用步骤3中描述的虚拟函数来触发事件。

步骤5 创建将接收事件通知的类:现在我们有了一个创建和初始化事件的类,让我们创建一个接收该事件的类。该类应该具有与事件处理委托签名对应的方法。当事件发生时,此函数实际上会处理一些事情。在我们的例子中,这是:

public void AddNewCallToLog(object sender, CallEventArgs e)
{
                Console.WriteLine("Adding to log call with following data:");
                Console.WriteLine("Name:" + e.CallerName);
                Console.WriteLine("Number:" + e.CallerNumber);
                Console.WriteLine("Time:" + e.CallStartTime.ToString());
}

这个类应该能够接收定义了事件的类的对象,并将委托方法连接或断开到事件。我将它们简化,下面是示例:

public void AttachListener(CellPhone phone)
{
               phone.NewCallEvent += AddNewCallToLog;
}
public void DetachListener(CellPhone phone)
{
               phone.NewCallEvent -= AddNewCallToLog;
}

完成所有5个步骤后,我们可以尝试测试我们创建的内容。下面的代码演示了我们上面步骤中描述的内容。

        ///////////////////////////////////////////////EVENTS////////////////////////////////////////////
        internal sealed class CallEventArgs: EventArgs
        {
            private readonly string   m_CallerName;
            private readonly DateTime m_CallStartTime;
            private readonly string m_CallerNumber;
            public CallEventArgs(string callername, string callernumber, DateTime starttime)
            {
                m_CallerName = callername;
                m_CallerNumber = callernumber;
                m_CallStartTime = starttime;
            }
            public string CallerName
            {
                get { return m_CallerName; }
            }
            public string CallerNumber
            {
                get { return m_CallerNumber; }
            }
            public DateTime CallStartTime
            {
                get { return m_CallStartTime; }
            }
        }
        internal class CellPhone
        {
            public event EventHandler<CallEventArgs> NewCallEvent;
            protected virtual void OnNewCallEvent(CallEventArgs e)
            {
                //here we make a copy of event to make sure we don't
                //call on null
                EventHandler<CallEventArgs> temp = System.Threading.Volatile.Read(ref NewCallEvent);
                if (temp != null)
                    temp(this, e);
            }
            public void NewCallHappened(string username, string usernumber, DateTime time)
            {
                //create event data
                CallEventArgs eventData = new CallEventArgs(username, usernumber, time);
                //raise the event
                OnNewCallEvent(eventData);
            }
        }
        internal sealed class CellPhoneCallsLog
        {
            public void AttachListener(CellPhone phone)
            {
                phone.NewCallEvent += AddNewCallToLog;
            }
            public void DetachListener(CellPhone phone)
            {
                phone.NewCallEvent -= AddNewCallToLog;
            }
            private void AddNewCallToLog(object sender, CallEventArgs e)
            {
                Console.WriteLine("I'm CallsLog and handling this call by adding to log following data:");
                Console.WriteLine("Name:" + e.CallerName);
                Console.WriteLine("Number:" + e.CallerNumber); 
                Console.WriteLine("Time:" + e.CallStartTime.ToString());
            }
        }
        internal sealed class CallHandler 
        {
            public CallHandler(CellPhone phone)
            {
                phone.NewCallEvent += HandleTheCall;
            }
            private void HandleTheCall(object sender, CallEventArgs e)
            {
                Console.WriteLine("I'm CallHandler and handling this call by:");
                Console.WriteLine("-ringing music");
                Console.WriteLine("-vibrate");
                Console.WriteLine("-show caller information at screen");
            }
            public void DetachFromNewCallEvent(CellPhone phone)
            {
                phone.NewCallEvent -= HandleTheCall;
            }
        }
Usage:
            Console.WriteLine("---------------------------------EVENTS------------------------:");
            CellPhone phone = new CellPhone();
            CellPhoneCallsLog log = new CellPhoneCallsLog();
            CallHandler handler = new CallHandler(phone);
            log.AttachListener(phone);
            Console.WriteLine("----------------Calling first time with two listeners:");
            phone.NewCallHappened("sergey", "1234567", DateTime.Now);
            handler.DetachFromNewCallEvent(phone);
            Console.WriteLine("----------------Calling again without CallHandler:");
            phone.NewCallHappened("sergey", "1234567", DateTime.Now);

一旦事件被定义并且您构建了您的项目,.NET编译器就会生成注册和注销事件的代码。我不会在这里深入讨论这些细节,但如果您想了解它是如何工作的,我建议您阅读一些解释此内容的资料。您可以从Jeff Richter的《CLR via C#》一书开始。简而言之,委托的声明会生成两个方法:add_<Event Name> 和 remove_<Event Name>。这些方法实现了事件订阅的功能。您无需每次都编写这些函数,您只需在类中定义委托,其余的一切.NET都会以标准方式为您完成。

委托示例

委托是C#中所谓回调函数的名称。回调在不同的编程语言中成功使用了许多年,C#也不例外。如果某些编程语言中使用回调很困难且是一个非常敏感的领域,那么在C# .NET中,开发人员让您的生活变得轻松。与其他语言相比,C#对委托有很好的支持。它们具有更广泛的功能,并且比非托管C++等其他编程语言更强大、更安全。回调在.NET中非常常用:窗口状态、菜单项更改、文件系统更改、异步操作等,这些都是您可以注册函数以接收通知的地方。下面我将展示什么是委托以及如何在您的程序中使用它。

让我们从一个例子开始,在C#中创建委托。

  1. 首先,要使用任何委托,您需要声明一个新的委托类型并指定回调函数的签名。为此,您需要使用delegate关键字。
    internal delegate string DelegateForOutput(string s);

    上面我声明了一个委托类型,它为返回字符串类型并接收一个字符串参数作为输入函数创建了签名。

  2. 一旦我们有了委托类型,我们就应该实现调用它的函数。
  3. 下一步是将委托传递给执行它的函数。在这里,我们可以使用静态方法和对象方法。如果您想将静态方法作为委托传递,您只需定义一个符合委托签名的静态函数,并将其传递给调用委托的函数。如果您想传递实例的委托,您可以像静态方式一样做,但使用对象名称而不是类型名称。

下面的代码演示了您在本文这一部分中阅读的内容。

       internal delegate string DelegateForNotification(string s);

        internal class Notificator
        {
            public void DemoNotification(string s, DelegateForNotification outputFunction)
            {
                Console.WriteLine("we're going to process string: " + s);
                if (outputFunction != null) //if we have callbacks let's call them
                {
                    Console.WriteLine("Function that is used as delegate is: " + outputFunction.Method);
                    Console.WriteLine("Delegate output is: " + outputFunction(s));
                }
                else 
                {
                    Console.WriteLine("Sorry, but no processing methods were registered!!!");
                }
            }
        }
        internal class NotificationHandler 
        {
            public string SendNotificationByMail(string s)
            {
                Console.WriteLine("We are sending notification:\"" + s + "\" by MAIL");
                return "MAIL NOTIFICATION IS SENT";
            }
            public string SendNotificationByFax(string s)
            {
                Console.WriteLine("We are sending notification: \"" + s + "\" by FAX");
                return "FAX NOTIFICATION IS SENT";
            }
            public static string SendNotificationByInstantMessenger(string s)
            {
                Console.WriteLine("We are sending notification: \"" + s + "\" by IM");
                return "IM NOTIFICATION IS SENT";
            }
        }

用法

//------------------DELEGATES-----------
Console.WriteLine("---------------------------------DELEGATES------------------------:");
Notificator notificator = new Notificator();
NotificationHandler notification_handler = new NotificationHandler();
//calling static delegate
notificator.DemoNotification("static example", null);//nothing happen here we don't pass any handler
notificator.DemoNotification("static example", new DelegateForNotification(NotificationHandler.SendNotificationByInstantMessenger));//nothing happen here
//calling instance delegate
notificator.DemoNotification("instance example", null);//nothing happen here we don't pass any handler
notificator.DemoNotification("instance example", new DelegateForNotification(notification_handler.SendNotificationByMail));
notificator.DemoNotification("instance example", new DelegateForNotification(notification_handler.SendNotificationByFax));
//passing null instead of correct value, it builds well
NotificationHandler nullhandler = null;
try 
{
notificator.DemoNotification("instance example", new DelegateForNotification(nullhandler.SendNotificationByFax));
}
catch (System.ArgumentException e)
{
Console.WriteLine("We've tried to pass null object for delegate and catched: " + e.Message);
}

委托的幕后

当我们在上一节中声明和使用委托时,一切看起来都很简单。这是因为.NET为我们做了大量的后台工作,使委托的使用变得简单。一旦您在代码中声明了委托,CLR在编译时就会为该委托生成一个单独的类。这个类在您的代码中是不可见的,但如果您反汇编您的二进制文件,您会看到它。所有委托类都派生自System.MulticastDelegate。从这个类中,每个委托都接收到4个方法:

  • 构造函数 - 接收一个对象和指向函数的指针。
  • Invoke - 在上面的例子中,当我调用 outputFunction(s) 时,没有这样的函数,在幕后,编译器将其替换为 outputFunction.Invoke(s)。Invoke 的签名与所需方法的签名相同。在 Invoke 内部,编译器会在 _target 上调用 _methodPtr(有关它们的描述,请参见下文)。
  • BeginInvoke - 不太相关,因为这种异步工作方式已过时。
  • EndInvoke - 不太相关,因为这种异步工作方式已过时。

从MulticastDelegate派生出来的每个委托不仅包含方法,还包含字段。其中最重要的字段是:

  • _target - 如果委托用于静态方法,则此字段为null;但如果用于实例方法,则此字段包含对具有委托方法的对象的引用。
  • _methodPtr - 保存回调方法的标识符。
  • _invocationList - 通常此值为null,它用于构建委托链。

由于委托是一个类,您可以在任何可以声明类的地方声明委托。为委托生成的类具有与委托本身相同的可见性修饰符。

委托链

委托本身就很酷,但你可以用它们做更酷更有用的事情,那就是构建委托链。链是一组委托,它能够调用这些委托所代表的所有方法。当你需要一个接一个地调用多个委托时,委托链是非常方便的方式。它帮助你为此编写更短更高效的代码。要使用链,你需要声明一个委托类型的变量,并且不要给它赋任何方法。我们使用null作为该变量的初始初始化。一旦定义了链的变量,你可以使用两种选项向链中添加更多委托:使用Delegate类的静态方法Combine,或者使用+=运算符,它简化了链的组合。从链中移除委托的相反方法是使用Delegate类的Remove方法或-=运算符。每次我们向链中添加新委托时,_invocationList(见上一节)——一个委托指针数组——都会增加一个成员。一旦我们把委托链传递给等待委托的函数,它就会分析_invocationList是否不等于null,并与委托链而不是单个委托一起工作。它按照它们添加到链中的顺序调用链中的所有委托。下面的代码演示了基于我们在第3节代码中创建的类的委托链的使用。

//------------------DELEGATES CHAIN-----------
Console.WriteLine("-----------------------------DELEGATES CHAIN--------------------:");
DelegateForNotification dlg1 = new DelegateForNotification(NotificationHandler.SendNotificationByInstantMessenger);
DelegateForNotification dlg2 = new DelegateForNotification(notification_handler.SendNotificationByMail);
DelegateForNotification dlg3 = new DelegateForNotification(notification_handler.SendNotificationByFax);

DelegateForNotification dlg_chain = null;
dlg_chain += dlg1;
dlg_chain += dlg2;
dlg_chain = (DelegateForNotification)Delegate.Combine(dlg_chain,dlg3);//more complex way to combine delegate to chain

//you can view invocation list
Delegate[] list = dlg_chain.GetInvocationList();
foreach (Delegate del in list)
{
    Console.WriteLine("Method: " + del.Method.ToString());
}
notificator.DemoNotification("chain example", dlg_chain);//calling notification to chain from 3 functions

来源

  1. Jeffrey Richter - CLR via C#
  2. Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
  3. https://msdn.microsoft.com
© . All rights reserved.