誰能在同一文件序列化多個對象并隨機讀寫(反序列化)?BinaryFormatter、SoapFormatter、XmlSerializer還是BinaryReader
隨機反序列化器
最近在做一個小型的文件數據庫SharpFileDB,遇到這樣一個問題:我需要找一個能夠在同一文件中序列化多個對象,并且能隨機進行反序列化的工具。隨機反序列化的意思是,假設我在文件里依次序列化存儲了a、b、c三種不同類型的對象,那么我可以通過Stream.Seek(,);或者Stream.Position來僅僅反序列化b。當然,這可能需要一些其它的數據結構輔助我找到Stream.Seek(,);或者Stream.Position所需的參數。
我找到了BinaryFormatter、SoapFormatter、XmlSerializer和BinaryReader這幾個類型,都是.NET Framework內置的。但是它們并非都能勝任單文件數據庫的序列化工具。
舉個例子
假設我有如下兩個類型,本文將一直使用這兩個類型作為數據結構。
1 [Serializable] 2 public class Cat 3 { 4 public override string ToString() 5 { 6 return string.Format("{0}: {1}", this.Id, this.Name); 7 } 8 9 public string Name { get; set; } 10 11 public int Id { get; set; } 12 } 13 14 [Serializable] 15 public class Fish 16 { 17 public override string ToString() 18 { 19 return string.Format("{0}: {1}", this.Id, this.Weight); 20 } 21 22 public float Weight { get; set; } 23 24 25 public int Id { get; set; } 26 }
順序讀寫
如果是按順序進行反序列化,應該是這樣的:
1 SomeFormatter formatter = new SomeFormatter();//某種序列化器 2 3 Cat cat = new Cat() { Id = 1, Name = "湯姆" }; 4 Cat cat2 = new Cat() { Id = 2, Name = "湯姆媳婦" }; 5 Fish fish = new Fish() { Id = 3, Weight = 1.5f }; 6 7 using (FileStream fs = new FileStream("singleFileDB.bin", FileMode.Create, FileAccess.ReadWrite)) 8 { 9 formatter.Serialize(fs, cat); 10 formatter.Serialize(fs, cat2); 11 formatter.Serialize(fs, fish); 12 } 13 14 object obj = null; 15 16 using (FileStream fs = new FileStream("singleFileDB.bin", FileMode.Open, FileAccess.Read)) 17 { 18 obj = formatter.Deserialize(fs);// 1: 湯姆 19 20 obj = formatter.Deserialize(fs);// 2: 湯姆媳婦 21 22 obj = formatter.Deserialize(fs);// 3: 1.5 23 }
隨機讀寫
所謂隨機讀寫,就是把上面的代碼稍微改一下。
1 SomeFormatter formatter = new SomeFormatter (); 2 3 Cat cat = new Cat() { Id = 1, Name = "湯姆" }; 4 Cat cat2 = new Cat() { Id = 2, Name = "湯姆媳婦" }; 5 Fish fish = new Fish() { Id = 3, Weight = 1.5f }; 6 7 using (FileStream fs = new FileStream("singleFileDB.bin", FileMode.Create, FileAccess.ReadWrite)) 8 { 9 formatter.Serialize(fs, cat); 10 formatter.Serialize(fs, cat2); 11 formatter.Serialize(fs, fish); 12 } 13 14 object obj = null; 15 16 using (FileStream fs = new FileStream("singleFileDB.bin", FileMode.Open, FileAccess.Read)) 17 { 18 obj = formatter.Deserialize(fs); // 1: 湯姆 19 20 long position = fs.Position; 21 22 obj = formatter.Deserialize(fs); // 2: 湯姆媳婦 23 24 fs.Position = position;//位置指針再次指向{2: 湯姆媳婦}的起始位置。(實現隨機反序列化) 25 obj = formatter.Deserialize(fs); // 2: 湯姆媳婦 26 27 obj = formatter.Deserialize(fs);// 3: 1.5 28 }
在反序列化時,我們先得到一個{1: 湯姆}對象,此時文件流指針指向了下一個對象的起始位置,我們把這個位置記錄下來。然后再次反序列化,得到了{2: 湯姆媳婦}。現在把文件流的位置指針重新指向剛剛記錄的位置,再次反序列化,仍舊得到了{2: 湯姆媳婦}。
能夠實現這樣的功能的序列化器就是我想要的。
BinaryFormatter
用 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter ,能夠完全勝任。而且他是用二進制格式序列化的,這樣更保密、占用空間更少。不再多說。
SoapFormatter
System.Runtime.Serialization.Formatters.Soap.SoapFormatter 與 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 是近親,他們都實現了 IRemotingFormatter 和 IFormatter 兩個接口。但是 SoapFormatter 的反序列化方式與 BinaryFormatter 不同,導致它不能勝任。
具體來說, SoapFormatter 反序列化一個對象X后,可能會讓 Stream.Position 超過下一個對象的起始位置,甚至一直讀到文件流的最后,無論這個對象X是在文件的開頭還是中間還是末尾。而單文件數據庫的文件是可能很大的,讓 SoapFormatter這么一下子讀到末尾,非常浪費,而且位置指針難以控制,無法用于隨機反序列化。
1 SoapFormatterformatter = new SoapFormatter (); 2 3 Cat cat = new Cat() { Id = 1, Name = "湯姆" }; 4 Cat cat2 = new Cat() { Id = 2, Name = "湯姆媳婦" }; 5 Fish fish = new Fish() { Id = 3, Weight = 1.5f }; 6 7 using (FileStream fs = new FileStream("singleFileDB.soap", FileMode.Create, FileAccess.ReadWrite)) 8 { 9 formatter.Serialize(fs, cat); 10 formatter.Serialize(fs, cat2); 11 formatter.Serialize(fs, fish); 12 } 13 14 object obj = null; 15 16 using (FileStream fs = new FileStream("singleFileDB.soap", FileMode.Open, FileAccess.Read)) 17 { 18 Console.WriteLine(fs.Position == fs.Length);// false 19 20 obj = formatter.Deserialize(fs); // 1: 湯姆 21 Console.WriteLine(fs.Position == fs.Length);// true 22 23 obj = formatter.Deserialize(fs); // 2: 湯姆媳婦 24 Console.WriteLine(fs.Position == fs.Length);// true 25 26 obj = formatter.Deserialize(fs);// 3: 1.5 27 Console.WriteLine(fs.Position == fs.Length);// true 28 }
XmlSerializer
原本我對 System.Xml.Serialization.XmlSerializer 寄予厚望,不過后來發現這家伙最難用。在創建 XmlSerializer 時必須指定能序列化的對象的類型。
1 XmlSerializer formatter = new XmlSerializer(typeof(Cat));
這個formatter只能序列化/反序列化 Cat 類型。需要序列化其它類型,就得創建一個對應的 XmlSerializer 。
最關鍵的, XmlSerializer根本不能在同一文件里保存多個對象。所以就徹底沒戲了。
BinaryReader
我看到LiteDB里用的是 System.IO.BinaryReader 。它能手動控制讀取任意位置的一個個字節,是進行精細化控制的能手。不過這也有不好的一面,就是凡事必須親力親為,代碼量會增長很多,讀寫字節+拼湊語義信息這種程序稍不留神就會出bug,必須用大量的測試進行驗證。
這方面, BinaryFormatter 就更好一點。它能自動反序列化任何對象,不需要你一個字節一個字節地去摳。你只需給對 Stream.Position 即可。而 System.IO.BinaryReader最終也是需要你給定這個位置指針的。
總結
為了隨機讀寫單文件數據庫,能用的.NET Framework內置序列化工具目前只找到了BinaryFormatter和BinaryReader兩個。由于使用BinaryReader需要寫的代碼更多更復雜,我暫定使用BinaryFormatter。
為了更詳細說明用BinaryFormatter實現單文件數據庫序列化/反序列化的思想,我做了如下一個Demo。

1 const string strHowSingleFileDBWorks = "HowSingleFileDBWorks.db"; 2 3 // 首先,創建數據庫文件。 4 using (FileStream fs = new FileStream(strHowSingleFileDBWorks, FileMode.Create, FileAccess.Write)) 5 { } 6 7 // 然后,在App中創建了一些對象。 8 Cat cat = new Cat() { Id = 1, Name = "湯姆" }; 9 Cat cat2 = new Cat() { Id = 2, Name = "湯姆的媳婦" }; 10 Fish fish = new Fish() { Id = 3, Weight = 1.5f }; 11 12 13 // 然后,用某種序列化方式將其寫入數據庫。 14 IFormatter formatter = new BinaryFormatter(); 15 16 // 寫入cat 17 long catLength = 0; 18 using (MemoryStream ms = new MemoryStream()) 19 { 20 byte[] bytes; 21 formatter.Serialize(ms, cat); 22 ms.Position = 0; 23 bytes = new byte[ms.Length]; 24 catLength = ms.Length;// 在實際數據庫中,catLength由文件字節管理器進行讀寫 25 ms.Read(bytes, 0, bytes.Length); 26 using (FileStream fs = new FileStream(strHowSingleFileDBWorks, FileMode.Open, FileAccess.Write)) 27 { 28 fs.Position = 0;// 在實際數據庫中,需要指定對象要存儲到的位置 29 fs.Write(bytes, 0, bytes.Length);//注意,若bytes.Length超過int.MaxValue,這里就需要特殊處理了。 30 } 31 } 32 33 // 寫入cat2 34 long cat2Length = 0; 35 using (MemoryStream ms = new MemoryStream()) 36 { 37 byte[] bytes; 38 formatter.Serialize(ms, cat2); 39 ms.Position = 0; 40 bytes = new byte[ms.Length]; 41 cat2Length = ms.Length;// 在實際數據庫中,cat2Length由文件字節管理器進行讀寫 42 ms.Read(bytes, 0, bytes.Length); 43 using (FileStream fs = new FileStream(strHowSingleFileDBWorks, FileMode.Open, FileAccess.Write)) 44 { 45 fs.Position = catLength;// 在實際數據庫中,需要指定對象要存儲到的位置 46 fs.Write(bytes, 0, bytes.Length);//注意,若bytes.Length超過int.MaxValue,這里就需要特殊處理了。 47 } 48 } 49 50 // 寫入fish 51 long fishLength = 0; 52 using (MemoryStream ms = new MemoryStream()) 53 { 54 byte[] bytes; 55 formatter.Serialize(ms, fish); 56 ms.Position = 0; 57 bytes = new byte[ms.Length]; 58 fishLength = ms.Length;// 在實際數據庫中,fishLength由文件字節管理器進行讀寫 59 ms.Read(bytes, 0, bytes.Length); 60 using (FileStream fs = new FileStream(strHowSingleFileDBWorks, FileMode.Open, FileAccess.Write)) 61 { 62 fs.Position = catLength + cat2Length;// 在實際數據庫中,需要指定對象要存儲到的位置 63 fs.Write(bytes, 0, bytes.Length);//注意,若bytes.Length超過int.MaxValue,這里就需要特殊處理了。 64 } 65 } 66 67 //查詢cat2 68 using (FileStream fs = new FileStream(strHowSingleFileDBWorks, FileMode.Open, FileAccess.Read)) 69 { 70 fs.Position = catLength;// 在實際數據庫中,需要指定對象存儲到的位置 71 object obj = formatter.Deserialize(fs); 72 Console.WriteLine(obj);// {2: 湯姆的媳婦} 73 } 74 75 //刪除cat2 76 // 在實際數據庫中,這由文件字節管理器進行控制,只需標記cat2所在的空間為沒有占用即可。實際操作是修改幾個skip list指針。 77 78 //新增cat3 79 Cat cat3 = new Cat() { Id = 4, Name = "喵" }; 80 long cat3Length = 0; 81 using (MemoryStream ms = new MemoryStream()) 82 { 83 byte[] bytes; 84 formatter.Serialize(ms, cat3); 85 ms.Position = 0; 86 bytes = new byte[ms.Length]; 87 cat3Length = ms.Length;// 在實際數據庫中,cat3Length由文件字節管理器進行讀寫 88 ms.Read(bytes, 0, bytes.Length); 89 using (FileStream fs = new FileStream(strHowSingleFileDBWorks, FileMode.Open, FileAccess.Write)) 90 { 91 fs.Position = catLength;// 在實際數據庫中,需要指定對象要存儲到的位置,這里由文件字節管理器為其找到可插入的空閑空間。 92 fs.Write(bytes, 0, bytes.Length);//注意,若bytes.Length超過int.MaxValue,這里就需要特殊處理了。 93 } 94 } 95 96 //查詢cat cat3 fish 97 using (FileStream fs = new FileStream(strHowSingleFileDBWorks, FileMode.Open, FileAccess.Read)) 98 { 99 object obj = null; 100 // cat 101 fs.Position = 0;// 在實際數據庫中,需要指定對象存儲到的位置 102 obj = formatter.Deserialize(fs); 103 Console.WriteLine(obj);// {1: 湯姆} 104 105 // cat3 106 fs.Position = catLength;// 在實際數據庫中,需要指定對象存儲到的位置 107 108 obj = formatter.Deserialize(fs); 109 Console.WriteLine(obj);// {4: 喵} 110 111 // fish 112 fs.Position = catLength + cat2Length;// 在實際數據庫中,需要指定對象存儲到的位置 113 114 obj = formatter.Deserialize(fs); 115 Console.WriteLine(obj);// {3: 1.5} 116 }
今后通過對此Demo進行不斷擴展,就可以實現一個單文件數據庫了。
由于FileStream.Length是long類型的,所以理論上的單文件數據庫的長度最大為long.MaxValue(9223372036854775807=0x7FFFFFFFFFFFFFFF)個字節,即8589934591GB = 8388607TB = 8191PB = 7EB
文章列表