.NET对象(poco)深度克隆

提供深度克隆对象功能,基于编译表达式实现,性能与原生代码几无差别,远超 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);

测试效果如下:

.NET对象(poco)深度克隆

测试代码,参考: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也会进行克隆吗?

会。