深層複製(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();
}
}
結果出爐
- 正確取出 Template 選單資訊
- 正確取得並修改為所需之選單文字資訊
- 再次取出 Template 選單資訊時,已經被異動了!!!
來回顧一下,由於我們傳出的是物件參考,因此取用端取得的資料就是相同記憶體位置的同一份資料,因此所有的異動都會影響到作為 Template 物件的 _notifySelections 資料;這時就需要將 _notifySelections 進行深層複製,然後把複製品傳出,最終取用端要如何操作該物件都將不再影響資料源。
調整後將以下列方式進行深層複製且回傳取用端
_notifySelections.DeepClone()
或 _notifySelections.DeepCloneViaJson()
最後看一下結果
- 正確取出 Template 選單資訊
- 正確取得並修改為所需之選單文字資訊
- 正確取出 Template 選單資訊,未再被異動了。
參考資訊
http://stackoverflow.com/questions/78536/deep-cloning-objects
http://www.newtonsoft.com/json/help/html/preserveobjectreferences.htm
https://www.dotblogs.com.tw/yc421206/archive/2012/05/25/72390.aspx
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !