C# 事件Event(个人整理)

内容来源:MSN:https://docs.microsoft.com/zh-cn/dotnet/csharp/event-pattern

               操作符详解(上)   https://www.youtube.com/watch?v=1DhDOJz_S98

               操作符详解(中)  https://www.youtube.com/watch?v=ntTmyMkgj0U&list=PLZX6sKChTg8GQxnABqxYGX2zLs4Hfa4Ca&index=23

 

目录

事件介绍

事件的命名约定

事件完整的声明如下(像属性)/事件简约声明(filed-like)

使用系统定义好的EventHandler、EventHandler<T>的事件委托

事件模型的6个组成部分

事件支持的设计目标

订阅/取消事件

引发事件

事件模式

事件和委托的区别

事件导致的内存泄漏

有了 委托字段/属性,为什么还需要事件呢?

如何实现接口事件

 事件介绍

和委托类似,事件是后期绑定机制。 实际上,事件是建立在对委托的语言支持之上的。
事件是对象用于(向系统中的所有相关组件)广播已发生事情的一种方式。 任何其他组件都可以订阅事件,并在
事件引发时得到通知。

通过订阅事件,还可在两个对象(事件源和事件接收器)之间创建耦合。 需要确保当不再对事件感兴趣时,事件接
收器将从事件源取消订阅。
事件具有以下属性:
1、发行者确定何时引发事件;订户确定对事件作出何种响应。
2、一个事件可以有多个订户。 订户可以处理来自多个发行者的多个事件。
3、没有订户的事件永远也不会引发。
4、事件通常用于表示用户操作,例如单击按钮或图形用户界面中的菜单选项。
5、当事件具有多个订户时,引发该事件时会同步调用事件处理程序。 若要异步调用事件,请参阅 “使用异步
方式调用同步方法”。
6、在 .NET 类库中,事件基于 EventHandler 委托和 EventArgs 基类。

事件的本质是委托字段的一个包装器

这个包装是对委托起到限制作用,防止对象内部的委托实例被外部乱用,

封装的一个重要的功能就是隐藏

事件对外界隐藏了委托实例的大部分功能

事件的命名约定:

事件的委托的命名约定

1、用于声明事件的委托,一般命名为事件名+EventHandler(除非是一个非常通用的事件约束)。

2、事件名+EventHandler的委托参数一般有两个(由win32API 演化而来,历史悠久)

第一个参数:sender是Object类型,表示事件源\事件发送者 。

第二个参数:e是EventArgs类型  EventArgs类表示包含事件数据的类的基类,并提供用于不包含事件数据的事件的值。因为EventArgs类里面只有一个静态的EvetnArgs 类型的Empty字段和默认构造函数。

Empty 是引用型静态字段,初始化后就Null。所以EventArgs 类型就是传递一个空数据。

一个事件发生后若要传递附加的参数信息,就需要定义事件参数类,需要继承EventArgs,否则就直接使用EventArgs.Empty即可。(EventArgs.Empty实际上就是new EventArgs())

 

所传递的消息,举个例子:如果手机铃声响力了(通知),手机拥有者可以根据

手机铃声判断,是短线、电话、闹钟,然后采集不同的处理方式。这响铃(名词)就是事件参数,这不同响铃就是 事件参数所包含的信息。

事件参数的命名约定

事件参数标准 是派生EventAgrs,一般类命名为事件名+EventAgrs,参数名e ,.net core 中这条约定不在强制要求。

 

事件的命名约定

带动时态的谓词或谓词短语来命名事件。

例如,窗口关闭之前引发的事件称为 Closing,窗口关闭之后引发的事件称为 Closed

事件触发器的命名约定

On+事件名,即“因何引发”、“事出有因”

访问级别为protected、不能为public、不然又成了借刀杀人的工具

在调用方法里面一定要判断一下委托是否为空或者  public event EventHandlerEvent = delegate { } 确保事件总被初始化的这样就可以不必每次在使用它之前都要检查它是否不为NULL

类的三大功能 存储数据(字段)、处理事情(方法)、通知别人(事件)

 

事件声明

事件完整的声明如下(像属性):以下订单为例子 自定义一个事件

public delegate void OrderEvenHandler(Custemer custemer, OrderEventAgrs e);
 private OrderEvenHandler orderEvenHandler= delegate { };//确保事件总被初始化的这样就可以不必每次在使用它之前都要检查它是否不为NULL
 public event OrderEvenHandler Order
        {
            add
            {

                this.orderEvenHandler += value;


            }
            remove
            {

                this.orderEvenHandler -= value;

            }

        }

 

事件简约声明(filed-like):

 public event OrderEvenHandler Orderw;//像字段声明

 系统回自动生成add remove

使用系统定义好的EventHandler、EventHandler<TEventArgs>的事件委托

 EventHandler委托是系统自带的通用性事件委托。

public delegate void EventHandler(object? sender, EventArgs e);

表示将用于处理不具有事件数据的事件的方法。

参数:sender  是Object类型,表示事件源\事件发送者 。

参数:EventArgs  表示事件信息。

所传递的消息,举个例子:如果手机铃声响力了(通知),手机拥有者可以根据

手机铃声判断,是短线、电话、闹钟,然后采集不同的处理方式。这响铃(名词)就是事件参数,这不同响铃就是 事件参数所包含的信息。 

 .NET Framework 类库中的所有事件均基于 EventHandler 委托,还有泛型版本EventHandler<EventArgs>

事件模型的7个组成部分

1、声明事件的委托、 EventHandler  

2、事件成员\声明事件 NameEvent
3、事件拥有者    sender
4、事件参数 EventAgrs
5、触发器 Raise

6、事件订阅者\事件监听器  subsriber\Listener(watcher)
7、订阅--把事件处理器和事件关联在一起,本质上是一种以委托为基础的约定。

 

事件拥有者

 也叫事件源。

事件响应者

也叫事件的订阅者\事件消息的接收者\事件的响应者\事件的处理者\被事件所通知的对象\事件侦听器 都是同一个意思

一个对象要想被作为事件监听器,需要将该对象中的事件处理程序 注册(或挂载)到另一个能够产生事件的对象(即事件源)

事件处理器

也叫事件处理程序,如果是匿名函数做事件处理器,最好将匿名函数绑定到委托上,在用委托订阅事件,这样可以避免内存泄漏。

触发器

事件只能从类或者派生类触发,不能由外部类触发。所以触发器是protect类型

 

 

事件支持的设计目标

事件的语言设计针对这些目标:
在事件源和事件接收器之间启用非常小的耦合。 这两个组件可能不会由同一个组织编写,甚至可能会通
过完全不同的计划进行更新。
订阅事件并从同一事件取消订阅应该非常简单。
事件源应支持多个事件订阅服务器。 它还应支持不附加任何事件订阅服务器。
你会发现事件的目标与委托的目标非常相似。 因此,事件语言支持基于委托语言支持构建。

通过使用 +=/-= 运算符订阅取消事件:

EventHandler<FileListArgs> onProgress = (sender, eventArgs) =>
Console.WriteLine(eventArgs.FoundFile);
fileLister.Progress += onProgress;

 

引发事件

引发事件时,使用委托调用语法调用事件处理程序:

Progress?.Invoke(this, new FileListArgs(file));

如委托部分中所介绍的那样,?. 运算符可以轻松确保在事件没有订阅服务器时不引发事件。

事件模式 

 经典事件模式

.NET框架为事件定义了一个标准模式,它的目的是保持框架和用户代码之间的一致性。

标准事件的模式核心是System.EventArgs——预定义的没有成员的框架类(不同于静态Empty属性)

EventArgs表示包含事件数据的类的基类,并提供用于不包含事件数据的事件的值。用于为事件传递信息的基类。

经典模式案例:

 

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace Order
{
    class Program
    {

        static void Main(string[] args)
        {
            EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
            {
                Console.WriteLine(eventArgs.FoundFile);
                eventArgs.CancelRequested = true;
            };
            FileSearcher fileLister = new();

            fileLister.DirectoryChanged += (sender, eventArgs) =>
            {
                Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
                Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
            };
           
        }
    }

    public class FileFoundArgs : EventArgs
    {
        public string FoundFile { get; }
        public bool CancelRequested { get; set; }
        public FileFoundArgs(string fileName)
        {
            FoundFile = fileName;
        }
    }
    internal class SearchDirectoryArgs : EventArgs
    {
        internal string CurrentSearchDirectory { get; }
        internal int TotalDirs { get; }
        internal int CompletedDirs { get; }

        internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)
        {
            CurrentSearchDirectory = dir;
            TotalDirs = totalDirs;
            CompletedDirs = completedDirs;
        }
    }
    public class FileSearcher
    {
        public event EventHandler<FileFoundArgs> FileFound;



        public void Search(string directory, string searchPattern, bool searchSubDirs = false)
        {
            if (searchSubDirs)
            {
                var allDirectories = Directory.GetDirectories(directory, "*.*", SearchOption.AllDirectories);
                var completedDirs = 0;
                var totalDirs = allDirectories.Length + 1;
                foreach (var dir in allDirectories)
                {
                    directoryChanged?.Invoke(this,
                        new SearchDirectoryArgs(dir, totalDirs, completedDirs++));
                    // Search 'dir' and its subdirectories for files that match the search pattern:
                    SearchDirectory(dir, searchPattern);
                }
                // Include the Current Directory:
                directoryChanged?.Invoke(this,
                    new SearchDirectoryArgs(directory, totalDirs, completedDirs++));
                SearchDirectory(directory, searchPattern);
            }
            else
            {
                SearchDirectory(directory, searchPattern);
            }
        }

        private void SearchDirectory(string directory, string searchPattern)
        {
            foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
            {
                var args = new FileFoundArgs(file);
                FileFound?.Invoke(this, args);
                if (args.CancelRequested)
                    break;
            }
        }
        public void List(string directory, string searchPattern)
        {
            foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
            {
                var args = new FileFoundArgs(file);
                FileFound?.Invoke(this, args);
                if (args.CancelRequested)
                    break;
            }
        }

        private EventHandler<SearchDirectoryArgs> directoryChanged;
        internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
        {
            add { directoryChanged += value; }
            remove { directoryChanged -= value; }
        }
       
    }
    
}

 

.NET Core 事件模式(不太明白)

 1、事件参数作为返回值,事件参数继续遵守经典模式

2、事件参数仅作 传递消息。那么事件参数不用遵守 事件参数命名规则和派生规则。也就是说事件参数可以是不用继承EventArgs类和事件参数可以是结构类型,事件参数类型命名不用以EventArgs结尾。

NET Core 的模式较为宽松。 在此版本中,EventHandler<TEventArgs> 定义不再要求 TEventArgs 必须是派生自 System.EventArgs 的类。

这就提高了灵活性,并且还具有后向兼容性。 首先讨论灵活性。 类 System.EventArgs 引入了一个方法 MemberwiseClone(),该方法可创建对象的浅表副本。 对于任何派生自 EventArgs 的类,该方法必须使用反射才能实现其功能。 该功能在特定的派生类中更容易创建。 实际上,这意味着派生自 System.EventArgs 的类会限制你的设计,且不会为你提供任何附加好处。 其实,可以更改 FileFoundArgsSearchDirectoryArgs 的定义,使它们不从 EventArgs 派生。 该程序的工作原理相同。

如果还要进行一处更改,还可将 SearchDirectoryArgs 更改为结构:

internal struct SearchDirectoryArgs
{
    internal string CurrentSearchDirectory { get; }
    internal int TotalDirs { get; }
    internal int CompletedDirs { get; }

    internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs) : this()
    {
        CurrentSearchDirectory = dir;
        TotalDirs = totalDirs;
        CompletedDirs = completedDirs;
    }
}

其他更改为:在输入初始化所有字段的构造函数之前调用无参数构造函数。 若没有此添加,C# 规则将报告先访问属性再分配属性。

不应将 FileFoundArgs 从类(引用类型)更改为结构(值类型)。 这是因为处理取消的协议要求通过引用传递事件参数。 如果进行了相同的更改,文件搜索类将永远不会观察到任何事件订阅者所做的任何更改。 结构的新副本将用于每个订阅者,并且该副本将与文件搜索对象所看到的不同。

接下来,让我们考虑这种更改如何具有后向兼容性。 删除约束不会影响任何现有代码。 任何现有的事件参数类型仍然派生自 System.EventArgs。 它们将继续从 System.EventArgs 派生,其中一个主要原因就是后向兼容性。 任何现有的事件订阅者都是遵循经典模式的事件的订阅者。

 

遵循类似的逻辑,现在创建的任何事件参数类型在任何现有代码库中都不会有任何订阅者。 只有从 System.EventArgs 派生的新事件类型才会破坏这些代码库。

 异步事件订阅者

还有最后一个模式需要了解:如何正确编写调用异步代码的事件订阅服务器。 该问题详见 async 和 await 一文。 异步方法可具有一个 void 返回类型,但强烈建议不要使用它。 事件订阅者代码调用异步方法时,只能创建 async void 方法。 事件处理程序签名需要该方法。

你需要协调此对立指南。 不管怎样,必须创建安全的 async void 方法。 需要实现的模式的基础知识如下:

 

worker.StartWorking += async (sender, eventArgs) =>
{
    try
    {
        await DoWorkAsync();
    }
    catch (Exception e)
    {
        //Some form of logging.
        Console.WriteLine($"Async task failure: {e.ToString()}");
        // Consider gracefully, and quickly exiting.
    }
};

 

首先请注意,处理程序已被标记为异步处理程序。 因为它将被分配给一个事件处理程序委托类型,所以它将有一个 void 返回类型。 这意味着必须遵循处理程序中显示的模式,并且不允许在异步处理程序上下文之外引发异常。 因为它不返回任务,所以没有任何可通过进入故障状态报告错误的任务。 因为方法是异步的,所以不能简单地引发异常。 (调用方法已继续执行,因为它是 async。)对于不同的环境,实际的运行时行为将有不同的定义。 它可以终止线程或拥有线程的进程,也可以使进程处于不确定状态。 所有这些潜在的结果都非常不理想。

这就是为什么你应该在自己的 try 块中包装异步任务的 await 语句。 如果它的确导致任务出错,则可以记录该错误。 如果它是应用程序无法从中恢复的错误,则可以迅速优雅地退出此程序。

这些就是 .NET 事件模式的主要更新。 你将在所使用的库中看到许多早期版本的示例。 但是,你也应了解最新的模式是什么。

委托和事件的区别

生命周期的区别:

这条是严格意义是不成立的,这边只是根据经验总结

委托经常作为参数传递,他声明周期经常随函数调用完就结束了。

事件是随对象建立后一直存在,直到对象被回收。

使用功能的区别:

1、 对于事件来讲,只能在本类型内部“触发”,外部只能“注册自己+=、注销自己-=“。 事件通常是公共类成员。

委托委托不管在本类型内部还是外部都可以“调用”。并存储为私有类成员(如果它们全部存储)。

2、委托经常使用匿名函数,而事件要尽量避免使用匿名函数,因为匿名函数很难取消订阅。

 返回值的区别:

事件没有返回值,委托由返回值

封装

委托是对方法的包装 、事件是对委托的封装

作用域
事件是类的成员,只能在对象或类使用 ,事件是对委托的实例封装;委托是和类同一个级别的,可以在类外声明,委托是是对方法的封装。

事件导致的内存泄漏

问题:事件导致内存泄漏

一个对象要想被作为事件监听器,需要将该对象中的事件处理程序 注册(或挂载)到另一个能够产生事件的对象(即事件源)上,这样会导致事件源必须保持一个到事件侦听器对象的引用,以便在事件发生时调用此侦听器的处理方法。

这很合理,但如果这个引用是一个 强引用,则监听器会作为事件源的一个依赖 从而不能作为垃圾回收,即使引用它的对象是事件源。(即,“+=”右边的事件所属的对象被所挂载的对象所使用,导致其无法释放)

解决方法:

问题解决方案:让事件源可以通过弱引用来引用监听器,在事件源存在时也可以回收监听器对象。

这里有一个标准的模式及其在.NET框架上的实现:弱事件模式(http://msdn.microsoft.com/en-us/library/aa970850.aspx)

内容来源:https://blog.csdn.net/u013986317/article/details/86236616

有了 委托字段/属性,为什么还需要事件呢?

因为委托可以被外部调用并且被执行,而事情只属于对象本身,只能本对象本身触发。使用事件是为了封装委托功能,让委托使用的更安全。就像属性封装字段一样意思。

如何实现接口事件

接口可以声明事件。 下面的示例演示如何在类中实现接口事件。 这些规则基本上都与实现任何接口方法或属性
时的相同。
在类中实现接口事件
在类中声明事件,然后在相应区域中调用它。

namespace ImplementInterfaceEvents
{
public interface IDrawingObject
{
event EventHandler ShapeChanged;
}
public class MyEventArgs : EventArgs
{
// class members
}
public class Shape : IDrawingObject
{
public event EventHandler ShapeChanged;
void ChangeShape()
{
// Do something here before the event...
OnShapeChanged(new MyEventArgs(/*arguments*/));
}
// or do something here after the event.
}
protected virtual void OnShapeChanged(MyEventArgs e)
{
ShapeChanged?.Invoke(this, e);
}
}

下面的示例演示如何处理不太常见的情况:类继承自两个或多个接口,且每个接口都具有相同名称的事件。 在这
种情况下,你必须为至少其中一个事件提供显式接口实现。 为事件编写显式接口实现时,还必须编写 add 和
remove 事件访问器。 通常这些访问器由编译器提供,但在这种情况下编译器不提供它们。
通过提供自己的访问器,可以指定两个事件是由类中的同一个事件表示,还是由不同事件表示。 例如,如果根据
接口规范应在不同时间引发事件,可以在类中将每个事件与单独实现关联。 在下面的示例中,订阅服务器确定它
们通过将形状引用转换为 IShape 或 IDrawingObject 接收哪个 OnDraw 事件。

namespace{
usingWrapTwoInterfaceEvents
System;
public interface IDrawingObject
{
// Raise this event before drawing
// the object.
// the object.
event EventHandler OnDraw;
}
public interface IShape
{
// Raise this event after drawing
// the shape.
event EventHandler OnDraw;
}
// Base class event publisher inherits two
// interfaces, each with an OnDraw event
public class Shape : IDrawingObject, IShape
{
// Create an event for each interface event
event EventHandler PreDrawEvent;
event EventHandler PostDrawEvent;
object objectLock = new Object();
// Explicit interface implementation required.
// Associate IDrawingObject's event with
// PreDrawEvent
#region IDrawingObjectOnDraw
event EventHandler IDrawingObject.OnDraw
{
add
{
lock (objectLock)
{
PreDrawEvent += value;
}
}
remove
{
lock (objectLock)
{
PreDrawEvent -= value;
}
}
}
#endregion
// Explicit interface implementation required.
// Associate IShape's event with
// PostDrawEvent
event EventHandler IShape.OnDraw
{
add
{
lock (objectLock)
{
PostDrawEvent += value;
}
}
remove
{
lock (objectLock)
{
PostDrawEvent -= value;
}
}
}
// For the sake of simplicity this one method
// implements both interfaces.
public void Draw()
{
// Raise IDrawingObject's event before the object is drawn.
PreDrawEvent?.Invoke(this, EventArgs.Empty);
Console.WriteLine("Drawing a shape.");
// Raise IShape's event after the object is drawn.
PostDrawEvent?.Invoke(this, EventArgs.Empty);
}
}
public class Subscriber1
{
// References the shape object as an IDrawingObject
public Subscriber1(Shape shape)
{
IDrawingObject d = (IDrawingObject)shape;
d.OnDraw += d_OnDraw;
}
void d_OnDraw(object sender, EventArgs e)
{
Console.WriteLine("Sub1 receives the IDrawingObject event.");
}
}
// References the shape object as an IShape
public class Subscriber2
{
public Subscriber2(Shape shape)
{
IShape d = (IShape)shape;
d.OnDraw += d_OnDraw;
}
}
void d_OnDraw(object sender, EventArgs e)
{
Console.WriteLine("Sub2 receives the IShape event.");
}
public class Program
{
static void Main(string[] args)
{
Shape shape = new Shape();
Subscriber1 sub = new Subscriber1(shape);
Subscriber2 sub2 = new Subscriber2(shape);
shape.Draw();
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
}
/* Output:
Sub1 receives the IDrawingObject event.
Drawing a shape.
Sub2 receives the IShape event.
*/

 

posted @ 2021-10-08 20:31  小林野夫  阅读(5059)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/