← 筆記

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