MyFramework:Unity TypeID 如何替代字符串和反射

项目地址:

github.com/ZHOURUIH/My...

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 的价值。

代码很短,但适合高频框架基础设施。

相关推荐
SmalBox5 小时前
【节点】[Spiral节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_9 小时前
MyFramework:Unity SafeList0 的延迟压缩设计
unity3d
_zhourui_h_1 天前
MyFramework:Unity SafeList 如何支持遍历中修改
unity3d
SmalBox1 天前
【节点】[SmoothWave节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[RoundedRectangle节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_2 天前
MyFramework:AssetBundle 延迟卸载与依赖保护
unity3d
_zhourui_h_3 天前
MyFramework:safe() 扩展函数的空集合设计
unity3d·游戏开发
SmalBox3 天前
【节点】[RoundedPolygon节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox4 天前
【节点】[Rectangle节点]原理解析与实际应用
unity3d·游戏开发·图形学