背景
我在项目中大量使用 [EventInterface] 接口事件,非常喜欢这个设计:一个接口声明就能同时产出事件 ID 常量、分发实现和类型安全的发送入口,是我们见过最干净的事件方案之一。
不过我注意到,发送侧和监听侧的自动化程度是不对称的。
发送侧:完全类型安全
GameEvent.Get<ITimeCountDown>().OnTimeCountDownChange(timer, timeLong); // 签名由编译器保证
监听侧:退化为「裸 int + 手写泛型」
GameEvent.AddEventListener<float, float>(ITimeCountDown_Event.OnTimeCountDownChange, OnTimeCountDownChange);
这里的 <float, float> 需要人肉与接口方法签名对齐。框架提供了 GameEventAnalyzer(EVENT001/002)来兜底,很感谢这个设计,但它属于事后纠错——实际流程是「写错 → 编译报错 → 跑 CodeFix 修」,而不是「写不错」。
想请教的问题
监听侧没有做成生成式的强类型 API,是有意的设计取舍,还是暂时没排上日程?
猜测可能存在以下考量,想确认一下:
GameEvent.Get<T>() 的语义是「取发送端包装器」,不希望它同时承担监听职责?
- 扩展方法会污染接口类型的补全列表,影响
GameEvent.Get<IXxx>(). 之后的输入体验?
- 希望保持
AddEventListener 作为唯一注册入口,便于 GameEventMgr / AddUIEvent 统一做生命周期管理?
- 纯粹是优先级问题?
建议方案(供参考,不一定是最优解)
在 EventInterfaceGenerator 中,除了现有的 {Iface}_Event 与 {Iface}_Gen,再为每个接口生成一个扩展方法类:
// 生成:ITimeCountDown_EventExt.g.cs
public static class ITimeCountDown_EventExt
{
public static bool AddListener_OnTimeCountDownChange(this ITimeCountDown _, Action<float, float> handler)
=> GameEvent.AddEventListener(ITimeCountDown_Event.OnTimeCountDownChange, handler);
public static void RemoveListener_OnTimeCountDownChange(this ITimeCountDown _, Action<float, float> handler)
=> GameEvent.RemoveEventListener(ITimeCountDown_Event.OnTimeCountDownChange, handler);
}
调用侧:
GameEvent.Get<ITimeCountDown>().AddListener_OnTimeCountDownChange(OnTimeCountDownChange);
GameEvent.Get<ITimeCountDown>().RemoveListener_OnTimeCountDownChange(OnTimeCountDownChange);
零参方法走 Action 重载,_Gen / _Event / AddEventListener 全部保持不变。
我们认为收益是:
- 泛型参数由生成器写死,handler 签名不匹配直接是编译错误,不再依赖 Analyzer 兜底
- 零运行时开销(纯静态转发,无反射、无装箱)
- 注册入口统一到
GameEvent.Get<IXxx>(),IDE 补全即可发现
- 与现有
AddEventListener 完全共存,可逐个迁移,无破坏性变更
我已经在自己的 fork 里准备实现这个改动。如果官方认可这个方向,我很乐意整理成 PR 提交;如果这条路与框架的设计意图冲突,也想请指教原因,好调整自己的扩展方式,避免和后续官方演进打架。
附:使用中发现的其他几个小问题(不确定是否已知,需要的话我可以拆成独立 issue 或直接提 PR)
GameEventAnalyzer 的 CheckMethodNameList 只覆盖 AddEventListener / AddUIEvent,RemoveEventListener 与 GameEventMgr.AddEvent 未被检查。注册写对、解绑写错时编译器不报错,运行时才 Log.Fatal("Delete handle failed")。
GameEventAnalyzer.cs:123-126,接口方法找不到时静默 return,事件名拼错不会有任何提示。
Analyzer 的 Definition.CommonNamespaces 写死 "GameLogic",接口不在该命名空间时匹配失败。
EventInterfaceGenerator.cs:37-42 只处理 NamespaceDeclarationSyntax,文件作用域命名空间(namespace GameLogic;)会导致 fullName 退化成裸接口名;接口无命名空间时 Aggregate 会抛 InvalidOperationException。另外接口继承(interface IA : IB)与非 void 方法会导致生成的 _Gen 编译失败(CS0535),报错指向生成代码,不易定位。
EventMgr.cs:55 的 RegWrapInterface 用的是 _eventEntryMap.Add,二次调用 GameEventHelper.Init() 会抛 duplicate key;EventDelegateData.AddHandler 对重复注册是 Log.Fatal 而非幂等,热重载场景容易触发。
EEventGroup 目前似乎没有被任何代码消费(只为 [EventInterface] 提供分组入参)。
EventInterfaceGenerator 用的是已废弃的非增量 ISourceGenerator,在较大的热更程序集上每次击键都会全量扫描语法树,IDE 响应会有可感知的开销。
背景
我在项目中大量使用
[EventInterface]接口事件,非常喜欢这个设计:一个接口声明就能同时产出事件 ID 常量、分发实现和类型安全的发送入口,是我们见过最干净的事件方案之一。不过我注意到,发送侧和监听侧的自动化程度是不对称的。
发送侧:完全类型安全
监听侧:退化为「裸 int + 手写泛型」
这里的
<float, float>需要人肉与接口方法签名对齐。框架提供了GameEventAnalyzer(EVENT001/002)来兜底,很感谢这个设计,但它属于事后纠错——实际流程是「写错 → 编译报错 → 跑 CodeFix 修」,而不是「写不错」。想请教的问题
监听侧没有做成生成式的强类型 API,是有意的设计取舍,还是暂时没排上日程?
猜测可能存在以下考量,想确认一下:
GameEvent.Get<T>()的语义是「取发送端包装器」,不希望它同时承担监听职责?GameEvent.Get<IXxx>().之后的输入体验?AddEventListener作为唯一注册入口,便于GameEventMgr/AddUIEvent统一做生命周期管理?建议方案(供参考,不一定是最优解)
在
EventInterfaceGenerator中,除了现有的{Iface}_Event与{Iface}_Gen,再为每个接口生成一个扩展方法类:调用侧:
零参方法走
Action重载,_Gen/_Event/AddEventListener全部保持不变。我们认为收益是:
GameEvent.Get<IXxx>(),IDE 补全即可发现AddEventListener完全共存,可逐个迁移,无破坏性变更我已经在自己的 fork 里准备实现这个改动。如果官方认可这个方向,我很乐意整理成 PR 提交;如果这条路与框架的设计意图冲突,也想请指教原因,好调整自己的扩展方式,避免和后续官方演进打架。
附:使用中发现的其他几个小问题(不确定是否已知,需要的话我可以拆成独立 issue 或直接提 PR)
GameEventAnalyzer的CheckMethodNameList只覆盖AddEventListener/AddUIEvent,RemoveEventListener与GameEventMgr.AddEvent未被检查。注册写对、解绑写错时编译器不报错,运行时才Log.Fatal("Delete handle failed")。GameEventAnalyzer.cs:123-126,接口方法找不到时静默return,事件名拼错不会有任何提示。Analyzer的Definition.CommonNamespaces写死"GameLogic",接口不在该命名空间时匹配失败。EventInterfaceGenerator.cs:37-42只处理NamespaceDeclarationSyntax,文件作用域命名空间(namespace GameLogic;)会导致fullName退化成裸接口名;接口无命名空间时Aggregate会抛InvalidOperationException。另外接口继承(interface IA : IB)与非 void 方法会导致生成的_Gen编译失败(CS0535),报错指向生成代码,不易定位。EventMgr.cs:55的RegWrapInterface用的是_eventEntryMap.Add,二次调用GameEventHelper.Init()会抛 duplicate key;EventDelegateData.AddHandler对重复注册是Log.Fatal而非幂等,热重载场景容易触发。EEventGroup目前似乎没有被任何代码消费(只为[EventInterface]提供分组入参)。EventInterfaceGenerator用的是已废弃的非增量ISourceGenerator,在较大的热更程序集上每次击键都会全量扫描语法树,IDE 响应会有可感知的开销。