前言

发布订阅模式很常见,每个发布者和订阅者之间都搭建了一条小线路,随着功能越来越多,事件和委托就会满天飞,就像私拉电线的蜘蛛网一样。这时候可能需要一种集中式的事件处理方法,即事件总线。

1,简介

事件总线就像一个集线器,原本直接从起点到终点的连接,如今全部都要经过事件总线,发布者和订阅者完全地解耦了。发布者只需要向事件总线发起事件,不需要关心事件处理。订阅者只需要处理事件总线派发过来的事件,不需要关心事件的来源。

2,设计

2.1 设计思路

我希望事件总线是简单整洁灵活的

  • 定义一个事件接口IEventData,所有的事件类都应该继承此接口,用不同的类型代表不同的事件,并且事件类包含了全部事件信息。
  • 事件总线维护一个字典Dictionary<Type, List<Action<IEventData>>,第一个泛型参数Type表示事件类型,第二个泛型参数List<Action<IEventData>>表示事件处理委托列表。(此处用Action举例,Func是类似的)
  • 订阅者手动向事件总线注册事件处理委托
  • 发布者创建一个IEventData实例即可向事件总线触发事件,把事件处理委托列表调用一遍

2.2 设计实现

2.2.1 IEventData

//事件接口,所有的事件都要实现该接口
public interface IEventData
{
}

一个空接口就行,应用时再根据业务定义事件类

2.2.2 EventBus

public class EventBus
{
public static EventBus Default = new EventBus(); //单例 private readonly Dictionary<Type, List<object>> eventDataAndActionHandlerDic; //Action<IEventData>
private static readonly object 字典锁 = new object(); private EventBus()
{
eventDataAndActionHandlerDic = new Dictionary<Type, List<object>>();
} //手动注册事件处理方法
public void Register<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (!eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
eventDataAndActionHandlerDic.Add(typeof(TEventData), new List<object>());
}
List<object> actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
if (!actionList.Contains(action))
{
actionList.Add(action);
}
}
} //手动注销事件处理方法
public void UnRegister<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
List<object> actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
actionList.Remove(action);
}
}
} //触发事件
public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
{
if (eventData == null)
{
throw new ArgumentNullException(nameof(eventData));
}
List<object> actionList = null;
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
}
}
if(actionList != null)
{
for (var index = 0; index < actionList.Count; index++)
{
actionList[index].GetType().GetMethod("Invoke").Invoke(actionList[index], new object[] { eventData });
}
}
}
}

很简单,就3个方法(此处用Action举例,Func是类似的)

  • Register:将订阅者的事件处理委托添加到事件处理委托列表
  • UnRegister:将订阅者的事件处理委托从事件处理委托列表里移除
  • Trigger:根据事件类型从字典里拿到事件处理委托列表都调用一遍

2.2.3 用起来

假设一个应用场景,一个气象台发布天气,多个电视台接收天气。

创建一个winform项目。

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void 订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.Register<EventData<string>>(中国电视台.收到天气);
EventBus.Default.Register<EventData<string>>(米国电视台.收到天气);
}
private void 取消订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.UnRegister<EventData<string>>(中国电视台.收到天气);
EventBus.Default.UnRegister<EventData<string>>(米国电视台.收到天气);
} private 气象台 气象台 = new 气象台();
private void 播报天气_Click(object sender, EventArgs e)
{
气象台.播报天气("下雨");
}
} //带泛型负载的事件
public class EventData<TPayload> : IEventData
{
public TPayload Payload { get; protected set; } public EventData(TPayload payload)
{
Payload = payload;
}
}
public class 气象台
{
public void 播报天气(string 天气)
{
EventData<string> eventData = new EventData<string>(天气);
EventBus.Default.Trigger<EventData<string>>(eventData); //直接通过事件总线触发即可
}
}
public class 中国电视台
{
public static void 收到天气(EventData<string> eventData)
{
MessageBox.Show($"中国天气是{eventData.Payload}");
}
}
public class 米国电视台
{
public static void 收到天气(EventData<string> eventData)
{
MessageBox.Show($"米国天气是{eventData.Payload}");
}
}

根据业务定义了一个带泛型负载的事件EventData<TPayload>。发布者气象台可以通过Trigger方法发起事件。订阅者电视台通过Register方法注册事件处理委托。先点击订阅天气,再点击播报天气时就会弹窗。

3,问题

3.1 起缘

一切都看起来很棒,直到有一天电视台要拆掉了。

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
} private 中国电视台 中国电视台 = new 中国电视台();
private 米国电视台 米国电视台 = new 米国电视台();
private void 订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.Register<天气数据, string>(中国电视台.收到天气);
EventBus.Default.Register<天气数据, string>(米国电视台.收到天气);
}
private void 取消订阅天气_Click(object sender, EventArgs e)
{
EventBus.Default.UnRegister<天气数据, string>(中国电视台.收到天气);
EventBus.Default.UnRegister<天气数据, string>(米国电视台.收到天气);
}
private void 销毁电视台_Click(object sender, EventArgs e)
{
//如果没有取消订阅,内存就泄露了
中国电视台 = null;
米国电视台 = null;
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
textBox1.AppendText($"销毁电视台\r\n");
} private 气象台 气象台 = new 气象台();
private void 播报天气_Click(object sender, EventArgs e)
{
气象台.播报天气("下雪", 播报天气CallBack);
}
private void 播报天气CallBack(string obj)
{
textBox1.AppendText($"{obj}\r\n");
}
} //带负载的事件
public class 天气数据: IEventData
{
public string 天气;
public 天气数据(string 天气)
{
this.天气 = 天气;
}
} public class 气象台
{
public void 播报天气(string 天气, Action<string> callBack)
{
天气数据 eventData = new 天气数据(天气);
List<string> list = EventBus.Default.Trigger<天气数据, string>(eventData); //直接通过事件总线触发即可
foreach(var str in list)
{
callBack?.Invoke(str);
}
}
}
public class 中国电视台
{
public string 收到天气(天气数据 eventData)
{
return $"中国电视台收到的天气是{eventData.天气}";
}
}
public class 米国电视台
{
public string 收到天气(天气数据 eventData)
{
return $"米国电视台收到的天气是{eventData.天气}";
}
}

先点击订阅天气,再点击播报天气时就会显示天气,这都没问题。此时再点击销毁电视台,再点击播报天气时仍然会显示天气,这是一个常见的内存泄露。



销毁电视台前手动注销事件处理委托可以避免这个问题,但这真的很难保证。有没有一种即便不手动注销也能正常GC的方法呢?有,就是弱引用。这个问题本来和事件总线没啥关系,但如果用到了弱引用就需要对事件总线进行改造。

3.2 改造

注册了事件处理委托之后,事件总线对订阅者就是强引用关系,强引用在,GC永远无法回收被引用者。弱引用不计入引用计数,引用计数归零GC可以正常回收。弱引用在使用时先判断对象是否存在,如果存在才访问对象。

下面将事件总结对事件处理委托的引用改造成弱引用。更确切地说,就是使用弱引用对Dictionary<Type, List<Action<IEventData>>中的List<Action<IEventData>>进行封装。

public class WeakEvent<T>
{
private class ActionUnit //弱引用封装
{
private WeakReference reference;
private MethodInfo method;
private bool noTarget; public bool IsDead
{
get
{
return !this.noTarget && !this.reference.IsAlive;
}
}
public ActionUnit(Action<T> action)
{
this.noTarget = action.Target == null; //静态方法没有Target,所以noTarget就是isStaticMethod
this.reference = new WeakReference(action.Target); //action.Target是订阅者实例
this.method = action.Method; //Method是MethodInfo的实例,即订阅者的事件处理方法
}
public bool Equals(Action<T> action)
{
return this.reference.Target == action.Target && this.method == action.Method;
}
public void Invoke(object[] args)
{
this.method.Invoke(this.reference.Target, args); //reference.Target就是action.Target,WeakReference构造函数中传入的
}
} private List<ActionUnit> actionUnitlist = new List<ActionUnit>(); //弱引用封装列表 public int Count
{
get
{
return this.actionUnitlist.Count;
}
} public void Add(Action<T> action)
{
this.actionUnitlist.Add(new ActionUnit(action));
}
public void Remove(Action<T> action)
{
for (int i = this.actionUnitlist.Count - 1; i > -1; i--)
{
if (this.actionUnitlist[i].Equals(action))
{
this.actionUnitlist.RemoveAt(i);
}
}
}
public void Clear()
{
this.actionUnitlist.Clear();
}
public bool Contains(Action<T> action)
{
return this.actionUnitlist.Any(item => item.Equals(action));
} public void Invoke(T arg)
{
List<int> removeList = new List<int>();
for (int i = 0; i < this.actionUnitlist.Count; i++)
{
if (this.actionUnitlist[i].IsDead)
{
removeList.Add(i);
}
else
{
this.actionUnitlist[i].Invoke(new object[] { arg });
}
}
for (int i = removeList.Count - 1; i >= 0; i--)
{
this.actionUnitlist.RemoveAt(removeList[i]);
}
}
}

ActionUnit对应的是Action<TEventData>WeakEvent<T>对应的是List<Action<TEventData>>

添加事件处理委托action时,就使用弱引用表示订阅者new WeakReference(action.Target),这样就不会增加订阅者的引用计数。在调用事件处理委托时,会逐个判断订阅者是否存在,存在则调用事件处理委托,不存在的就从列表中移除掉。

public class EventBus
{
public static EventBus Default = new EventBus(); private readonly Dictionary<Type, object> eventDataAndActionHandlerDic;
private static readonly object 字典锁 = new object(); private EventBus()
{
eventDataAndActionHandlerDic = new Dictionary<Type, object>();
} //手动注册事件处理方法
public void Register<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (!eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
eventDataAndActionHandlerDic.Add(typeof(TEventData), new WeakEvent<TEventData>());
}
object actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
WeakEvent<TEventData> weakEvent = (WeakEvent<TEventData>)actionList;
if (!weakEvent.Contains(action))
{
weakEvent.Add(action);
}
}
} //手动注销事件处理方法
public void UnRegister<TEventData>(Action<TEventData> action) where TEventData : IEventData
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
object actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
((WeakEvent<TEventData>)actionList).Remove(action);
}
}
} //触发事件
public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
{
if (eventData == null)
{
throw new ArgumentNullException(nameof(eventData));
}
object actionList = null;
lock (字典锁)
{
if (eventDataAndActionHandlerDic.ContainsKey(typeof(TEventData)))
{
actionList = eventDataAndActionHandlerDic[typeof(TEventData)];
}
}
if(actionList != null)
{
((WeakEvent<TEventData>)actionList).Invoke(eventData);
}
}
}

EventBus和之前基本差不多,仅将List<Action<TEventData>>替换成WeakEvent<TEventData>

3.3 用起来

业务代码是一样的,结果就是销毁电视台就真的销毁了,再播报也不会显示天气了。

C#应用 - 事件总线的更多相关文章

  1. Android事件总线

    Android中Activity.Service.Fragment之间的相互通信比较麻烦,主要有以下一些方法: (1)使用广播,发送者发出广播,接收者接收广播后进行处理: (2)使用Handler和M ...

  2. ABP理论学习之事件总线和领域事件

    返回总目录 本篇目录 事件总线 定义事件 触发事件 处理事件 句柄注册 取消注册 在C#中,我们可以在一个类中定义自己的事件,而其他的类可以注册该事件,当某些事情发生时,可以通知到该类.这对于桌面应用 ...

  3. Lind.DDD.Events事件总线~自动化注册

    回到目录 让大叔兴奋的自动化注册 对于领域事件之前说过,在程序启动时订阅(注册)一些事件处理程序,然后在程序的具体位置去发布(触发)它,这是传统的pub/sub模式的体现,当然也没有什么问题,为了让它 ...

  4. Guava - EventBus(事件总线)

    Guava在guava-libraries中为我们提供了事件总线EventBus库,它是事件发布订阅模式的实现,让我们能在领域驱动设计(DDD)中以事件的弱引用本质对我们的模块和领域边界很好的解耦设计 ...

  5. DDD~领域事件与事件总线

    回到目录 谈谈它 终于有些眉目了,搜刮了很多牛人的资料,英文的,中文的,民国文的,终于小有成就了,同时也做了个DEMO,领域事件这东西好,但需要你明白它之后才会说好,而对于明白领域事件这件事来说,它的 ...

  6. Android学习系列(43)--使用事件总线框架EventBus和Otto

    事件总线框架 针对事件提供统一订阅,发布以达到组件间通信的解决方案. 原理 观察者模式. EventBus和Otto 先看EventBus的官方定义: Android optimized event ...

  7. ASP.NET ZERO 学习 事件总线

    用于注册和触发客户端的全局事件. 介绍 Pub/sub事件模型广泛用于客户端,ABP包含了一个简单的全局事件总线来 注册并 触发事件. 注册事件 可以使用abp.event.on来注册一个全局事件.一 ...

  8. 【DDD-Apwork框架】事件总线和事件聚合器

    第一步:事件总线和事件聚合器 [1]事件总线 IEventBus IUnitOfWork.cs using System; using System.Collections.Generic; usin ...

  9. AndroidEventBus ( 事件总线 ) 的设计与实现

    1. 功能介绍 AndroidEventBus是一个Android平台的事件总线库, 它简化了Activity.Fragment.Service等组件或者对象之间的交互,非常大程度上减少了它们之间的耦 ...

  10. Guava: 事件总线EventBus

    EventBus 直译过来就是事件总线,它使用发布订阅模式支持组件之间的通信,不需要显式地注册回调,比观察者模式更灵活,可用于替换Java中传统的事件监听模式,EventBus的作用就是解耦,它不是通 ...

随机推荐

  1. 海思SDK 学习 :000-海思HI35xx平台软件开发快速入门之背景知识

    背景 参考自:<HiMPP V3.0 媒体处理软件开发参考.pdf> 由于在音视频处理领域,海思芯片占有全球市场的很大份额.当我们选择使用海思芯片开发时,程序开发模型主要是围绕HIMPP( ...

  2. LangGraph实战

    1.概述 前段时间LangChain发布了LangGraph,它引起了很多关注.LangGraph 的主要优势在于它能够实现循环工作流,这对于在 LLM 应用程序中模拟类似代理的行为至关重要.本篇博客 ...

  3. WPF在.NET9中的重大更新:Windows 11 主题

    在2023年的2月20日,在WPF的讨论区,WPF团队对路线的优先级发起了一次讨论. 对三个事项发起了投票. 第一个是Windows 11 主题 第二个是更新的控件 第三个是可空性注释 最终Windo ...

  4. 『vulnhub系列』Hack Me Please-1

    『vulnhub系列』Hack Me Please-1 下载地址: https://www.vulnhub.com/entry/hack-me-please-1,731/ 信息搜集: 使用nmap进行 ...

  5. 单片机升级,推荐此79元双核A7@1.2GHz国产平台的8个理由

    含税79元即可运行Linux操作系统 对于嵌入式软件开发者而言,单片机令人最痛苦的莫过于文件操作.79元T113-i工业核心板(基于全志国产处理器,国产化率100%)可运行Linux操作系统,可使用L ...

  6. 【资料分享】全志科技T507工业核心板硬件说明书(上)

    目    录 前言 1硬件资源 1.1CPU 1.2ROM 1.3RAM 1.4时钟系统 1.5电源 1.6LED 1.7外设资源 2引脚说明 2.1引脚排列 2.2引脚定义 2.3内部引脚使用说明 ...

  7. 在Mac上使用Emacs初步

    其他操作系统估计也差不多. 安装 如果使用brew就是brew install emacs.安装后不会在Applications里面显示一个程序,需要在命令行里执行emacs. 使用 进入和退出 上面 ...

  8. TCP/UDP 协议和 HTTP/FTP/SMTP 协议之间的区别

    前言 我们经常会听到HTTP协议.TCP/IP协议.UDP协议.Socket.Socket长连接.Socket连接池等字眼,然而它们之间的关系.区别及原理并不是所有人都能理解清楚. 计算机网络体系结构 ...

  9. 谈谈你对MVVM开发模式和MVT的理解?

    MVVM分为Model.View.ViewModel三者. Model 代表数据模型,数据和业务逻辑都在Model层中定义: View 代表UI视图,负责数据的展示: ViewModel 负责监听 M ...

  10. 火山引擎数智平台赋能火花思维,A/B测试加速创新

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群.   在数字化浪潮下,火花思维凭借其对数据驱动的理解与实践,搭上了业务快速增长的快车.这一效果的背后,离不开火花思 ...