GenEvent
May 8, 2026 · View on GitHub
GenEvent 是一个高性能事件库,通过源码生成器在编译期生成全部派发代码,无运行时反射,兼容 .NET 与 Unity(netstandard2.0)。
目录
主要特性
- 无运行时反射:所有派发、注册代码在编译期由源码生成器生成
- 零分配热路径:默认发布路径和常见内建 fluent 过滤链在稳态下不分配;自定义
WithFilter(Predicate<object>)、多次自定义 predicate 叠加,或超过当前内联规则容量的内建过滤链,仍可能产生分配 - IL2CPP 友好:不依赖运行时反射,可安全运行在 IL2CPP/AOT 环境
- 优先级:通过
[OnEvent(SubscriberPriority.XXX)]在编译期确定调用顺序,运行时零排序开销 - 取消传播:处理器返回
false配合Cancelable()可中止事件派发 - 灵活的订阅生命周期:
StartListening返回轻量值类型句柄SubscriptionHandle(IDisposable),支持using自动取消,也可手动调用StopListening - 流式发布 API:链式配置
Cancelable、WithFilter、OnlyType等,可按需组合 - 异步支持:处理器可返回
Task/Task<bool>,通过PublishAsync按序 await - 嵌套发布:支持在处理器内部再次发布事件,各层配置相互独立
- 静态处理器:
[OnEvent]可以标注static方法(普通类或static类均可)。静态处理器在GenEventBootstrap.Init()时自动注册,生命周期与程序一致,无需也无法调用StartListening/StopListening
快速开始
安装
通过 .csproj 引用主库和生成器。生成器以 Analyzer 形式接入,仅在编译期生成代码,不参与运行时:
<ItemGroup>
<ProjectReference Include="path\to\GenEvent\GenEvent.csproj" />
<ProjectReference Include="path\to\GenEvent.SourceGenerator\GenEvent.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
Unity 项目
在 Unity 中打开 Window > Package Manager,点击左上角 Add,选择 Add package from git URL...,输入:
https://github.com/Puring103/GenEvent.git?path=src/GenEvent.Unity/Assets/Plugins/GenEvent
自动初始化:当生成器检测到项目引用了 UnityEngine / UnityEditor 时,会自动为 GenEventBootstrap.Init 添加 [RuntimeInitializeOnLoadMethod],程序启动时自动完成所有注册,无需手动调用。如需在特定时机初始化,仍可手动调用 GenEventBootstrap.Init()。
最小示例
using GenEvent;
using GenEvent.Interface;
// 1. 定义事件:struct + IGenEvent<T>
public struct PlayerDeathEvent : IGenEvent<PlayerDeathEvent>
{
public int PlayerId;
}
// 2. 定义订阅者:class + [OnEvent] 标记处理方法
public class GameManager
{
[OnEvent]
public void OnPlayerDeath(PlayerDeathEvent e)
{
Console.WriteLine($"Player {e.PlayerId} died.");
}
}
// 3. 初始化(非 Unity 项目需在首次订阅或发布前调用一次)
GenEventBootstrap.Init();
// 4. 订阅,StartListening 返回 SubscriptionHandle(IDisposable)
var manager = new GameManager();
using var handle = manager.StartListening(); // using 结束时自动取消订阅
// 5. 发布事件
new PlayerDeathEvent { PlayerId = 1 }.Publish();
运行时契约
初始化规则
- 非 Unity 项目中,必须在首次订阅或发布前调用
GenEventBootstrap.Init()。 Init()是幂等的,可以安全地重复调用。- 可通过
GenEventBootstrap.IsInitialized检查当前程序集的 GenEvent 是否已初始化。
if (!GenEventBootstrap.IsInitialized)
{
GenEventBootstrap.Init();
}
如果在初始化前调用 Publish()、PublishAsync()、StartListening() 或 StopListening(),GenEvent 会抛出清晰的 InvalidOperationException,并提示先调用 GenEventBootstrap.Init()。
线程模型
- 当前版本不保证并发线程安全。
- 支持在处理器内部进行嵌套发布。
- 不支持多线程并发发布、并发订阅、并发反订阅,以及多线程同时修改链式发布配置。
- 如果宿主环境可能从多个线程访问 GenEvent,需要由宿主自行保证串行化。
诊断辅助
可使用以下 API 做快速排查:
bool hasPublisher = PublisherHelper.HasPublisher<DamageEvent>();
bool hasSubscriberRegistry = hud.HasSubscriberRegistry();
int count = SubscriberHelper.GetSubscriberCount<DamageEvent, HUDDisplay>();
HasPublisher<TEvent>():确认目标事件发布器是否已注册。HasSubscriberRegistry():确认当前运行时订阅者类型是否存在生成的注册表。GetSubscriberCount<TEvent, TSubscriber>():确认某个事件/订阅者组合当前注册了多少实例。
核心 API
定义事件
事件必须是 struct 并实现 IGenEvent<T>。值类型可避免装箱,并让默认发布路径在稳态下保持零分配。
public struct DamageEvent : IGenEvent<DamageEvent>
{
public int Amount;
public string Source;
}
定义订阅者与处理器
订阅者是普通 class,用 [OnEvent] 特性标记处理方法。根据是否需要参与传播控制,选择对应的返回类型:
| 签名 | 说明 |
|---|---|
void Method(TEvent e) | 只接收事件,不参与传播控制 |
bool Method(TEvent e) | 返回 false 可中止后续订阅者接收(需配合发布时的 Cancelable()) |
async Task Method(TEvent e) | 异步处理,不参与传播控制 |
async Task<bool> Method(TEvent e) | 异步处理,返回 false 可中止传播(需配合发布时的 Cancelable()) |
public class HUDDisplay
{
// void:相当于永远返回true
[OnEvent]
public void OnDamage(DamageEvent e)
{
UpdateHealthBar(e.Amount);
}
}
public class ShieldSystem
{
// bool:可以拦截事件并阻止后续订阅者收到
[OnEvent]
public bool OnDamage(DamageEvent e)
{
if (HasShield)
{
AbsorbDamage(e.Amount);
return false; // 中止传播,后续订阅者(如 HUDDisplay)不会收到
}
return true;
}
}
同一 class 对同一事件最多定义一个同步和一个异步处理器,分别由 Publish 和 PublishAsync 触发。
静态处理器
[OnEvent] 也可以标注 static 方法。静态处理器与实例处理器有三点不同:
- 自动生命周期:在
GenEventBootstrap.Init()时自动注册一次,并在整个程序生命周期内保持激活。无需也无法调用StartListening/StopListening。 - 无需实例:适用于普通类和
static类。 - 调用顺序:在相同优先级内,静态处理器先于实例处理器被调用。
// 静态类 —— 整个类作为全局处理器
public static class GameSystems
{
[OnEvent]
public static bool OnPlayerDeath(PlayerDeathEvent e)
{
// 全局常驻,无需 StartListening
return true;
}
}
// 普通类 —— 静态与实例处理器混用
public class PlayerController
{
// 静态处理器:全局常驻
[OnEvent]
public static void OnReset(ResetEvent e)
{
ResetAllPlayers();
}
// 实例处理器:仅在该实例 StartListening 期间生效
[OnEvent]
public void OnDamage(DamageEvent e)
{
TakeDamage(e.Amount);
}
}
对某个 PlayerController 实例调用 StopListening() 时,OnDamage 被注销,但 OnReset 仍保持激活——它与任何实例无关。
初始化
首次订阅或发布前,调用 GenEventBootstrap.Init() 完成所有 Publisher 与 Subscriber 的注册。若项目有多个程序集,每个程序集需各自调用其生成的 Init()。
// 在程序入口调用一次即可;重复调用安全
GenEventBootstrap.Init();
Unity 项目由生成器自动插入 [RuntimeInitializeOnLoadMethod],无需手动调用。
bool ready = GenEventBootstrap.IsInitialized;
订阅生命周期
StartListening() 将订阅者注册到事件系统,并返回一个轻量值类型句柄 SubscriptionHandle(IDisposable)。持有该句柄并在合适时机 Dispose,即可取消订阅,等价于调用 StopListening()。
推荐:持有句柄,在销毁时 Dispose
public class Enemy : MonoBehaviour
{
private SubscriptionHandle _handle;
void OnEnable() => _handle = this.StartListening();
void OnDisable() => _handle.Dispose(); // 等价于 this.StopListening()
}
或使用 using 限定作用域
using (subscriber.StartListening())
{
new DamageEvent { Amount = 10 }.Publish(); // 正常接收
} // 离开 using 块,自动取消订阅
new DamageEvent { Amount = 5 }.Publish(); // 不再接收
也可以直接调用 StopListening()(忽略句柄)
subscriber.StartListening(); // 忽略返回值,与旧写法完全相同
new DamageEvent { Amount = 10 }.Publish();
subscriber.StopListening(); // 手动取消
SubscriptionHandle.Dispose() 是幂等的,多次调用安全。
共享生命周期代码中的可选订阅
StartListening() 与 StopListening() 是严格 API:当运行时类型没有生成订阅者注册表时会抛出异常。如果你在公共基类、UI 生命周期等位置统一处理订阅,而部分实例并没有 [OnEvent] 处理器,请使用 Try API:
private SubscriptionHandle _handle;
void OnEnable()
{
if (this.TryStartListening(out var handle))
_handle = handle;
}
void OnDisable()
{
_handle.Dispose();
// 或者在不保存句柄时:
this.TryStopListening();
}
当运行时类型没有生成订阅者注册表时,TryStartListening(out handle) 与 TryStopListening() 返回 false。它们不会隐藏实际事件发布路径的初始化错误;用途是表达“该对象可能不是订阅者”的可选生命周期。
仅订阅某一种事件(当订阅者处理多种事件类型时):
// 只注册 DamageEvent,其他事件类型不受影响
using var handle = subscriber.StartListening<MySubscriber, DamageEvent>();
未取消订阅的订阅者会阻止 GC 回收,请在对象销毁时务必取消。
发布事件
同步发布:Publish() 返回 bool,表示事件是否完整派发到所有订阅者(未触发取消传播时始终为 true)。
bool completed = new DamageEvent { Amount = 10 }.Publish();
异步发布:PublishAsync() 按优先级顺序依次 await 每个处理器,返回 Task<bool>(未触发取消传播时始终为 true)。
bool completed = await new DamageEvent { Amount = 10 }.PublishAsync();
同步
Publish()只调用 sync 处理器;PublishAsync()会调用 sync 与 async 处理器。
事件优先级
通过 [OnEvent(SubscriberPriority.XXX)] 指定优先级,调用顺序在编译期由生成器确定,运行时零排序开销。
优先级从高到低:Primary > High > Medium(默认)> Low > End
public class ShieldSystem
{
[OnEvent(SubscriberPriority.High)] // 先于默认 Medium 执行
public bool OnDamage(DamageEvent e)
{
if (HasShield) { AbsorbDamage(e.Amount); return false; }
return true;
}
}
public class HUDDisplay
{
[OnEvent] // 默认 Medium,在 ShieldSystem 之后执行
public void OnDamage(DamageEvent e) => UpdateHealthBar(e.Amount);
}
取消传播
在发布时链式调用 .Cancelable(),此后若某个处理器返回 false,传播立即中止,后续订阅者不再收到本次事件,Publish 返回 false。
不调用 Cancelable() 时,所有处理器的返回值被忽略,事件始终完整派发给所有订阅者。
// 带 Cancelable:ShieldSystem(High)返回 false 时,HUDDisplay(Medium)不会收到
bool handled = new DamageEvent { Amount = 10 }
.Cancelable()
.Publish();
// handled == false 说明传播被中止
// 不带 Cancelable:所有订阅者都会收到,返回值无效
new DamageEvent { Amount = 10 }.Publish();
发布过滤
以下 API 都是作用在“已配置发布值”上的链式配置,不修改订阅注册状态,可自由组合:
| API | 说明 |
|---|---|
evt.Cancelable() | 允许处理器通过返回 false 中止传播 |
evt.WithFilter(Predicate<object> filter) | filter(subscriber) 返回 true 时跳过该订阅者 |
evt.OnlyType<TEvent, TSubscriber>() | 仅 TSubscriber 类型的订阅者收到 |
evt.ExcludeType<TEvent, TSubscriber>() | 排除 TSubscriber 类型的订阅者 |
evt.OnlySubscriber(subscriber) | 仅指定实例收到 |
evt.ExcludeSubscriber(subscriber) | 排除指定实例 |
evt.OnlySubscribers(HashSet<object>) | 仅集合中的实例收到 |
evt.ExcludeSubscribers(HashSet<object>) | 排除集合中的实例 |
链式配置现在会返回一个携带本次发布配置的 ConfiguredEvent<TEvent>。大多数链式写法无需修改;如果要把 fluent 结果存入变量,应使用 var 或 ConfiguredEvent<TEvent>,而不是原始事件类型。
复用同一个 ConfiguredEvent<TEvent> 变量时,会复用其中保存的配置。也就是说,configured.Publish(); configured.Publish(); 会使用同一份过滤 / Cancelable 配置发布两次。
内建 fluent 过滤器,如 OnlySubscriber、ExcludeSubscriber、OnlyType、ExcludeType,在常见的内联规则路径上会保持零分配。WithFilter(Predicate<object>) 继续作为自定义逻辑入口保留;它是否分配取决于调用方传入的 predicate。若传入捕获外部状态的 lambda,例如 obj => obj == target,通常仍会分配。多个自定义 predicate 叠加,或超过当前内联规则容量的内建过滤链,也会退回到 predicate 组合路径并可能产生分配。
// 仅通知 UI 层,不触发游戏逻辑
new DamageEvent { Amount = 5 }
.OnlyType<DamageEvent, HUDDisplay>()
.Publish();
// 排除自身,避免收到自己发出的事件
new DamageEvent { Amount = 5 }
.ExcludeSubscriber(this)
.Publish();
// 链式组合:可取消 + 仅指定类型
new DamageEvent { Amount = 5 }
.Cancelable()
.OnlyType<DamageEvent, ShieldSystem>()
.Publish();
// 排除多个实例
var exclude = new HashSet<object> { enemyA, enemyB };
new DamageEvent { Amount = 5 }.ExcludeSubscribers(exclude).Publish();
// 如果要保存链式配置后的结果,应保存已配置事件,而不是原始事件 struct
var configured = new DamageEvent { Amount = 5 }.ExcludeSubscriber(this);
configured.Publish();
configured.Publish(); // 会再次使用同一份已保存配置发布
兼容性说明
- GenEvent 目前仍处于 1.0 之前版本,底层辅助 / 运行时类型仍可能继续演进。
- 更推荐通过 fluent 发布 API 使用配置,而不是直接构造或长期持有
PublishConfig<TEvent>。 SubscriptionHandle现在是轻量值类型。GenEventFilters会继续保留为兼容性的 predicate 辅助 API,但零分配保证针对的是内建 fluent 发布方法,而不是直接调用这些 predicate helper。
异步支持
将处理方法签名改为返回 Task 或 Task<bool> 即可定义异步处理器,无需额外配置。
public class NetworkSync
{
[OnEvent]
public async Task OnDamage(DamageEvent e)
{
await SendToServerAsync(e);
}
}
// 异步发布:按优先级顺序依次 await 每个处理器
bool completed = await new DamageEvent { Amount = 10 }.PublishAsync();
同一订阅者可以同时定义 sync 和 async 两个处理器处理同一事件,分别由 Publish 和 PublishAsync 触发:
public class CombatLogger
{
[OnEvent]
public void OnDamage(DamageEvent e) // 由 Publish() 触发
{
LogToFile(e);
}
[OnEvent]
public async Task OnDamageAsync(DamageEvent e) // 由 PublishAsync() 触发(注意,同步版本也会被调用)
{
await LogToRemoteAsync(e);
}
}
.NET 基准
仓库内提供了独立的 BenchmarkDotNet 项目 Benchmarks/GenEvent.Benchmarks/,用于做可重复的 .NET 侧性能测量。基准会区分默认 / 内建 fluent 零分配路径与自定义 predicate fallback 路径。
运行完整 benchmark:
dotnet run -c Release --project Benchmarks/GenEvent.Benchmarks/GenEvent.Benchmarks.csproj
只运行单条 benchmark 示例:
dotnet run -c Release --project Benchmarks/GenEvent.Benchmarks/GenEvent.Benchmarks.csproj -- --filter *PublishBenchmarks.Publish_NoSubscribers*
benchmark 结果更适合用于同一台机器、同一套配置下的版本趋势对比;不同机器或不同电源配置下的绝对耗时不宜直接横向比较。
源码生成器约束与诊断
生成器对事件与 [OnEvent] 方法有明确约束,违反时会在编译期报告诊断,不会静默失败。
事件约束
- 必须是
struct,并实现IGenEvent<T>
处理器方法约束
- 必须是
public方法(实例或static均可) - 必须有且仅有一个参数,且类型为实现了
IGenEvent<>的事件类型 - 返回类型只能是
void、bool、Task或Task<bool> - 同一 class 对同一事件类型,最多一个 sync handler 和一个 async handler
诊断码
| 代码 | 严重性 | 含义 |
|---|---|---|
| GE001 | Warning | 未找到 IGenEvent 接口,请检查 GenEvent 引用 |
| GE002 | Warning | 未找到 OnEventAttribute,请检查 GenEvent 引用 |
| GE010 | Error | [OnEvent] 方法必须为 public |
| GE011 | Error | [OnEvent] 方法必须恰好有一个参数 |
| GE012 | Error | [OnEvent] 方法参数必须是 IGenEvent 类型 |
| GE013 | Error | 同一 class 对同一事件类型不能有两个同步或两个异步 handler |
| GE014 | Error | [OnEvent] 方法返回类型必须为 void、bool、Task 或 Task<bool> |
| GE999 | Error | 源码生成器内部异常,请查看编译器输出获取详情 |
出现 GE001/GE002 时请检查主库与生成器的引用是否均已正确添加;GE010–GE014 按上表修正方法签名;GE999 请查看编译器完整输出。
License
本项目采用 MIT License。Copyright (c) 2026 Puring。详见 LICENSE 文件。