C#筆記 – 序列化與反序列化

(反)序列化基礎
  • 序列化

    • 將對象(對象圖)轉換成字節流的過程

  • 反序列化

    • 對字節流轉換成對象(對象圖)的過程

  • static void Main(string[] args)
    {
       List<string> objGraph = new List<string> { "Apple", "Orange", "Banana" };
       Stream stream = SerializeToMemory(objGraph);

       stream.Position = 0;
       objGraph = null;

       objGraph = DeserializeFromMemory(stream) as List<string>;
       foreach (var item in objGraph)
      {
           Console.WriteLine(item);
      }
    }

    //Serialize
    static MemoryStream SerializeToMemory(object objectGraph)
    {
       MemoryStream stream = new MemoryStream();
       BinaryFormatter formatter = new BinaryFormatter();
       formatter.Serialize(stream, objectGraph);
       return stream;
    }

    //Deserialize
    static object DeserializeFromMemory(Stream stream)
    {
       BinaryFormatter formatter = new BinaryFormatter();
       return formatter.Deserialize(stream);            
    }
    • 使用格式化器:BinaryFormatter,進行(反)序列化的工作

      • 序列化時,接受一個流引用,和一個序列化對象,把序列化對象序列化後的字節放在流引用中

        • 格式化器利用反射查看對象類型中的實例字段,如果引用了其他對象,這些對象也要進行序列化

          • 格式化器會確保每個對象只序列化一次

      • 反序列化時,接受一個流引用,將流引用裡的字節反序列化後,把一個對象object返回出去

        • 格式化器檢查流的內容,構造流中所有對象的實例,並將這些對象的字段初始化,使它們具有與序列化時相同的值

  • 如果將多個對象圖序列化到一個流中,只要反序列化時的順序正確,就可以把這些對象按序列化時的順序反序列化出來

    • List<int> intTarget_Single = new List<int> { 1, 3, 5, 7, 9 };
      List<string> stringTarget = new List<string> { "Hello", "World", "!" };
      List<int> intTarget_Double = new List<int> { 2, 4, 6, 8, 10 };

      MemoryStream ms = new MemoryStream();
      BinaryFormatter bf = new BinaryFormatter();

      bf.Serialize(ms, intTarget_Single);
      bf.Serialize(ms, stringTarget);
      bf.Serialize(ms, intTarget_Double);

      ms.Position = 0; //序列化後,重置流的下標,才能重新以正確的順序反序列化
      List<int> resultA = new List<int>();
      List<string> resultB = new List<string>();
      List<int> resultC = new List<int>();

      resultA = (List<int>)bf.Deserialize(ms);
      resultB = (List<string>)bf.Deserialize(ms);
      resultC = (List<int>)bf.Deserialize(ms);

      foreach (var a in resultA) { Console.WriteLine(a); }
      foreach (var b in resultB) { Console.WriteLine(b); }
      foreach (var c in resultC) { Console.WriteLine(c); }
  • 序列化對象的時候,類型的全名和定義類型的程序集全名會被寫入流,BinaryFormatter默認輸出程序集的完整標識

    • 反序列化對象時,格式化器獲得該完整標識,調用Assembly.Load將程序集加載到AppDomain中

    • 加載好後,格式化器在程序集中查找與要反序列化對象匹配的類型,找到之後創建類型的實例,並用流中包含的值對其字段進行初始化

(反)序列化相關特性
  • System.Serializable

    • 類型默認是不可序列化的,需要顯式聲明該特性

    • 該特性適用於引用類型、值類型、枚舉類型、委托類型

      • 枚舉和委托總是可序列化的,不需要顯式指定該特性

    • 該特性不會被派生類所繼承

    • [System.Serializable]
      public class SerializeClass { }
  • System.NonSerialized

    • 指出可序列化類型中不應被序列化的字段

    • 可減少需要傳輸的數據,提升性能

    • [Serializable]
      public class SerializeClass
      {
         double radius;
         
        [NonSerialized]
         double area;

         public SerializeClass(double radius)
        {
             this.radius = radius;
             area = Math.PI * this.radius * this.radius;
        }
      }
    • 序列化時,只有radius字段的值會被寫入流;反序列化時,radius的值會被正常初始化為序列化時的值,而area由於沒有序列化,所以值會被初始化為0

  • Serialization.OnSerializing

    • [OnSerializing]
      void OnSerializing(StreamingContext context) { }
    • 序列化前調用

  • Serialization.OnSerialized

    • [OnSerialized]
      void OnSerialized(StreamingContext context) { }
    • 序列化後調用

  • Serialization.OnDeserializing

    • [OnDeserializing]
      void OnDeserializing(StreamingContext context) { }
    • 反序列化前調用

  • Serialization.OnDeserialized

    • [OnDeserialized]
      void OnDeserialized(StreamingContext context) { }
    • 每次反序列化類型的實例後,格式化器會檢查類型中是否定義了應用了該特性的方法,如果是,就調用該方法

      • 該方法調用時,所有可序列化的字段都已經被正確設置

  • OptionalField

    • 防止試圖反序列化不包含新字段的對象時,格式化器拋出異常

(反)序列化過程
  • 序列化一組對象時,格式化器:

    • 首先調用對象中標記了「OnSerializing」特性的所有方法

    • 序列化對象的所有字段

      • 格式化器調用FormatterServices的GetSerializableMembers方法,獲取類型的公有/私有字段(不包含有NonSerialized特性的),返回MemberInfo數組

      • 對象被序列化,MemberInfo數組傳給FormatterServices的GetObjectData方法

        • 該方法返回一個Object數組,每個元素都標識了被序列化的那個對象中的一個字段的值

        • 該Object數組與MemberInfo數組是并行

      • 格式化器將程序集標識和類型全名寫入流中

      • 格式化器遍歷兩個數組,將每個成員名稱和值寫入流中

    • 調用對象中標記了「OnSerialized」特性的所有方法

  • 反序列化時,格式化器:

    • 首先調用對象中標記了「OnDeserializing」特性的所有方法

    • 反序列化對象的所有字段

      • 格式化器從流中讀取程序集標識和完整類名,嘗試將其加載至當前AppDomain,並將程序集標識信息和完整類名傳給FormatterServices的GetTypeFromAssembly方法

        • 該方法返回一個Type對象,代表要反序列化的對象的類型

      • 格式化器調用FormatterServices的GetUninitializedObject方法

        • 該方法為新對象分配內存,但不調用其構造器,所有字段會被初始化為0/null

      • 格式化器調用FomatterServices的GetSerializableMembers方法構造並初始化一個MemberInfo數組

        • 返回序列化好,需要反序列化的一組字段

      • 格式化器根據流中包含的數據創建並初始化一個Object數組

      • 將新分配對象、MemberInfo數組和Object數組的引用傳給FormatterServices的PopulateObjectMembers方法

        • 該方法遍歷數組,將每個字段初始化成對應的值

    • 調用對象中標記了「OnDeserialized」特性的所有方法

  • 在反序列化期間,如果格式化器看到有方法標記了「OnDeserialized」特性,其會將該對象引用添加到一個內部列表中

    • 所有對象都反序列化之後,格式化器反向遍歷列表,調用每個OnDeserialized的方法

流上下文
  • 在某些情況下,一個對象可能需要知道它要在甚麼地方反序列化,可以通過StreamingContext來指定。StreamingContext包含兩個字段

    • StreamingContextStates:一組位標志,判斷(反)序列化的(目的)來源地

    • Object:包含用戶希望的任何上下文信息

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter