MyFramework:Unity TypeID 如何替代字符串和反射
项目地址https://github.com/ZHOURUIH/MyFrameworkMyFramework 里有一个很小的类型 ID 工具TypeIDT.ID它的作用是给每个类型分配一个运行时唯一的intID。这个 ID 可以用在事件系统、红点系统、状态系统等需要“按类型索引”的地方。它不依赖字符串也不需要手动维护枚举。一、代码完整实现很短using System.Threading; static public class TypeID { static public int mGlobalCounter 0; // 全局唯一计数器 } // 用来获取Type的对应ID,比GetHashCode要快,线程安全 static public class TypeIDT { public static readonly int ID Interlocked.Increment(ref TypeID.mGlobalCounter); }核心只有一句public static readonly int ID Interlocked.Increment(ref TypeID.mGlobalCounter);每个泛型类型T都会有自己的一份静态字段。所以TypeIDEventLogin.ID TypeIDEventKill.ID TypeIDEventItemChange.ID会分别得到不同的 ID。同一个类型重复访问得到的 ID 不会变化。二、泛型静态字段C# 泛型静态字段有一个特点TypeIDint TypeIDfloat TypeIDstring它们不是共用一份ID。每个封闭泛型类型都有自己的静态字段。所以int id0 TypeIDint.ID; int id1 TypeIDfloat.ID; int id2 TypeIDstring.ID;会触发三个不同类型的静态初始化。每个类型初始化时都会从TypeID.mGlobalCounter中申请一个新的编号。三、线程安全ID 分配使用Interlocked.Increment(ref TypeID.mGlobalCounter)这保证了多个线程同时首次访问不同TypeIDT时不会拿到重复编号。如果写成TypeID.mGlobalCounter在多线程场景下可能出现竞争。这里用Interlocked.Increment成本很低语义也明确全局计数器原子递增 每个类型拿到一个唯一 ID四、替代字符串事件系统里最常见的写法是用字符串作为事件名listenEvent(EventKill, callback); pushEvent(EventKill);这种写法的问题很明显字符串容易写错 重命名不安全 没有类型约束 IDE 很难检查TypeIDT.ID的写法是listenEventEventKill(callback, listener); pushEventEventKill();内部转换成int eventType TypeIDEventKill.ID;调用层写的是类型不是字符串。类型名改了编译器能发现问题。五、替代枚举也可以用枚举做事件 IDpublic enum GameEventType { EventLogin, EventKill, EventItemChange, }然后listenEvent(GameEventType.EventKill, callback);枚举的问题是需要集中维护。每新增一个事件类型都要改枚举。事件类型多了以后这个枚举会越来越大。而TypeIDT.ID不需要集中注册。新增事件类后直接使用TypeIDEventNew.ID第一次访问时自动分配 ID。六、替代 Type Key另一种写法是直接用Type做字典 KeyDictionaryType, ListAction eventMap;注册时eventMap[typeof(EventKill)].Add(callback);这种方式也能工作。但 MyFramework 更倾向于把类型转换成int。事件表可以写成Dictionaryint, SafeList0GameEventRegisteInfo索引时使用TypeIDT.ID这样事件系统内部只处理整数 ID。结构更直接也更符合框架里大量用int做类型索引的风格。七、事件系统中的使用事件分发时使用public void pushEventT(T param) where T : GameEvent { if (mGlobalListenerEventList.TryGetValue(TypeIDT.ID, out var infoList)) { int count infoList.count(); for (int i 0; i count; i) { try { infoList.get(i)?.call(param); } catch (Exception e) { logException(e); } } } }监听时使用public void listenEventT(ActionT callback, IEventListener listener) where T : GameEvent { GameEventRegisteInfo info createEventAddToListenList(0, callback, listener); mGlobalListenerEventList.getOrAddClass(info.mEventTypeID).add(info); }注册信息里保存的是info.mEventTypeID TypeIDT.ID;事件类型最终变成一个整数。分发时直接通过整数查表。八、红点系统中的使用红点系统也会保存事件类型列表protected Listint mEventTypeList new(); // 会触发此红点改变的事件类型子类添加事件时protected void addEventT() where T : GameEvent { mEventTypeList.Add(TypeIDT.ID); }初始化时注册监听foreach (int type in mEventTypeList.safe()) { mEventSystem.listenEvent(type, onEventTrigger, this); }这里的好处是红点内部只保存int。外部配置事件时仍然写类型addEventEventKill(); addEventEventItemChange();类型约束和运行时索引都保留下来了。九、单元测试MyFramework 里也有对应测试。不同类型 ID 不同int idInt TypeIDint.ID; int idFloat TypeIDfloat.ID; int idString TypeIDstring.ID; assertTrue(idInt ! idFloat); assertTrue(idInt ! idString); assertTrue(idFloat ! idString);同一类型 ID 恒定int id1 TypeIDint.ID; int id2 TypeIDint.ID; assertEqual(id1, id2);这个测试覆盖了TypeIDT最核心的行为不同类型不同 ID 同一类型 ID 不变十、适用范围TypeIDT.ID适合运行时内部索引。例如事件类型 状态类型 命令类型 组件类型 红点触发事件 对象池类型索引这些场景只需要在当前运行期间保持唯一。不需要写入配置表。不需要存档。不需要和服务器同步。十一、使用边界TypeIDT.ID不是稳定协议 ID。它的 ID 分配顺序取决于运行时首次访问顺序。所以它不适合这些场景网络协议 ID 配置表持久化 ID 存档数据 ID 跨进程通信 ID 需要版本稳定的 ID这些场景应该使用显式定义的固定 ID。例如协议号、配置表 ID、枚举值或生成代码中的固定常量。TypeIDT.ID更适合框架内部运行时索引。十二、设计取舍优点不用字符串 不用手动维护枚举 访问方式简单 同一类型 ID 恒定 不同类型 ID 唯一 使用 int 做字典 Key 线程安全分配 ID限制ID 只保证运行时唯一 ID 不保证跨运行稳定 首次访问顺序会影响具体数值 不适合作为外部数据 ID这个取舍很明确。它解决的是框架内部类型索引不解决外部数据协议编号。总结TypeIDT.ID的实现只有几行static public class TypeIDT { public static readonly int ID Interlocked.Increment(ref TypeID.mGlobalCounter); }它利用泛型静态字段为每个类型自动分配一个唯一整数。在 MyFramework 中它可以替代字符串事件名也可以避免手动维护庞大的事件枚举。事件系统、红点系统等模块可以直接使用int做索引。调用层仍然使用具体类型listenEventEventKill(callback, listener); pushEventEventKill(); addEventEventKill();这就是TypeIDT.ID的价值。代码很短但适合高频框架基础设施。