目录:
本章主要记录如何使用事件。
【参考文章】《c# 图解教程》(第五版)
事件
事件是类或结构的成员。事件成员被隐式自动初始化为null。
当一个特定的程序事件发生时,程序的其他部分可以得到该事件已经发生的通知。
事件和委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托。
事件包含了一个私有的委托,关于事件的私有委托需要了解:
- 事件提供了对它的私有控制委托的结构化访问,也就是说,无法直接访问该委托。
- 事件中可用的操作比委托要少,只能添加,删除或调用事件处理程序。
- 事件被触发时,它调用委托来依次调用调用列表中的方法。
事件必要5元素
- 声明委托(类型):事件和事件处理程序必须有共同的参数和返回类型,它们通过委托类型进行描述。
delegate void Handler();
- 声明事件处理程序(订阅者):订阅者类中会在事件触发时执行的方法声明。
void IncrementDozensCount()
{
DozensCount++;
}
- 声明事件(发布者):发布者必须声明一个订阅者可以注册的事件成员。当类事件为public时,称为发布了事件。
class Incrementer
{
//关键字:event+委托类型:EventHandler+事件名:CountedADozen1
public event EventHandler CountedADozen1;
//多个事件用逗号隔开
public event EventHandler MyEvent1, MyEvent2,MyEvent3;
//static 让事件变成静态的
public static event EventHandler CountedADozen2;
}
- 订阅事件/注册事件(一般是订阅者):订阅者必须注册事件,才能在事件被触发时得到通知。
//方法引用形式
//incrementer: 类; IncrementDozensCount: 实例方法
incrementer.CounterADozen += IncrementDozensCount;
//方法引用形式
//CounterADozen: 事件成员; ClassB.CounterHandlerB: 静态方法
incrementer.CounterADozen += ClassB.CounterHandlerB;
//委托形式
mc.CounterADozen += new EventHandler(cc.CounterHandlerC);
//Lambda形式
mc.CounterADozen += () => DozensCount++; //隐式+无参
//匿名方法
mc.CounterADozen += delegate { DozensCount++ };
- 触发事件(发布者):发布者类中触发事件,并导致调用注册的所有事件处理程序的代码。
if(CounterADozen != null) //确认有方法可以执行
{
//CounterADozen: 事件名称; (source, args): 参数列表
CounterADozen(source, args); //触发事件
}
无参事件demo
delegate void Handler(); //声明委托
// 发布者:声明事件+触发事件
class Incrementer
{
public event Handler CountedADozen; //声明事件
public void DoCount()
{
for (int i = 1; i<100; i++)
{
if (i%12 ==0)
{
if (null != CountedADozen)
{
CountedADozen(); //触发事件
}
}
}
}
}
// 订阅者:订阅事件+声明事件处理程序
class Dozens
{
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; //订阅事件(注册事件)
}
void IncrementDozensCount()
{
DozensCount++; //声明事件处理程序
}
}
class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount);
//执行顺序
//1. 创建 Incrementer 实例
//2.创建 Dozens 实例,在构造函数中为 Incrementer 实例订阅事件
//3.Incrementer 实例执行 DoCount 方法,在满足条件时 触发事件,执行事件处理程序
}
}
输出结果:
Number of dozens = 8
有参事件demo
public class IncrementerEventArgs : EventArgs //自定义类派生自 EventArgs
{
public int IterationCount { get; set; } //存储一个整数
}
// 发布者:声明事件+触发事件
class Incrementer
{
public event EventHandler<IncrementerEventArgs> CountedADozen; //声明事件
public void DoCount()
{
IncrementerEventArgs args = new IncrementerEventArgs();
for (int i = 1; i < 100; i++)
{
if (i % 12 == 0 && null != CountedADozen)
{
args.IterationCount = i;
CountedADozen(this, args); //触发事件,并传递参数
}
}
}
}
// 订阅者:订阅事件+声明事件处理程序
class Dozens
{
public int DozensCount { get; private set; }
public Dozens(Incrementer incrementer)
{
DozensCount = 0;
incrementer.CountedADozen += IncrementDozensCount; //订阅事件(注册事件)
}
void IncrementDozensCount(object source, IncrementerEventArgs e)
{
Console.WriteLine($"Incremented at iteration: {e.IterationCount} in {source.ToString()}");
DozensCount++; //声明事件处理程序
}
}
class Program
{
static void Main()
{
Incrementer incrementer = new Incrementer();
Dozens dozensCounter = new Dozens(incrementer);
incrementer.DoCount();
Console.WriteLine("Number of dozens = {0}", dozensCounter.DozensCount);
}
}
输出结果:
Incremented at iteration: 12 in DelegateEventDemo.EventDemo.Incrementer
Incremented at iteration: 24 in DelegateEventDemo.EventDemo.Incrementer
Incremented at iteration: 36 in DelegateEventDemo.EventDemo.Incrementer
Incremented at iteration: 48 in DelegateEventDemo.EventDemo.Incrementer
Incremented at iteration: 60 in DelegateEventDemo.EventDemo.Incrementer
Incremented at iteration: 72 in DelegateEventDemo.EventDemo.Incrementer
Incremented at iteration: 84 in DelegateEventDemo.EventDemo.Incrementer
Incremented at iteration: 96 in DelegateEventDemo.EventDemo.Incrementer
Number of dozens = 8
事件和委托
在 C# 中,事件通常与委托结合使用。这是因为事件本质上就是基于委托的一种特殊机制,用于实现对象间的解耦通信。下面我将详细解释何时以及如何将事件与委托结合使用。
事件与委托的关系
-
事件的基础是委托:
- 事件实际上是基于委托的,事件本身就是一个特殊的委托类型。
- 事件的声明通常使用委托类型来定义事件处理程序的签名。
-
事件的安全性:
- 事件提供了额外的安全性,确保只有在事件被触发时才能调用事件处理程序。
- 事件还确保了事件处理程序不会被意外修改或删除。
-
事件的订阅和取消订阅:
- 事件允许对象订阅事件,并在不再需要时取消订阅。
- 订阅事件时,实际上是将事件处理程序添加到事件中;取消订阅则是从事件中移除事件处理程序。
何时结合使用
-
对象间通信:
- 当你需要在对象之间传递信息时,可以使用事件。
- 例如,一个对象检测到了某种状态的变化,然后通过触发事件来通知其他对象。
-
解耦设计:
- 当你想实现解耦的设计时,可以使用事件。
- 发布事件的对象(事件发布者)不需要知道哪些对象订阅了事件,也不需要了解事件处理程序的实现细节。
-
异步处理:
- 当你需要异步地处理某些操作时,可以使用事件。
- 例如,在用户界面中,你可以订阅控件的事件来响应用户的操作。
-
多处理器订阅:
- 当多个对象需要订阅同一个事件时,可以使用事件。
- 事件可以被多个对象订阅,当事件被触发时,所有订阅的事件处理程序都会被调用。
示例代码
下面是一个具体的示例,演示了如何将事件与委托结合使用:
using System;
public class MyClass
{
// 声明一个事件
public event Action<string> OnDataReceived;
public void ReceiveData(string data)
{
// 触发事件
OnDataReceived?.Invoke(data);
}
}
public class Program
{
static void Main()
{
var myClass = new MyClass();
// 订阅事件
myClass.OnDataReceived += DataReceivedHandler;
// 触发事件
myClass.ReceiveData("Hello, Event Handling!");
// 等待输入以保持控制台窗口打开
Console.ReadKey();
}
static void DataReceivedHandler(string data)
{
// 处理事件
Console.WriteLine("Received data: " + data);
}
}
解释
-
声明事件:
- 在
MyClass
类中声明了一个名为OnDataReceived
的事件,它接受一个字符串参数,并使用Action<string>
委托类型。
- 在
-
订阅事件:
- 在
Program
类的Main
方法中,通过将DataReceivedHandler
方法添加到OnDataReceived
事件中来订阅事件。 - 这里使用的是
+=
运算符来订阅事件,它将事件处理程序添加到事件中。
- 在
-
触发事件:
- 当调用
ReceiveData
方法时,MyClass
类中的OnDataReceived
事件被触发。 - 这个事件由
MyClass
类触发,因为它定义了事件并实现了触发事件的逻辑。
- 当调用
-
处理事件:
DataReceivedHandler
方法作为事件处理程序,负责处理接收到的数据。- 当事件被触发时,事件处理程序
DataReceivedHandler
被调用。
-
总结
- 事件与委托结合使用是为了实现对象间的解耦通信。通过使用事件,你可以轻松地在对象之间传递信息,而不需要直接调用其他对象的方法。
同步、异步事件
事件本身不一定必须是异步的。事件机制本身是用于解耦对象间的通信,无论是同步还是异步都可以使用事件。事件是否异步取决于事件处理程序的实现以及事件发布者的触发方式。
同步事件
同步事件是指事件处理程序在事件被触发时立即执行,并且执行期间不会返回控制权给调用方。这是最常见的情况,尤其是在单线程环境中。
using System;
public class MyClass
{
public event Action<string> OnDataReceived;
public void ReceiveData(string data)
{
// 触发事件
OnDataReceived?.Invoke(data);
}
}
public class Program
{
static void Main()
{
var myClass = new MyClass();
// 订阅事件
myClass.OnDataReceived += DataReceivedHandler;
// 触发事件
myClass.ReceiveData("Hello, Event Handling!");
// 等待输入以保持控制台窗口打开
Console.ReadKey();
}
static void DataReceivedHandler(string data)
{
// 处理事件
Console.WriteLine("Received data: " + data);
}
}
异步事件
异步事件是指事件处理程序在一个独立的线程或异步上下文中执行,这样事件处理程序可以在后台执行,而不会阻塞事件发布者或其他正在运行的代码。
using System;
using System.Threading.Tasks;
public class MyClass
{
public event Action<string> OnDataReceived;
public void ReceiveData(string data)
{
// 异步触发事件
Task.Run(() => OnDataReceived?.Invoke(data));
}
}
public class Program
{
static void Main()
{
var myClass = new MyClass();
// 订阅事件
myClass.OnDataReceived += DataReceivedHandler;
// 触发事件
myClass.ReceiveData("Hello, Async Event Handling!");
// 等待输入以保持控制台窗口打开
Console.ReadKey();
}
static void DataReceivedHandler(string data)
{
// 处理事件
Console.WriteLine("Received data: " + data);
}
}
总结
-
同步事件:
- 事件处理程序在主线程上同步执行。
- 适合于简单的事件处理或不需要长时间执行的任务。
-
异步事件:
- 事件处理程序在独立的线程或异步上下文中执行。
- 适合于需要长时间执行的任务,以避免阻塞主线程。
注意事项
-
UI 更新:
- 在 GUI 应用程序中,如果事件处理程序需要更新 UI,那么通常需要确保更新操作在 UI 线程上执行。
- 例如,在 WinForms 中,你可以使用
Control.Invoke
或Control.BeginInvoke
方法来确保更新操作在 UI 线程上执行。
-
异步上下文:
- 如果你的应用是基于异步编程模型的,那么通常推荐使用异步事件处理。
- 使用
async/await
关键字可以简化异步事件处理的实现。
-
性能考虑:
- 对于资源密集型的操作,异步事件处理可以显著提高应用程序的响应性和效率。
问题:Task.Run();是否会创建大量线程?
你提到的 Task.Run(() => OnDataReceived?.Invoke(data));
确实可能会创建新的线程来执行事件处理程序,但这并不意味着一定会创建大量的线程。下面我会详细解释这一点,并提供一些建议来避免不必要的线程创建。
Task.Run
的行为
-
线程池线程:
Task.Run
通常会从 .NET 的线程池中获取可用的线程来执行任务。- 如果线程池中有可用的线程,那么这些线程会被重用,而不是创建新的线程。
-
新线程的创建:
- 当线程池中的线程都被占用时,
Task.Run
会创建新的线程来执行任务。 - 但是,.NET Framework 会尽力管理线程池中的线程数量,以避免不必要的线程创建。
- 当线程池中的线程都被占用时,
-
线程池的管理:
- 线程池会根据当前系统的负载动态调整线程的数量。
- 当线程空闲时,它们会被返回到线程池中,以备后续使用。
避免大量线程创建的策略
-
使用
Task
的复用:- 如果可能的话,尽量复用相同的
Task
实例,而不是每次触发事件都创建新的Task
。 - 例如,你可以创建一个可复用的
Task
实例,并在其完成后重新安排它。
- 如果可能的话,尽量复用相同的
-
使用
TaskScheduler
:- 你可以使用特定的
TaskScheduler
来控制任务的执行。 - 例如,使用
TaskScheduler.FromCurrentSynchronizationContext()
可以确保任务在当前同步上下文中执行,这对于 UI 更新特别有用。
- 你可以使用特定的
-
异步编程:
- 如果你的应用支持异步编程,可以使用
async/await
关键字来简化异步事件处理。 - 这可以避免显式创建新的
Task
实例,并利用现有的异步机制。
- 如果你的应用支持异步编程,可以使用
下面是一个修改后的示例,展示了如何使用异步编程来处理事件:
using System;
using System.Threading.Tasks;
public class MyClass
{
public event Action<string> OnDataReceived;
public async void ReceiveDataAsync(string data)
{
// 异步触发事件
await Task.Run(() => OnDataReceived?.Invoke(data));
}
}
public class Program
{
static void Main()
{
var myClass = new MyClass();
// 订阅事件
myClass.OnDataReceived += DataReceivedHandler;
// 触发事件
myClass.ReceiveDataAsync("Hello, Async Event Handling!");
// 等待输入以保持控制台窗口打开
Console.ReadKey();
}
static void DataReceivedHandler(string data)
{
// 处理事件
Console.WriteLine("Received data: " + data);
}
}
注意事项
-
异步上下文:
- 在 GUI 应用程序中,如果事件处理程序需要更新 UI,那么通常需要确保更新操作在 UI 线程上执行。
- 例如,在 WinForms 中,你可以使用
Control.Invoke
或Control.BeginInvoke
方法来确保更新操作在 UI 线程上执行。
-
性能考虑:
- 对于资源密集型的操作,异步事件处理可以显著提高应用程序的响应性和效率。
- 但是,频繁地使用
Task.Run
可能会导致不必要的线程创建,因此应该谨慎使用。
-
线程池管理:
- .NET Framework 的线程池管理机制通常能够有效地管理线程池中的线程数量,以避免不必要的线程创建。