不邀自来,起码两种办法,能混合用,更多可以再想。
一、人家 Stack Overflow 的答案很好了,你还想干嘛?用 StructLayout 不香吗?
二、自定义 implicit cast,共享一个通用值,所有的值都能换成它(implicit),它也能换成所有要支持的值(explicit)。装箱成本其实不高,顶多顶多 40 次整数加法而已,算上垃圾回收,最多最多等同 100-200 次加法,因此如果你的转换代码太长,结果会不划算。值倘使超过 32 字节,大于 CPU 缓存线,一般不如装箱,毕竟缓存掉页一次也就可能 100-200 次加法了。微软官方 2002 年建议,凡大于 16 字节的数据,不适合用值,而应该用类,即强迫装箱,不过现代 CPU 都 64 位了,缓存线也至少 32 字节,我个人接受以 32 字节为上限。
想要单凭技术升迁加薪,专攻性能是最快的,而性能优化最显著的,是 I/O 优化,通常可以短时间内提升十到百倍速度。听其他答主的话,别再问一堆有的没的无聊问题了。
你大概没懂我的意思。我的意思是,用 struct CommonMsg : IMsg,然后定义 WalkMsg、RunMsg、AttackMsg 跟 CommonMsg 的双向 implicit/explicit cast,很方便。请注意把双向 implicit/explicit cast 的定义摆在 WalkMsg、RunMsg、AttackMsg,这样 CommonMsg 在最底层的 assembly,而其它值可以放其它 assemblies。为了 CommonMsg 最通用化,它可以排两个或四个长整数,挤下任何不过大的值。配合 StructLayout,你可以优化 implicit/explicit cast 的代码,两三步完成转换。
抱歉,昨晚半夜两点写的,昏昏沉沉,犯了个严重错误。拿出容器时,你需要知道它哪种值,怎办?很简单,CommonMsg 存个 RuntimeTypeHandle 就行了,explicit cast 时可以报错。微软 C# 看到 value.GetType().TypeHandle 会跳过 GetType() 而不装箱,RuntimeTypeHandle 只是个长整数(64 位 CPU),尚可接受。你如果觉得长整数太大,也能用小一点的整数,自己加个 _typeCount++ 去算。
那你要不想写 switch 或一大串 if-else 呢?也很简单,用 Dictionary<RuntimeTypeHandle, Action> 就可以多态了。字典有很多好处,你能动态维护多态,也能 double dispatch、multiple dispatch、dynamic dispatch 或任何 dispatch,超级非常灵活。况且,Action 是 Delegate,因此能 JIT inline,会接受 MethodImplOptions 的暗示,包括其中你喜欢的 AggressiveInlining 跟 AggressiveOptimization,比虚函数快!微软曾经承诺,将考虑 inline 虚函数,超越 Java 在这方面的成就,但我认为大可不必相信渣男。起码在 .Net 8 之前,微软不太可能 inline 虚函数,所以你想 inline 多态函数,那只能靠 Dictionary<RuntimeTypeHandle, Action> 了。
微软在承诺方面是个渣男,但在实际表现方面却不,Java 至今无法支持类似 Unity 的游戏引擎,说明 .Net 是至少比 Java 快四五倍的(if not 十倍)。目前除了虚函数 inlining、类本身的垃圾回收,.Net 在所有方面性能是超越 Java 的。缺乏类本身的垃圾回收是 .Net 最大败笔,因为 Java 可以支持上亿个类,而 .Net 由于无法回收类的 metadata,会挤爆内存,除非你想经常上下载 AppDomain,慢死程序。
所以,想要快,选 .Net;而想要大,选 Java。两者各有千秋。
严格来说,不能写出一个不装箱的通用容器,写出别的通用容器那也跟object差不多,毕竟object就是为了通用而生的
但是,在你这个应用场景,还是能有尽量不产生额外GC压力的方法的,首先各种msg作为结构体都有其相同不变的字段,这些字段完全可作为原始msg的字段,然后用一个索引content去运行时组装成新的msg,而不是编译时扩展,如下
struct MSG { All allHaveFields; //用结构包装 type of index(integer or pointer or reference) content; } type index { enum_HowToUseThisMSG filed1; AdditinalMSG TheAdditinalMSG {get;}; } class or struct with interface WALKMSG //需要被装箱的部分 { private ownFields; public void Main(All all){}; //所有扩充MSG都需要实现 } static class HandleMSG { type Handle(MSG msg) { swtich (msg.content.field1) { case walk: msg.content.TheAdditinalMSG.Main(msg.all);break; } } }
这个只是个大概的数据行为模型,但是核心思想就是用组装代替扩展和利用索引进行再映射
另外,完全不装箱,也是可以的,那就是采用指针利用byte数组手动使用指针存取,写成非托管类型,但是这种内存模型就更难分析,在实现上,难以扩展,必须手动维护前后一致。
每个结构体布局都可能不一样啊...一个24bit一个32bit存不到一起去...非要一个容器统一处理的似乎只能开个byte数组,手动记录每个实例的元信息,比如偏移量,大小,类型,存取的时候强转(写着写着变成C了