zoukankan      html  css  js  c++  java
  • 艾伟_转载:把委托说透(3):委托与事件 狼人:

    把委托说透(1)(2)中,先后介绍了委托的语法和本质,本文重点介绍.NET中与委托息息相关的概念——事件。在此之前,首先需要补充(2)中遗漏的一部分内容,即C#在语法上对委托链的支持。

    C#编译器为委托类型提供了+=和-=两个操作符的重载,分别对应Delegate.Combine和Delegate.Remove方法,使用这两个操作符可以大大简化委托链的构造和移除。

    好了,有了+=和-=,我们就可以开始今天的话题了。

    什么是事件?

    事件(event)是类型中的一种成员,定义了事件成员的类型允许类型(或者类型的实例)在某些特定事情发生的时候通知其他对象。如Button类型的Click事件,在按钮被点击的时候,程序中的其他对象可以得到一个通知,并执行相应的动作。事件就是支持这种交互的类型成员

    CLR中的事件模型是建立在委托这一机制之上的,这种关联存在其必然性。

    我们知道,委托是对方法的抽象,它将方法的调用与实现相分离。方法的调用者(即委托的执行者)并不知道方法的内部是如何实现的,而方法的实现者也不知道该方法会在何时被调用。

    事件也是如此。事件被触发后会执行什么样的操作,是由触发者决定的,如点击一个按钮之后是插入一条记录还是用户登录。事件的拥有者只知道什么情况下会触发事件,但并不知道事件的具体实现。因此用委托来实现事件的机制就是自然而然的事情了。

    事件与委托的关系到底是什么样呢?委托是与类、接口同一级别的概念,而事件属于类型的成员,与方法、属性、字段等是同一级别的概念。一个与事件相关联的委托的定义如下:

    public delegate void FooEventHandler(object sender, FooEventArgs e);

    而相应事件成员的定义为:

    public event FooEventHandler Foo;

    可见,事件用event关键字定义,其类型为一个委托类型,即事件是通过委托来实现的。

    一个完整的事件定义和使用的例子如下:

    public delegate void FooEventHandler(object sender, FooEventArgs e);
    public class FooEventArgs : EventArgs { }
    public class Bar
    {
        public event FooEventHandler Foo;
        protected virtual void OnFoo(FooEventArgs e)
        {
            FooEventHandler handler = Foo;
            if (handler != null)
                handler(this, e);
        }
        public void SomeMethod()
        {
            // ...
            OnFoo(new FooEventArgs());
            // ...
        }
    }
    public class Client
    {
        public Client()
        {
            Bar b = new Bar();
            b.Foo += new FooEventHandler(b_Foo);
        }
        void b_Foo(object sender, FooEventArgs e)
        {
            throw new NotImplementedException();
        }
    }

    我们注意到在SomeMethod方法中并没有直接调用委托,而是调用了一个辅助方法OnFoo。在该方法中,先将Foo事件的引用传递给新定义的委托,然后再进行空判断,在委托不为null的情况下才进行调用。这样做是为了保证线程和类型的安全,我们在下面将会介绍。

    还有一个需要注意的地方是,客户端为事件注册方法时,使用的是+=操作符。在本文开头已经介绍,+=对应Delegate.Combine方法,回顾(2)中阐述的委托链的构造,我们可以得出如下结论:在为事件注册方法时,实际上是在构造一个委托链

    事件的设计规范

    《Framework Design Guidelines 2nd Edition》一书应该成为我们设计.NET程序的规范手册。书中对于事件的定义采取了如下的规定:

    事件的命名

    由于通常事件以为着某种行为,因此事件的名称应该为一个动词,并用动词的时态来指明事件发生的时间。《Framework Design Guidelines 2nd Edition》对事件命名的建议如下:

    1. 用动词或动词短语来为事件命名。如Clicked、Painting、DroppedDown等等。

    2. 用现在时和将来时来表示“之前”和“之后”的概念,不要用Before和Arfter前缀。例如在窗体关闭之前触发的事件可以命名为Closing,而窗体关闭之后触发的事件则应该命名为Closed。

    3. 为事件处理程序(委托)的名称添加EventHandler后缀。如

    4. 使用sender和e来命名时间的两个参数。如上例。

    5. 为事件的数据参数类型的名称添加EventArgs后缀。如上例。

    事件的设计

    1. 通常情况下,事件所对应的委托的返回值为void,并且包含两个参数:第一个参数为触发事件的对象,通常为事件的拥有者(即上例中的Bar对象)。第二个参数为事件相关的数据,由事件的拥有者传递给事件的调用者。

    2. 在.NET 2.0及以后的版本中自定义事件时,使用System.EventHandler委托,而不要自定义新的委托类型。因此上例中如果在.NET 2.0下应该定义为:

    public event EventHandler<FooEventArgs> Foo;

    在.NET 2.0以前,由于不支持泛型,我们仍然需要像上面例子中那样定义。

    3. 为事件自定义一个EventArgs的子类,作为传递数据的参数。如果不需要传递任何参数,可以直接使用EventArgs类。

    4. 为每个事件编写一个受保护的虚方法作为触发方法,如上例中的OnFoo方法。这仅适用于unsealed类的非静态事件,并不适用于struct、sealed class和静态事件。这样做的原因是,通过override为子类提供一种处理事件的方式。按照惯例,该虚方法以On开头,以事件名称结尾,如OnFoo方法。

    为了确保委托在调用时不抛出NullReferenceException,在OnXxx方法中通常都会对委托进行判空操作,如

    if (Xxx != null) Xxx(this, e);

    然而仅仅这样是不够的,因为事件处理程序的添加和移除并不是线程安全的,因此在多线程环境下,Xxx委托在判空之后很可能被Remove,导致Xxx在调用时可能为null。由于Remove方法将会构造一个新的委托实例,而不会改变原委托的引用,因此需要先将委托的引用传递给一个新的委托,再对这个新委托进行判空和调用等操作,这样即使原委托被Remove,也不会NullReferenceException。

    FooEventHandler handler = Foo;
    if (handler != null) handler(this, e);

    5. 触发事件的方法有且仅有一个参数,XxxEventArgs参数。

    6. 在触发非静态事件时,sender参数不要为null。对于静态事件,sender参数要为null。

    7. 触发事件时,如果不需要传递任何数据,数据参数可以为EventArgs.Empty,不要为null。

    事件的应用举例

    在前面随笔的评论中,有同学提出希望列举委托在窗体间传值的例子。好吧,我们就举一个简单的WinForm窗体传值的例子。

    我们首先新建一个Windows From应用程序,并新建两个窗体MainForm和SubForm,在MainForm中建立两个Button,在SubForm中添加一个RichTextBox。如下图所示:

    image image

    当点击“开始”的时候,会弹出SubForm,点击“传值”的时候,会将当前时间显示在SubForm的RichTextBox中。

    需求大体就是这样了,我们该如何设计呢?

    点击“传值”按钮后,会引起SubForm的变化。SubForm只负责显示,它并不知道引起变化的原因。MainForm负责引起变化,并将变化传递给SubForm,但它并不关心SubForm如何进行处理。这与我们之前对事件的描述十分相似:

    事件被触发后会执行什么样的操作,是由触发者决定的,如点击一个按钮之后是插入一条记录还是用户登录。事件的拥有者只知道什么情况下会触发事件,但并不知道事件的具体实现。

    因此,在这个示例中,我们可以通过事件来实现传值。我们首先创建数据参数类SendEventArgs,它包含一个Message属性,用来保存数据。

    public class SendEventArgs : EventArgs
    {
        public string Message { get; private set; }
        public SendEventArgs(string message)
        {
            this.Message = message;
        }
    }

    然后在MainForm中添加一个事件:Send。

    public event EventHandler<SendEventArgs> Send;

    然后我们为该事件编写触发方法OnSend:

    protected virtual void OnSend(SendEventArgs e)
    {
        EventHandler<SendEventArgs> handler = Send;
        if (handler != null)
            handler(this, e);
    }

    MainForm中两个按钮的事件处理程序如下:

    private void btnBegin_Click(object sender, EventArgs e)
    {
        SubForm subForm = new SubForm(this);
        subForm.Show();
    }
    private void btnSend_Click(object sender, EventArgs e)
    {
        SendEventArgs sendEventArgs = new SendEventArgs(DateTime.Now.ToString());
        OnSend(sendEventArgs);
    }

    btnBegin按钮用来打开一个SubForm,并将当前MainForm实例作为参数传入。btnSend按钮用来构造Send事件的数据参数,并调用Send事件的触发方法。

    在SubForm中,有一个MainForm类型的私有字段,用于保存构造函数里传入的参数。

    private MainForm parent;

    构造函数中除了给parent字段赋值外,还要注册parent的Send事件的处理程序:

    public SubForm(MainForm main)
    {
        InitializeComponent();
        this.parent = main;
        parent.Send += new EventHandler<SendEventArgs>(parent_Send);
    }

    parent_Send处理程序负责向RichTextBox中添加信息:

    private void parent_Send(object sender, SendEventArgs e)
    {
        this.rtbTime.AppendText(e.Message);
        this.rtbTime.AppendText(Environment.NewLine);
    }

    最后我们在SubForm的Closing事件里移除parent_Send,这样就可以打开多个SubForm了。

    private void SubForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        parent.Send -= new EventHandler<SendEventArgs>(parent_Send);
    }

    整个Demo的显示如下:

    image

    总结

    本文重点讲解了.NET中的事件,并对事件的设计进行了规范,最终通过一个示例加深了我们对事件的理解。

    您是否从以上示例中感觉到了观察者模式的影子呢?本系列接下来的一篇随笔中,我们将会讨论委托与设计模式的微妙联系。

  • 相关阅读:
    2050大会,让世界离年青人更近,让年青人离世界更近
    2050大会,让世界离年青人更近,让年青人离世界更近
    Mybatis-Configuration-详解
    Mybatis-Configuration-详解
    .NET内置的Ajax工作原理
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
    xgqfrms™, xgqfrms® : xgqfrms's offical website of GitHub!
  • 原文地址:https://www.cnblogs.com/waw/p/2157137.html
Copyright © 2011-2022 走看看