文章出處

誰能在同一文件序列化多個對象并隨機讀寫(反序列化)?BinaryFormatter、SoapFormatter、XmlSerializer還是BinaryReader

隨機反序列化器

+BIT祝威+悄悄在此留下版了個權的信息說:

最近在做一個小型的文件數據庫SharpFileDB,遇到這樣一個問題:我需要找一個能夠在同一文件中序列化多個對象,并且能隨機進行反序列化的工具。隨機反序列化的意思是,假設我在文件里依次序列化存儲了a、b、c三種不同類型的對象,那么我可以通過Stream.Seek(,);或者Stream.Position來僅僅反序列化b。當然,這可能需要一些其它的數據結構輔助我找到Stream.Seek(,);或者Stream.Position所需的參數。

我找到了BinaryFormatter、SoapFormatter、XmlSerializer和BinaryReader這幾個類型,都是.NET Framework內置的。但是它們并非都能勝任單文件數據庫的序列化工具。

 

舉個例子

+BIT祝威+悄悄在此留下版了個權的信息說:

假設我有如下兩個類型,本文將一直使用這兩個類型作為數據結構。

 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                 }

隨機讀寫

+BIT祝威+悄悄在此留下版了個權的信息說:

所謂隨機讀寫,就是把上面的代碼稍微改一下。

 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

+BIT祝威+悄悄在此留下版了個權的信息說:

用 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter ,能夠完全勝任。而且他是用二進制格式序列化的,這樣更保密、占用空間更少。不再多說。

 

SoapFormatter

+BIT祝威+悄悄在此留下版了個權的信息說:

 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

+BIT祝威+悄悄在此留下版了個權的信息說:

原本我對 System.Xml.Serialization.XmlSerializer 寄予厚望,不過后來發現這家伙最難用。在創建 XmlSerializer 時必須指定能序列化的對象的類型。

1 XmlSerializer formatter = new XmlSerializer(typeof(Cat));

這個formatter只能序列化/反序列化 Cat 類型。需要序列化其它類型,就得創建一個對應的 XmlSerializer 。

最關鍵的, XmlSerializer根本不能在同一文件里保存多個對象。所以就徹底沒戲了。

 

BinaryReader

+BIT祝威+悄悄在此留下版了個權的信息說:

我看到LiteDB里用的是 System.IO.BinaryReader 。它能手動控制讀取任意位置的一個個字節,是進行精細化控制的能手。不過這也有不好的一面,就是凡事必須親力親為,代碼量會增長很多,讀寫字節+拼湊語義信息這種程序稍不留神就會出bug,必須用大量的測試進行驗證。

這方面, BinaryFormatter 就更好一點。它能自動反序列化任何對象,不需要你一個字節一個字節地去摳。你只需給對 Stream.Position 即可。而 System.IO.BinaryReader最終也是需要你給定這個位置指針的。 

總結

+BIT祝威+悄悄在此留下版了個權的信息說:

為了隨機讀寫單文件數據庫,能用的.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


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()