好长时间没有写博文了,今天继续。html
此次跟你们分享的内容原由于对一个枚举值列表的序列化,下面简化后的代码即能重现。为了明确起见,我显式指定了枚举的基础类型。框架
// 定义一个枚举类型。 public enum SomeEnum :int { First, Second, Third, ... ... } // 重现问题的代码。 var list = new List<SomeEnum>(); for (int i = 0; i < 1000; ++i) { list.Add((SomeEnum)(i % 3)); } var formatter = new BinaryFormatter(); var stream = File.OpenWrite("c:\\a.data"); formatter.Serialize(stream, list); stream.Close()
你预料生成的a.data文件大约有多大?函数
获得4K结果的同窗,我想是这样估计的,SomeEnum枚举用int表示,每一个值占用4字节,1000个大约就是4K左右,加上其它一些序列化信息,可能就4K多一些吧。最初我也是这么想的,直到在软件中这样的列表占用了几十兆的内存时,问题才暴露出来。我想我仍是比较天真,觉得那么简洁的类型应该有相应简洁的序列化方式,我甚至天真到历来没有意识到这是个问题。post
我用Reflector跟踪了具体的持久化过程,才发现原来在.NET framework内部,对枚举值并无像基本类型那样进行处理,而是直接当成普通的值对象处理的。更糟糕的是,对于值对象的处理,竟然也要像引用对象那样保存objectId和mapId。我用了“竟然”这个词,由于我真的认为值对象(ValueType)就只是数据,不会存在两个reference引用同一个值对象的状况(我知道这样说有些奇怪,但但愿你能明白个人意思)——直到如今我也这么认为。this
下面是 formatter.Serialize(stream, list) 这句代码执行过程当中某一时刻的堆栈状态,为了不大量的折行影响你的心情,我只保留了函数名部分。spa
mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryObject.Write(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.__BinaryWriter.WriteObject(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArrayMember(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.WriteArray(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Write(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(...) mscorlib.dll!System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(System.IO.Stream serializationStream, object graph)
在栈顶上是.NET framework二进制序列化中BinaryObject.Write方法,其实现以下:code
public void Write(__BinaryWriter sout) { sout.WriteByte(1); sout.WriteInt32(this.objectId); sout.WriteInt32(this.mapId); }
也就是说每写一个枚举值,系统都会先写入1 + 4 + 4 = 9个字节的额外数据!这样算起来,开始处代码产生的文件就大约是 1K * (9 + 4) = 13K !orm
这几天我一直在想:为何对值对象也要写入objectId和mapId呢?根据框架的代码的实际输出来看,系统不会“对值相等的多个值对象只保存一份数据”,那么为何还要写入这些额外的数据呢?对此我仍不得其解,若是有人知道,还请不吝赐教。htm
为了解决这个问题,我在类型内部使用了List<int>来保存数据,而在对外接口中完成int和SomeEnum的转换,这样作一来不会影响其它模块的代码,二来也能够将此处理进行屏蔽。对象
基于一样的缘由,对于以下一个值类型来讲,要直接使用.NET提供的序列化机制,则每保存一个对象,将额外消耗一倍多的空间。是的,对于引用类型来讲也是同样,但仍是那句话——我只是没有意识到这个问题,或者说如今还不能接受framework那么粗糙的实现!
[Serializable] public struct Point { private float x, y; }
为了不这样的问题,最直接的方法是在包含此类成员的类型上实现ISerializable接口,而后存储转换到基本类型的数据。若是类中要序列化的成员比较多的话,这样作可能会致使其它成员也要手工处理。若是感兴趣,也能够参考个人另外一篇博文《深刻挖掘.NET序列化机制——实现更易用的序列化方案》看看能不能实现一个统一的机制。
最后再次呼吁:有谁能告诉我微软为何要如此处理值类型的序列化?