2018年5月15日 星期二

[C#] 深層複製(Deep Clone)功能實作及應用

前言

有時候在操作物件時,某些物件只是做為 Template 使用,我們並不希望因為後續的操作而造成 Template 異動,因此會回傳複製品供取用者來使用;一方面確保 Template 物件絕不被異動,而另一方面則是避免取用者直接取用Tempalte物件時,不經意地透過相同物件參考位置而不小心同步變更資料源。

環境

  • .Net Framework 4.5
  • Json.Net 7.0.1

深層複製

稍微研究了一下深層複製方式後,發現約略有以下兩種方式;以下代碼皆以擴充方法(Extensions)進行實現,可以方便地讓所有物件都享有此功能。

使用BinaryFormatter複製

方式僅針對可序列化物件進行複製,也就是目標物件須標記 Serializable 標籤,否則是無法透過此方式執行 Deep Clone;因此先檢核目標物件是否為Serializable,若否將直接拋出錯誤,停止後續工作的進行。
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public static class CommonExtensions
{

    /// <summary>
    /// 深層複製(複製對象須可序列化)
    /// </summary>
    /// <typeparam name="T">複製對象類別</typeparam>
    /// <param name="source">複製對象</param>
    /// <returns>複製品</returns>
    public static T DeepClone<T>(this T source)
    {

        if (!typeof(T).IsSerializable)
        { 
            throw new ArgumentException("The type must be serializable.", "source"); 
        }

        if (source != null)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(stream, source);
                stream.Seek(0, SeekOrigin.Begin);
                T clonedSource = (T)formatter.Deserialize(stream);
                return clonedSource;
            }
        }
        else
        { return default(T); }

    }
  
}

使用Json.Net複製

此方式是將物件序列化為Json格式後,再透過反序列化來反射出所需實體,因此複製對象若有循環參考(Self Reference)物件的情況存在時(ex. Entity Framework 的 navigation properties ),會造成序列化無限循環的錯誤發生;而針對此問題可以透過 PreserveReferencesHandling = PreserveReferencesHandling.Objects 設定讓Json.Net在序列化及反序列化時,將物件參考列入其中來避免無限循環參考問題發生。
using Newtonsoft.Json;

public static class CommonExtensions
{

    /// <summary>
    /// 深層複製(需使用Json.Net組件)
    /// </summary>
    /// <typeparam name="T">複製對象類別</typeparam>
    /// <param name="source">複製對象</param>
    /// <returns>複製品</returns>
    public static T DeepCloneViaJson<T>(this T source)
    {

        if (source != null)
        {
            // avoid self reference loop issue
            // track object references when serializing and deserializing JSON
            var jsonSerializerSettings = new JsonSerializerSettings
            {
                PreserveReferencesHandling = PreserveReferencesHandling.Objects,
                TypeNameHandling = TypeNameHandling.Auto
            };

            var serializedObj = JsonConvert.SerializeObject(source, Formatting.Indented, jsonSerializerSettings);
            return JsonConvert.DeserializeObject<T>(serializedObj, jsonSerializerSettings);
        }
        else
        { return default(T); }
     
    }
  
}

實例演練

假設有個 Utility 類別來提供遠端資料快取,由於測試方便所以在此就直接使用 static field 作為快取存放容器,其中 NotifySelections 存放的就是一個下拉式選單的項目清單,只設定Getter來避免取用端直接重新賦予新值,我們將以此作為下拉式選單Template資料源。
public class Utility
{

    // Fields
    private static List<SelectItem> _notifySelections;


    // Properties
    public static List<SelectItem> NotifySelections
    {
        get
        {
            if (_notifySelections == null)
            {
                _notifySelections = new List<SelectItem>
       {
        new SelectItem() { Name="Mail User", Value=1},
        new SelectItem() { Name="Call User", Value=2}
       };
            }

            return _notifySelections;
        }
    }

}

[Serializable]
public class SelectItem
{
    public string Name { get; set; }
    public int Value { get; set; }

    public override string ToString()
    {
        return string.Format("name:{0} weight:{1}", Name, Value);
    }
}

接著直接取用該資料源,並依照實際登入用戶來取代其中User文字 (ex. Mail User 改為 Mail Chirs),讓畫面上選單可以更貼近使用者感受;代碼如下圖所示,筆者會在取用Template資料物件且變更資料(2)前後,直接印出 Template 資料源物件資料(1)(3),我們可以來比較一下結果是否如預期。
class Program
{

    static void Main(string[] args)
    {
        // show cached NotifySelections
        Console.WriteLine("== Utility.NotifySelections ==");
        Console.WriteLine(GetContent(Utility.NotifySelections));
        

        // get selections as template
        var selections = Utility.NotifySelections;

        // replace some info
        foreach (var select in selections)
        { select.Name = select.Name.Replace("User", "Chris"); }

        // use it as drop down list items
        Console.WriteLine("== selections ==");
        Console.WriteLine(GetContent(selections));


        // show cached NotifySelections
        Console.WriteLine("== Utility.NotifySelections ==");
        Console.WriteLine(GetContent(Utility.NotifySelections));

    }

    // show items info
    static string GetContent<T>(List<T> items)
    {
        StringBuilder sb = new StringBuilder();
        foreach (var item in items)
        {  sb.AppendLine(item.ToString()); }

        return sb.ToString();
    }

}

結果出爐
  1. 正確取出 Template 選單資訊
  2. 正確取得並修改為所需之選單文字資訊
  3. 再次取出 Template 選單資訊時,已經被異動了!!!
來回顧一下,由於我們傳出的是物件參考,因此取用端取得的資料就是相同記憶體位置的同一份資料,因此所有的異動都會影響到作為 Template 物件的 _notifySelections 資料;這時就需要將 _notifySelections 進行深層複製,然後把複製品傳出,最終取用端要如何操作該物件都將不再影響資料源。
調整後將以下列方式進行深層複製且回傳取用端
_notifySelections.DeepClone() 或 _notifySelections.DeepCloneViaJson()
最後看一下結果
  1. 正確取出 Template 選單資訊
  2. 正確取得並修改為所需之選單文字資訊
  3. 正確取出 Template 選單資訊,未再被異動了。

參考資訊


from : https://dotblogs.com.tw/wasichris/2015/12/03/152540

沒有留言:

張貼留言