Keep on going never give up.

Let's Go

C# 学习笔记(51)事件

C#Lonely2019-05-28 03:03:02110次0条

C#事件(Event)

事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些出现,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。事件是用于进程间通信。

事件涉及两类角色——事件发布者事件订阅者当某个事件发生后,事件发布者会发布消息;事件订阅者会收到事件已发生的通知,并做出相应的处理。其中,触发事件的对象称为事件发布者,捕获事件并对其做出处理的对象称为事件订阅者。

事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。包含事件的类用于发布事件。这被称为 发布器(publisher)类。其他接受该事件的类被称为订阅器(subscriber)类。事件使用 发布-订阅(publisher-subscriber)模型。

发布器(publisher)是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。

订阅器(subscriber)是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。

事件定义的语法:

访问修饰符 event 委托类型 事件名;

这里,访问修饰符一般定义为public,因为事件的订阅者需要对事件进行订阅与取消操作,定义为公共类型可使事件对其他类可见。事件定义中还包含委托类型,它既可以是自定义的委托类型,也可以是.NET类库中预定义的委托类型EventHandler。

订阅和取消事件

事件订阅者需要订阅 事件发布者 发布的事件,以便在事件被触发时接收消息并做出处理。使用“+=”运算符来订阅事件,使用“-=”运算符来取消事件订阅

代码示例(模拟音乐播放完毕后触发的事件):

using System;
using System.Threading;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Music music = new Music("恭喜发财-刘德华"); //创建对象
            music.playOver += Music_playOver; //绑定事件
            music.PlayMusic(); //开始播放音乐
            Console.ReadKey();
        }

        //事件处理函数,该函数需要符合自定义委托 DelPlayOver 的定义
        private static void Music_playOver()
        {
            Console.WriteLine("歌曲播放完毕了!");
        }
    }

    public class Music
    {
        //例子:模拟音乐播放完毕后触发的事件

        public delegate void DelPlayOver(); //自定义委托

        //使用自定义委托类型定义事件,事件名为playOver
        public event DelPlayOver playOver; 

        public string MusicName { get; set; } //字段

        public Music(string musicName) //构造函数
        {
            this.MusicName = musicName;
        }

        public void PlayMusic()
        {
            Console.WriteLine("正在播放:{0}", this.MusicName);

            // Thread.Sleep 使当前线程停止一段时间运行
            Thread.Sleep(5000); // 模拟播放歌曲,5秒后播放完毕

            //判断是否绑定了事件处理方法
            if (playOver != null)
            {
                //歌曲播放完毕后触发事件
                playOver();
            }
        }
    }

}

运行结果:

image.png

上面的代码演示了使用自定义委托类型来定义事件的例子,值得注意的是,事件处理函数的定义需要与委托的定义保持一致,即其参数个数、参数类型和返回类型等需要与委托相同。除了可以使用自定义委托类型来定义事件外,还可以使用.NET类型中预定义的委托类型EventHandler来定义事件,后者也是实际开发中普遍采用的一种方式。

代码示例:

using System;
using System.Threading;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Music music = new Music("恭喜发财-刘德华"); //创建对象
            music.playOver += Music_playOver; //绑定事件
            music.PlayMusic(); //开始播放音乐
            Console.ReadKey();
        }

        /// <summary>
        /// 事件处理函数,该函数需要符合委托 EventHandler 的定义
        /// </summary>
        /// <param name="sender">事件源(触发当前事件的对象)</param>
        /// <param name="e">不包含事件数据的对象</param>
        private static void Music_playOver(object sender, EventArgs e)
        {
            Console.WriteLine("歌曲播放完毕了!");
        }
    }

    public class Music
    {
        //例子:模拟音乐播放完毕后触发的事件

        //使用.NET类库中预定义的委托类型来定义事件
        public event EventHandler playOver; 
        
        public string MusicName { get; set; } //字段

        public Music(string musicName) //构造函数
        {
            this.MusicName = musicName;
        }

        public void PlayMusic()
        {
            Console.WriteLine("正在播放:{0}", this.MusicName);

            // Thread.Sleep 使当前线程停止一段时间运行
            Thread.Sleep(5000); // 模拟播放歌曲,5秒后播放完毕

            //判断是否绑定了事件处理方法
            if (playOver != null)
            {
                //歌曲播放完毕后触发事件
                playOver(this, new EventArgs());
            }
        }
    }

}

以上代码运行结果与上一致。EventHandler是.NET类库中预定义的委托类型,用于处理不包含事件数据的事件。如果想在事件中包含事件数据,可以通过派生EventArgs来实现。下面,先让我们看看EventHandler委托的定义,你可以通过在代码中把光标移到EventHandler处,再按F12快捷键的方式来查看,它的具体定义如下:

image.png

从EventHandler委托的定义可以看出:

该委托的返回类型为void,因此实例化委托的方法也需要满足这点;

第一个参数sender负责保存对触发事件对象的引用,其类型为object;

第二个参数e负责保存事件数据,EventArgs类也是.NET类库中定义的类,它不保存任何数据。


扩展EventArgs类

因为EventHandler只用于处理不包含事件数据的事件,如果我们想要在由这种方式定义的事件中包含事件数据,则可以通过派生EventArgs类来实现。下面通过扩展EventArgs类来使事件参数e带有事件数据,具体实现代码如下:

using System;
using System.Threading;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Music music = new Music("恭喜发财-刘德华"); //创建对象
            music.playOver += Music_playOver; //绑定事件
            music.PlayMusic(); //开始播放音乐
            Console.ReadKey();
        }

        //事件处理函数
        private static void Music_playOver(object sender, MyEventArgs e)
        {
            Console.WriteLine(e.Message);
        }
    }

    //自定义事件类,并使其带有事件数据
    public class MyEventArgs : EventArgs
    {
        public string Message { get; set; }
        public MyEventArgs(string msg)
        {
            this.Message = msg;
        }
    }

    public class Music
    {
        //例子:模拟音乐播放完毕后触发的事件

        //自定义委托类型,委托包含两个参数
        public delegate void DelplayOver(object sender, MyEventArgs e);

        //定义事件
        public event DelplayOver playOver; 
        
        public string MusicName { get; set; } //字段

        public Music(string musicName) //构造函数
        {
            this.MusicName = musicName;
        }

        public void PlayMusic()
        {
            Console.WriteLine("正在播放:{0}", this.MusicName);

            // Thread.Sleep 使当前线程停止一段时间运行
            Thread.Sleep(5000); // 模拟播放歌曲,5秒后播放完毕

            string msg = "歌曲:" + this.MusicName + ",播放完毕了!";

            //判断是否绑定了事件处理方法
            if (playOver != null)
            {
                //歌曲播放完毕后触发事件
                playOver(this, new MyEventArgs(msg));
            }
        }
    }   
}

运行结果:

image.png


事件的本质

从事件的使用过程可以看出,事件的定义中包含了委托类型。那么,事件与委托之间到底有着什么样的关系呢?下面通过探究事件所产生的IL代码来说明它们之间的关系。我们在一个类中定义了如下事件:

using System;

namespace ConsoleApp
{
    class Program
    {
        public event EventHandler MyEvent;

        static void Main(string[] args)
        {
           
        }
    }
}

将以上代码生成所示IL代码,如下图所示:

image.png

由上IL代码可以看出,C#事件被编译成包含两个公共方法的代码段,一个带有add_前缀,另一个带有remove_前缀,前缀后面是C#事件的名称。查看add_MyEvent()的IL代码,你会发现add_MyEvent()方法是通过调用Delegate.Combine()方法来实现的,Delegate.Combine()方法将多个委托组合为了一个多路广播委托,下面给出了add_MyEvent()的IL代码与反编译后的C#代码,如下所示:

image.png

image.png

相应地,remove_MyEvent方法也将间接调用Delegate.Remove()方法,Delegate.Remove()方法用于将一个委托从多路广播委托中移除。

image.png

image.png

其中还有一个MyEvent字段的定义,查看其IL代码内容如下:

image.png

从以上分析可以得出,C#中的事件的本质是一个特殊的多路广播委托,事件默认含有一个私有的委托类型变量,该变量用于保存对事件处理方法的引用,且该委托类型的变量为私有,只能从定义该事件的类中进行访问(只能在类内部调用(触发)事件)。C#事件提供了对私有委托字段进行访问的有效方法。

由事件的IL代码段,我们可以联想到类中属性的定义。与事件不同,属性中定义了set访问器和get访问器,两个访问器的本质是以“get_”和“set_”为前缀的两个方法。属性用于对类中的私有字段进行访问,而C#事件也可以看作是“委托字段的属性”,因此可以通过事件来对私有的委托字段进行访问,这也是C#事件特性存在的原因。C#事件机制符合面向对象的封装特性,它使得应用程序代码更加安全。



暗锚,解决锚点偏移

文章评论

    嘿,来试试登录吧!