好長時間沒有寫博文了,今天繼續。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序列化機制——實現更易用的序列化方案》看看能不能實現一個統一的機制。
最後再次呼籲:有誰能告訴我微軟爲何要如此處理值類型的序列化?