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