物件(poco)深度克隆

語言: CN / TW / HK

提供深度克隆物件功能,基於編譯表示式實現,效能與原生程式碼幾無差別,遠超 json/binary 序列化實現。

1. 簡單示例

class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime Birth { get; set; }
    public double Score { get; set; }
    public DateTime CreateTime { get; set; }
    public DateTime UpdateTime { get; set; }
    public EnumState State { get; set; }
    public string Desc { get; set; }
    public string Phone { get; set; }
}

//克隆
var list = new List<Person>(){/*放點資料*/}
var newList = list.DeepClone();

2. 效能

分別與原生程式碼,json、binary 序列化機制比對;

原生程式碼如:

var newList = list.Select(i => new Person { Id = i.Id/*其他屬性*/}).ToList();
 ```

json序列化如:

var newList = JsonConvert.DeserializeObject<List<Person>>(JsonConvert.SerializeObject(list));

binary序列化如:

BinaryFormatter bf = new BinaryFormatter();
var stream = new MemoryStream();
bf.Serialize(stream, list); stream.Seek(0, SeekOrigin.Begin);bf.Deserialize(stream);

測試效果如下:

測試程式碼,參考:https://gitee.com/jackletter/DotNetCommon/blob/master/tests/DeepClonePerformanceTest/Program.cs

3. 詳細功能

單元測試地址:https://gitee.com/jackletter/DotNetCommon/blob/master/tests/DotNetCommon.Test/Extensions/ObjectTests_DeepClone.cs

3.1 支援的完整資料型別如下:

  • 基礎型別 sbyte/byte/short/ushort/int/uint/long/ulong/float/double/decimal bool/enum/char/string

    DateTime/DateTimeOffset/DateOnly/TimeOnly/TimeSpan/Guid

  • pojo、結構體

  • 陣列、集合、字典 T[]List<T>Dictionary<TKey, TValue>HashSet<T>LinkedList<T>ReadOnlyCollection<T>

    注意:必須是泛型的且指定具體的型別,而不是 List<object>

  • 元組

    Tuple<T1,... ValueTuple<T1....

  • 匿名型別

    new {Id=1.Name="小明",Teacher=new Teacher()}.DeepClone()

  • JObject/JArray/JToken

  • 已實現 ICloneable 介面的型別

3.2 特點

該克隆方法支援引用關係的拷貝,如:

class Node
{
    public int Id { get; set; }
    public Node Parent { get; set; }
    public List<Node> Children { get; set; }
}
//構造
var node=new Node{ Id = 1, Childrem = new List<Node>()};
var subNode=new Node{ Id = 2, Parent = node };
node.Children.Add(subNode);

//深度克隆,不會死迴圈,引用關係會一併拷貝過來
var newNode = node.DeepClone();
Assert.IsTrue(newNode != node);
Assert.IsTrue(newNode.Children[0].Parent == newNode);

之所以能將引用關係也拷貝過來,是因為內部使用了字典進行快取,如果明確例項內部沒有引用關係的話,可以將它關閉,關閉後效能提升將近一倍。

//關閉克隆時的引用關係
node.DeepClone(false);

4. FAQ

4.1 為什麼會支援元組的克隆,元組不是值型別嗎?

元組確實是值型別,但裡面可以存放 物件引用,如:

(int Id, Teacher teacher) tuple = (1,new Teacher{ Name = "小明"});
var newTuple = tuple.DeepClone(false);
newTuple.teacher.Name+="update";

//由於是深拷貝,舊資料並未更改
Assert.IsTrue(tuple.teacher.Name == "小明");

4.2 為什麼會支援匿名型別的克隆,匿名型別不是隻讀的嗎?

這個和元組就相似了,雖然匿名型別是隻讀的,但它裡面可以存放物件引用,如:

var obj = new { Id = 1, teacher = new Teacher{ Name = "小明"}};
var newObj = obj.DeepClone(false);
newObj.teacher.Name+="update";

4.3 為什麼會支援 ReadOnlyCollection 這不是隻讀的嗎?

一方面,雖然 ReadOnlyCollection 本身只讀,但它裡面存的物件例項屬性是可更改,肯定要拷貝;

另一方面,ReadOnlyCollection 只是對外暴露的介面只讀,但沒有說它裡面的資料集一定不能改,如:

var list = new List<int>{ 1, 2 };
var readList = new ReadOnlyCollection<int>(list);
//readList 此時是隻讀的,但仍然可以更改 list
list.Add(3);
//readList 也隨之被更改
Assert.IsTrue(readList.Count == 3);

4.4 為什麼List、Array 都必須是泛型且指定具體的型別?

這是因為,克隆的邏輯是基於編譯表示式實現的,相當於在執行時 生成一個函式,在生成這個函式時會分析 List<T> 中的 T

如果 T 是 Person{ int Id,string Name} 那麼生成的函式就是 old=>new Person(){Id=old.Id,Name=old.Name}。

如果是非泛型的 List 或者是 List<object> 那麼將不能反射到具體的屬性,也就不能生成對應的函式。

4.5 字典 Dictionary<TKey, TValue> 的TKey也會進行克隆嗎?

會。