C#基础篇——事件


C#基础篇——事件

前言

在本章中,主要是借机这个C#基础篇的系列整理过去的学习笔记、归纳总结并更加理解透彻。

在上一篇文章,我们已经对委托有了进一步了解,委托相当于用方法作为另一方法参数,同时,也可以实现在两个不能直接调用的方法中做桥梁。

下面我们来回顾一下委托的例子。

    public delegate void ExecutingDelegate(string name);

    public class ExecutingManager
    {
        public void ExecuteProgram(string name, ExecutingDelegate ToExecuting)
        {
            ToExecuting(name);
        }
    }
    private static void StartExecute(string name)
    {
        Console.WriteLine("开始执行:" + name);
    }

    private static void EndExecute(string name)
    {
        Console.WriteLine("结束执行:" + name);
    }

    static void Main(string[] args)
    {
        ExecutingManager exec = new ExecutingManager();
        exec.ExecuteProgram("开始。。。", StartExecute);
        exec.ExecuteProgram("结束。。。", EndExecute);
        Console.ReadKey();
    }

根据上述的示例,再利用上节学到的知识,将多个方法绑定到同一个委托变量实现多播,该如何做呢?

再次修改代码:

    static void Main(string[] args)
    {
        ExecutingManager exec = new ExecutingManager();
        ExecutingDelegate executingDelegate;
        executingDelegate = StartExecute;
        executingDelegate += EndExecute;
        exec.ExecuteProgram("yuan", executingDelegate);

        Console.ReadKey();
    }

但是,此刻我们发现是不是可以将实例化声明委托的变量封装到ExecutingManager类中,这样是不是更加方便调用呢?

    public class ExecutingManager
    {
        /// <summary>
        /// 在 ExecutingManager 类的内部声明 executingDelegate 变量
        /// </summary>
        public ExecutingDelegate executingDelegate;
        public void ExecuteProgram(string name, ExecutingDelegate ToExecuting)
        {
            ToExecuting(name);
        }
    }
    static void Main(string[] args)
    {
        ExecutingManager exec = new ExecutingManager();
        exec.executingDelegate = StartExecute;
        exec.executingDelegate += EndExecute;
        exec.ExecuteProgram("yuan", exec.executingDelegate);
        Console.ReadKey();
    }

写到这里了,这样做没有任何问题,但我们发现这条语句很奇怪。在调用exec.ExecuteProgram方法的时候,再次传递了exec的executingDelegate字段, 既然如此,我们何不修改 ExecutingManager类成这样:

    public class ExecutingManager
    {
        /// <summary>
        /// 在 GreetingManager 类的内部声明 delegate1 变量
        /// </summary>
        public ExecutingDelegate executingDelegate;
        public void ExecuteProgram(string name)
        {
            if (executingDelegate != null) // 如果有方法注册委托变量
            {
                executingDelegate(name); // 通过委托调用方法
            }
        }
    }
    static void Main(string[] args)
    {
        ExecutingManager exec = new ExecutingManager();
        exec.executingDelegate = StartExecute;
        exec.executingDelegate += EndExecute;
        exec.ExecuteProgram("yuan");
        Console.ReadKey();
    }

这样再看,发现调用一下就更加简洁了。

正文

在日常生活中,我们可能都会遇到这样的各种各样的事情,而对于这些事情我们都会采取相应的措施。比如,当你要给一个女神过生日的时候,你就可以给她送礼物。而这种情况,在C#开发中,就相当于过生日被当作事件来对待,而送礼物就是事件做出的响应。

当女神过生日的时候,女神就会发布生日事件,而你就会接受到这个事件的通知,并做出响应的处理(送礼物等骚操作)。其中,触发这个事件的对象我们可称之为事件发布者,而捕获这个事件并做出相应处理的称之为事件订阅者,我们可以看出,女神就是充当了发布者,而你自己则充当了订阅者。

这里由生日事件引申出两类角色,即事件发布者事件订阅者

开始

1.发布者/订阅者模式

在开发中,我们是否遇到这样的情景,当一个特定的程序事件发生时,其他程序部分可以得到该事件注册发生通知。

发布者定义一系列事件,并提供一个注册方法;订阅者向发布者注册,并提供一个可被回调的方法,也就是事件处理程序;当事件被触发的时候,订阅者得到通知,而订阅者所提交的所有方法会被执行。

  • 发布者:发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
  • 订阅者:注册并在事件发生时得到通知的类或结构。
  • 事件处理程序:由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结果中,也可以定义在不同的类或结构中。
  • 触发事件:调用事件的术语。当事件触发时,所有注册到它的方法都会被一次调用。

2.基本使用

        /// <summary>
        /// 先自定义一个委托
        /// </summary>
        /// <param name="oldPrice"></param>
        /// <param name="newPrice"></param>
        public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
        /// <summary>
        /// 这个一个发布者
        /// </summary>
        public class IPhone
        {
            decimal price;
            /// <summary>
            /// 定义一个事件
            /// event 用来定义事件
            /// PriceChangedHandler委托类型,事件需要通过委托来调用订阅者需要的方法
            /// </summary>
            public event PriceChangedHandler PriceChanged;

            public decimal Price
            {
                get { return price; }
                set
                {
                    if (price == value)
                        return;
                    decimal oldPrice = price;
                    price = value;             // 如果调用列表不为空,则触发。            
                    if (PriceChanged != null)   //用来判断事件是否被订阅者注册过
                        PriceChanged(oldPrice, price);   //调用事件
                }
            }
        }
        /// <summary>
        /// 这个一个订阅者
        /// </summary>
        /// <param name="oldPrice"></param>
        /// <param name="price"></param>
        static void iPhone_PriceChanged(decimal oldPrice, decimal price)
        {
            Console.WriteLine("618促销活动,全场手机 只卖 " + price + " 元, 原价 " + oldPrice + " 元,快来抢!");
        }
        static void Main()
        {
            ///实例化一个发布者类
            IPhone phone = new IPhone()
            {
                Price = 5288
            };         // 订阅事件   
            phone.PriceChanged += iPhone_PriceChanged;          //完成事件的注册 调整价格(事件发生)    
            phone.Price = 3999;                                //激发事件,并调用事件
            Console.ReadKey();
        }

输出:

618促销活动,全场手机 只卖 3999 元, 原价 5288 元,快来抢!

3.解析

  1. 委托类型声明:事件与事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
  2. 事件声明:使用关键字evet来声明一个事件,当声明的事件为一个public时,称为发布了一个事件。
  3. 事件注册:订阅者通过+=操作符来注册事件,并提供一个事件处理程序。
  4. 事件处理程序: 订阅者向事件注册的方法,它可以是显示命名的方法、匿名方法或者Lambda表达式
  5. 触发事件:发布者用来调用事件的代码

4.语法

事件的声明语法:

//声明一个事件
public [static] event EventHandler EventName;
//声明多个同类型的事件
public [static] event EventHandler EventName1, EventName2, EventName3;

事件必须声明在类或结构中,因为事件它不是一个类型,它是一个类或者结构中的一员。

在事件被触发之前,可以通过和null做比较,判断是否包含事件注册处理程序。因为事件成员被初始化默认是null。

委托类型EventHandler是声明专门用来事件的委托。事件提供了对委托的结构化访问;也即是无法直接访问事件中的委托。

5.用法

img

查看源码:

事件的标准模式就是System命名空间下声明的EventHandler委托类型。

EventArgs是System下的一个类,如下:

using System.Runtime.InteropServices;

namespace System
{
    [Serializable]
    [ComVisible(true)]
    [__DynamicallyInvokable]
    public class EventArgs
    {
        [__DynamicallyInvokable]
        public static readonly EventArgs Empty = new EventArgs();

        [__DynamicallyInvokable]
        public EventArgs()
        {
        }
    }
}

根据EventArgs源码看出,EventArgs本身无法保存和传递数据的。

如果想保存和传递数据,可以实现一个EventArgs的派生类,然后定义相关的字段来保存和传递参数。

    public class IPhone
    {
        decimal price;
        /// <summary>
        /// 使用EventHandler定义一个事件
        /// </summary>
        public event EventHandler PriceChanged;
        protected virtual void OnPriceChanged()
        {
            if (PriceChanged != null)
                PriceChanged(this, null);
        }
        public decimal Price
        {
            get { return price; }
            set
            {
                if (price == value) return;
                decimal oldPrice = price; 
                price = value;             // 如果调用列表不为空,则触发。      
                if (PriceChanged != null)  // //用来判断事件是否被订阅者注册过
                    OnPriceChanged();
            }
        }
    }
    /// <summary>
    /// 这个一个订阅者
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    static void iphone_PriceChanged(object sender, EventArgs e)
    {
        Console.WriteLine("年终大促销,快来抢!");
    }
    static void Main()
    {
        IPhone phone = new IPhone()
        {
            Price = 5288M
        };         // 订阅事件  
        phone.PriceChanged += iphone_PriceChanged;
        // 调整价格(事件发生)   
        phone.Price = 3999;
        Console.ReadKey();
    }

通过扩展EventHanlder来传递数据

img

System下另有泛型EventHandler类。由此,这里我们可以将派生于EventArgs的类作为类型参数传递过来,这样,既可以获得派生类保存的数据。

    ///扩展类
    public class PriceChangedEventArgs : System.EventArgs
    {
        public readonly decimal OldPrice;
        public readonly decimal NewPrice;
        public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
        {
            OldPrice = oldPrice;
            NewPrice = newPrice;
        }
    }
    public class IPhone
    {
        decimal price;
        public event EventHandler<PriceChangedEventArgs> PriceChanged;
        protected virtual void OnPriceChanged(PriceChangedEventArgs e)
        {
            if (PriceChanged != null)
                PriceChanged(this, e);
        }
        public decimal Price
        {
            get { return price; }
            set
            {
                if (price == value) return;
                decimal oldPrice = price;
                price = value;             // 如果调用列表不为空,则触发。      
                if (PriceChanged != null)
                    OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
            }
        }
    }
    static void iphone_PriceChanged(object sender, PriceChangedEventArgs e)
    {
        Console.WriteLine("618促销活动,全场手机 只卖 " + e.NewPrice + " 元, 原价 " + e.OldPrice + " 元,快来抢!");
    }
    static void Main()
    {
        IPhone phone = new IPhone()
        {
            Price = 5288M
        };         // 订阅事件  
        phone.PriceChanged += iphone_PriceChanged;
        // 调整价格(事件发生)   
        phone.Price = 3999;
        Console.ReadKey();
    }

输出

618促销活动,全场手机 只卖 3999 元, 原价 5288 元,快来抢!

6.移除事件

可以利用 -= 运算符处理程序从事件中移除,当程序处理完后,可以将事件从中把它移除掉。

        class Publiser
        {
            public event EventHandler SimpleEvent;

            public void RaiseTheEvent()
            {
                SimpleEvent(this, null);
            }
        }

        class Subscriber
        {
            public void MethodA(object o, EventArgs e) { Console.WriteLine("A"); }
            public void MethodB(object o, EventArgs e) { Console.WriteLine("B"); }
        }


        static void Main(string[] args)
        {
            Publiser p = new Publiser();
            Subscriber s = new Subscriber();

            p.SimpleEvent += s.MethodA;
            p.SimpleEvent += s.MethodB;
            p.RaiseTheEvent();

            Console.WriteLine("\n移除B事件处理程序");
            p.SimpleEvent -= s.MethodB;
            p.RaiseTheEvent();

            Console.ReadKey();
        }

输出:

img

7.事件访问器

运算符+= 、-=事件允许的唯一运算符。这些运算符是有预定义的行为。然而,我们可以修改这些运算符的行为,让事件执行任何我们希望定义的代码。

可以通过为事件定义事件访问器,来控制事件运算符+=、-=运算符的行为

  1. 两个访问器: add 和 remove
  2. 声明事件的访问器看上去和声明一个熟悉差不多。

下面示例演示了具有访问器的声明.两个访问器都有叫做value的隐式值参数,它接受实例或静态方法的引用

public event EventHandler Elapsed
{
    add
    {
        //... 执行+=运算符的代码
    }

     remove
     {
        //... 执行-=运算符的代码
     }

}

声明了事件访问器后,事件不包含任何内嵌委托对象.我们必须实现自己的机制来存储和移除事件的方法。

事件访问器表现为void方法,也就是不能使用会返回值的return语句。

示例:

        //声明一个delegate
        delegate void EventHandler();

        class MyClass
        {
            //声明一个成员变量来保存事件句柄(事件被激发时被调用的delegate)
            private EventHandler m_Handler = null;

            //激发事件
            public void FireAEvent()
            {
                if (m_Handler != null)
                {
                    m_Handler();
                }
            }

            //声明事件
            public event EventHandler AEvent
            {
                //添加访问器
                add
                {
                    //注意,访问器中实际包含了一个名为value的隐含参数
                    //该参数的值即为客户程序调用+=时传递过来的delegate
                    Console.WriteLine("AEvent add被调用,value的HashCode为:" + value.GetHashCode());
                    if (value != null)
                    {
                        //设置m_Handler域保存新的handler
                        m_Handler = value;
                    }
                }

                //删除访问器
                remove
                {
                    Console.WriteLine("AEvent remove被调用,value的HashCode为:" + value.GetHashCode());
                    if (value == m_Handler)
                    {
                        //设置m_Handler为null,该事件将不再被激发
                        m_Handler = null;
                    }
                }

            }

        }

        static void Main(string[] args)
        {
            MyClass obj = new MyClass();
            //创建委托
            EventHandler MyHandler = new EventHandler(MyEventHandler);
            MyHandler += MyEventHandle2;
            //将委托注册到事件
            obj.AEvent += MyHandler;
            //激发事件
            obj.FireAEvent();
            //将委托从事件中撤销
            obj.AEvent -= MyHandler;
            //再次激发事件
            obj.FireAEvent();


            Console.ReadKey();
        }
        //事件处理程序
        static void MyEventHandler()
        {
            Console.WriteLine("This is a Event!");
        }

        //事件处理程序
        static void MyEventHandle2()
        {
            Console.WriteLine("This is a Event2!");
        }

输出:

img

总结

  1. 这节对事件的基本使用,以及事件的标准语法、事件访问器等多个地方进行说明,大致可以了解和掌握事件的基本使用。
  2. 结合上一篇的委托和这一节的事件,委托和事件我们大概掌握了基本用法。并加以实践,结合实际开发,应用其中。
  3. 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。

参考 文档 《C#图解教程》

注:搜索关注公众号【DotNet技术谷】–回复【C#图解】,可获取 C#图解教程文件


文章作者: 艾三元
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 艾三元 !
 上一篇
基于.NetCore3.1系列 —— 认证授权方案之JwtBearer认证 基于.NetCore3.1系列 —— 认证授权方案之JwtBearer认证
基于.NetCore3.1系列 —— 认证授权方案之JwtBearer认证前言回顾:认证方案之初步认识JWT 在现代Web应用程序中,即分为前端与后端两大部分。当前前后端的趋势日益剧增,前端设备(手机、平板、电脑、及其他设备)层出不穷。因此
2020-06-19
下一篇 
C#基础篇——委托 C#基础篇——委托
C#基础篇——委托前言在本章中,主要是借机这个C#基础篇的系列整理过去的学习笔记、归纳总结并更加理解透彻。 在.Net开发中,我们经常会遇到并使用过委托,如果能灵活的掌握并加以使用会使你在编程中游刃有余,然后对于很多接触C#时间不长的开发者
2020-05-31
  目录