zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C#中的委托与事件

c#事件 委托
2023-09-11 14:18:38 时间

委托

声明一个委托类(型):

public delegate int Comparison<in T>(T left, T right);

这里声明了一个委托,叫Comparison(Comparison等级上是与String、Button等一样的,是类,非变量),用Comparison可以创建实例(就像用String str = new String() 创建str实例一样),一般我们把这个实例也叫做“委托”,可以把一个签名与上面声明语句一样的的方法(上面语句中是返回一个int类型,并具有两个T类型的参数的方法)赋值给用Comparison声明的实例。

需要注意的点:

1.delegate是委托关键字,使用该关键字后编译器会生成一个类,它派生自与使用的签名匹配的 System.Delegate。也就是说,上面的声明语句实际上相当于定义了一个派生自System.Delegate的一个类。
2.语法可能看起来像是声明变量,但它实际上是声明class(只不过该class是派生自 System.Delegate)。
3.可以在类中、直接在命名空间中、甚至是在全局命名空间中定义委托类型。
4.建议不要直接在全局命名空间中声明委托类型(或其他类型)。
5.委托代表的是方法,任何签名相同的方法都可以赋值给委托变量(实例)。类似c中的函数指针。

声明实例:

public Comparison<T> comparator;

由于委托是类型所以上面代码中变量(实例)的类型是 Comparison<T>(前面定义的委托类型)。 变量(实例)的名称是 comparator。

分配、添加和移除调用目标:

1.在给委托变量(实例)赋值之前使用,会造成NullReferenceException异常。
2.可以使用lambda表达式的方式分配方法给委托变量(实例):

Comparison<string> comparer = (left, right) => left.Length.CompareTo(right.Length);

3.也可以直接分配与委托类型签名一样的函数名给委托变量(实例)

Comparison<string> comparer = CompareLength;

4.调用的时候直接执行comparer()方法就类似调用绑定到委托上的方法。 

Delegate和MulticastDelegate

C# 编译器会在你使用 C# 语言关键字声明委托类型时,创建派生自MulticastDelegate 的类的实例。也就是我们使用的每个委托都派生自 MulticastDelegate(MulticastDelegate派生自System.Delegate)。
多播委托意味着通过委托进行调用时,可以调用多个方法目标(通过+=添加到委托上)。关于这两个类具体可以查看.net类库。
这里做一个多播的示例:

using System;
namespace testBroadCast
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("start");

            SaySomthing saySomthing = null;
            saySomthing += sayHi;
            saySomthing += sayHello;

            saySomthing();
            Console.ReadKey();
        }

        private delegate void SaySomthing();

        private static void sayHi()
        {
            Console.WriteLine("hi");
        }
        private static void sayHello()
        {
            Console.WriteLine("hello");
        }
    }
}

输出如下

强类型委托

实际上,无论何时需要不同的方法签名,这都会创建新的委托类型。 一段时间后此操作可能变得繁琐。 每个新功能都需要新的委托类型。幸运的是,没有必要这样做。 .NET Core 框架包含几个在需要委托类型时可重用的类型。 这些是泛型定义,因此需要新的方法声明时可以声明自定义。
1.Action 类型

public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

Actoin类型封装了返回void类型与一些参数的签名。

2.Func 类型

public delegate TResult Func<out TResult>();
public delegate TResult Func<in T1, out TResult>(T1 arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

Func类型相对于Action有返回值类型。

3. Predicate<T> 类型

public delegate bool Predicate<in T>(T obj);

对于下面这种情况

Func<string, bool> TestForString;
Predicate<string> AnotherTestForString;

你可能认为这两种类型是等效的。 其实它们不是。 这两个变量不能互换使用。 一种类型的变量无法赋予另一种类型。C# 类型系统使用的是已定义类型的名称,而不是其结构。
.NET Core 库中的所有这些委托类型定义意味着你不需要为创建的任何需要委托的新功能定义新的委托类型。 这些泛型定义应已提供大多数情况下所需要的所有委托类型。 只需使用所需的类型参数实例化其中一个类型。 对于可成为泛型算法的算法,这些委托可以用作泛型类型。这样可以节省时间,并尽量减少为了使用委托而需要创建的新类型的数目。

Invoke()方法
暂时未完成该部分

委托的常见应用场景:

委托提供了一种机制,可实现涉及组件间最小耦合度的软件设计。通过参数传递进来具体的实现方法,可以在设计好接口功能之后不用管底层到底是如何实现的,哪怕底层有n中实现方式,也不需要修改上层的代码,只管修改实现即可。参考下方的链接的示例
https://docs.microsoft.com/zh-cn/dotnet/csharp/delegates-patterns


事件

使用 event 关键字声明一个事件:

public event EventHandler<FileListArgs> Progress; 

该事件的类型必须为委托类型,在此示例中,为 EventHandler<FileListArgs>
对于上面事件的声明,实际上编译器会为委托类型EventHandler<FileListArgs>创建一个新的实例,并把这个实例赋值给Progress。事实上事件是一种特殊的多播委托他的特殊之处在于仅可以从声明事件的类或结构(发布服务器类)中对其进行调用,类之外的代码无法引发事件,也不能执行任何其它操作。 如果其他类或结构订阅该事件,则在发布服务器类引发该事件时,将调用其事件处理程序方法。

使用事件需要注意的一些地方
1.特定事件可能没有任何注册的对象。必须编写代码,以确保在未配置侦听器时不会引发事件。
2.要引发事件需要使用委托调用语法调用事件处理程序。

下面展示如何引发事件并确保未配置侦听器时不会引发事件:

Progress?.Invoke(this, new FileListArgs(file));  //?. 运算符可以轻松确保在事件没有订阅服务器时不引发事件。

1.通过订阅事件,可以在两个对象(事件源和事件接收器)之间创建耦合。 

2.需要确保当不再对事件感兴趣时,事件接收器将从事件源取消订阅。

一些涉及到事件的类
1.EventHandler类

[System.Runtime.InteropServices.ComVisible(true)]
public delegate void EventHandler(object sender, EventArgs e); 

2.EventHandler<TEventArgs>类

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e); 

3.EventArgs类

[System.Runtime.InteropServices.ComVisible(true)]
public class EventArgs; 

EventArgs的实例用作事件事件处理函数的参数,承载事件数据,该类有n多派生类,都是同样的功能。下面会详细介绍。

事件声明应为谓词或谓词短语
当事件报告已发生的事情时,请使用过去时。
使用现在时谓词(例如 Closing)报告将要发生的事情。 通常,使用现在时表示类支持某种类型的自定义行为。 最常见的方案之一是支持取消。 例如,Closing 事件可能包括指示是否应继续执行关闭操作的参数。 其他方案可能会允许调用方通过更新事件参数的属性来修改行为。 你可以引发一个事件以指示算法将采取的建议的下一步操作。 事件处理程序可以通过修改事件参数的属性授权不同的操作。

通过使用 += 运算符订阅事件:

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

处理程序方法通常为前缀“On”,后跟事件名称,如上所示。
使用 -= 运算符取消订阅。 

.NET 事件委托的标准签名是:

void OnEventRaised(object sender, EventArgs args); 

返回类型为 void。 事件基于委托,而且是多播委托。 对任何事件源都支持多个订阅服务器。
参数列表包含两种参数:发件人事件参数。 
sender 的编译时类型为 System.Object,即使有一个始终正确的更底层派生的类型亦是如此。 按照惯例使用 object
args 通常是派生自 System.EventArgs 的类型。 (在.net core中此约定不再强制执行。)即使事件类型无需任何其他参数,你仍将提供这两种参数。 应使用特殊值 EventArgs.Empty 来表示事件不包含任何附加信息。EventArgs类表示包含事件数据的类的基类,并提供要用于不包含事件数据的事件的值,直接继承自Object。

要将事件添加到类,最简单的方式是将该事件声明为公共字段,如上面的示例中所示:

public event EventHandler<FileFoundArgs> FileFound;

看起来它像在声明一个公共字段,这似乎是一个面向对象的不良实践,因为多数情况下我们希望通过属性或方法来保护数据访问。尽管这看起来像一次不良实践,但通过编译器生成的代码却创建了包装器,使事件对象仅能以安全的方式进行访问。 

在类似字段的事件上,唯一可用的操作是添加处理程序和删除处理程序

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
Console.WriteLine(eventArgs.FoundFile);
filesFound++;
};
//添加
lister.FileFound += onFileFound;
//删除
lister.FileFound -= onFileFound;

从事件订阅服务器返回值(即从事件的订阅者方法返回给事件)
事件处理程序不返回值,因此需以其它方式进行通信。 标准事件模式使用 EventArgs 对象来包含字段,事件订阅服务器使用这些字段进行通信取消。其中一种模式允许任一订阅服务器取消操作。 在此模式下,新字段会初始化为 false。 任何订阅服务器都可将其更改为 true。 当所有订阅服务器观察到事件已引发后,FileSearcher 组件将检查布尔值,并执行操作。关于这一部分的详细操作请看下面链接,这里不在详细说明。https://docs.microsoft.com/zh-cn/dotnet/csharp/event-pattern

.NET Core 事件模式的一些差别
.NET Core 的模式较为宽松。 EventHandler<TEventArgs> 定义不再要求 TEventArgs 必须是派生自 System.EventArgs 的类。

事件的使用示例(阻塞方式)
首先写一个带有自定义事件的类

using System;
using System.Threading;

namespace CSharpEventTest
{
    public class Dog
    {
        /// <summary>
        /// 自定义事件
        /// </summary>
        public event EventHandler EatCompleted;


        /// <summary>
        /// Feed方法
        /// </summary>
        public void Feed()
        {
            Thread.Sleep(500);
            EatCompleted?.Invoke(this, EventArgs.Empty);
        }
    }
}

然后在窗体中绑定事件处理程序,并调用能触发事件的方法

using System;
using System.Windows.Forms;

namespace CSharpEventTest
{
    public partial class Form1 : Form
    {
        /// <summary>
        /// Form1的构造函数
        /// </summary>
        public Form1()
        {
            InitializeComponent();

            Dog dog = new Dog();
            dog.EatCompleted += OnEatCompleted;
            dog.Feed();
        }

        /// <summary>
        /// 事件处理程序
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnEatCompleted(object sender, EventArgs e)
        {
            MessageBox.Show("EatCompleted");
        }
    }
}

效果如下:

相关资料:https://docs.microsoft.com/zh-cn/dotnet/csharp/event-pattern


委托和事件的异步操作

事件的异步操作是很重要的,一个我常见的使用场景:开一个IO读写线程用于读写数据,读到或写完后反馈到UI界面更改控件显示的内容。
这里就要使用异步的事件处理。建议使用BackgroundWorker类来做,比较简单。关于BackgroundWorker类的使用我专门写了一篇文章,再次把示例代码复制过来:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestBackGroundWorkerCompleteEvent
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            backgroundWorker.DoWork += OnDoWork;
            backgroundWorker.RunWorkerCompleted += OnRunWorkerCompleted;
        }        

        static BackgroundWorker backgroundWorker = new BackgroundWorker();

        private void OnDoWork(object sender, DoWorkEventArgs e)
        {
            Thread.Sleep(1000);
            e.Result = "任务完成!";//传递结果
        }

        private void OnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            Action action = new Action(() =>
            {
                textBox.Text = e.Result.ToString();
            });

            this.textBox.BeginInvoke(action);  //在控件创建的线程更新控件,必须要执行上面的委托才行          
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            backgroundWorker.RunWorkerAsync();
        }
    }
}

关于委托和事件的异步操作还有很多,回头再单独写。