一、前言
笔者曾在高中时写过一篇文风浮夸的文章(点我跳转),虽然发布在2021年,但是它的诞生时间更早(大概2020),只不过中途进了回收站,转眼笔者已经大一了,就想写一个属于自己的初步游戏框架(进阶工具集),其中最重要的一部分就是消息驱动。本文需要一定的编程基础。
另外本文不会讲很多内容,仅仅是作为高一时那篇文章的填坑笔记,简单讨论一下怎么设计一个比较好的消息机制。
二、填坑秘籍
第一个坑:泛型太多难维护
当然还是跟上篇文章非常类似,主要是为了那篇文章的坑。
那篇文章里说到Register和Remove,Invoke的时候只给出了无参数的实现,然后我给出的后续实现是通过写一大堆泛型来补充参数。
void Register<T,K,A,V>(T a,K b, A c,V d);
void Register<T,K,A>(T a,K b, A c);
void Register<T,K>(T a,K b);
void Register<T>(T a);
void Register();
这样的函数要写多个重载,代码很难维护…但是性能非常好
当然读者可能在网上看到这种实现
void Register(params object[] parm);
//void Register(params dynamic[] parm); 也是object实现的有装箱
void MyMethod()
{
Register(1,"str");
}
这种实现最大的问题就是在值类型(栈空间)到object(堆空间)的装箱过程,性能瓶颈,但是代码极容易维护。
我无意间想起C# 提供了一种新的数据类型,Tuple/ValueTuple(元组),
void Register(ValueTuple tuple);
void MyMethod()
{
Register((10,"30"));
}
这样可以避免装箱问题,但是要求用户注册的委托只能带有ValueTuple类型的唯一参数然后自己处理元组类型转换,一定程度上有些侵入性(对用户有强制性),但是代码变得容易维护且兼顾性能。
如果你不能很好的理解这些内容,请尝试自己实现上篇文章的消息机制。
至于选择什么,还要看具体情景,没有最好的设计,只有最合适。
第二个坑:选什么当key
上篇文章我们只提到了三种选择,如下
1.string - 描述性极强,容易引发GC (string来回拷贝)
2.enum - 兼顾描述性和性能但不是很方便(手动增加枚举)
3.int/long - 不具备描述性 (不知道int对应什么事件)
虽然心里一百个不愿意用int,但是很多公司确实是这样做的,但是会建立工具自动生成并绑定int和一些描述性的值
后来受到启发,我发现任意类型都可以作为key,如下
1.object - 较为松散,容易引发装箱,通过C#的拓展方法可以实现object.Send()这样的快乐API
2. interface - 可以要求用户仅使用实现接口的类作为key,也有object的优点
可能读完这段内容难以理解,简单概括上篇文章的Dictionary < TKey,Delegate > 中的Key可以是任意类型,不一定非要局限在一些特殊类型。
interface IMessage{}
class MyMessage:IMessage
{
public int id = 10;
}
class AObj
{
void OnMyMessage(MyMessage message)
{
//处理消息
}
void MyMethod()
{
Register<MyMessage>(OnMyMessage);
Send<MyMessage>(new MyMessage{id = 20});
}
}
这些内容笔者因为还在上学没有时间统一整理,只能简单对读者进行启发,更多还是要靠自己探索。建议读者康康Github上的这些优秀项目的相关实现
MediatR
IFramework
在下一篇文章:基于观察者模式的全局消息机制中,笔者将会介绍使用观察者模式实现消息机制