Deep cloning objects

Deep cloning objects

技术背景

在编程中,经常会遇到需要复制对象的情况。浅拷贝只复制对象的引用,而深拷贝则会创建一个新的对象,并复制其所有属性和子对象,从而避免对原对象的修改影响到新对象。在 C# 中,实现深克隆有多种方法,每种方法都有其优缺点和适用场景。

实现步骤

1. 使用序列化方法

序列化是一种常见的实现深克隆的方法。其原理是将对象序列化为字节流,然后再反序列化为一个新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

public static class ObjectCopier
{
public static T Clone<T>(T source)
{
if (!typeof(T).IsSerializable)
{
throw new ArgumentException("The type must be serializable.", nameof(source));
}

if (ReferenceEquals(source, null)) return default;

using var stream = new MemoryStream();
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, source);
stream.Seek(0, SeekOrigin.Begin);
return (T)formatter.Deserialize(stream);
}
}

2. 使用 Json 序列化

Json 序列化是另一种实现深克隆的方法,它比二进制序列化更轻量级,并且避免了 [Serializable] 标签的开销。

1
2
3
4
5
6
7
8
9
10
11
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public static T CloneJson<T>(this T source)
{
if (ReferenceEquals(source, null)) return default;

var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };

return JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(source), deserializeSettings);
}

3. 使用 ICloneable 接口

实现 ICloneable 接口是一种手动实现深克隆的方法。需要在类中实现 Clone 方法,并手动复制所有属性和子对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Person : ICloneable
{
private readonly Brain brain;
private int age;

public Person(Brain aBrain, int theAge)
{
brain = aBrain;
age = theAge;
}

protected Person(Person another)
{
Brain refBrain = null;
try
{
refBrain = (Brain)another.brain.clone();
}
catch (CloneNotSupportedException e) { }
brain = refBrain;
age = another.age;
}

public object Clone()
{
return new Person(this);
}
}

4. 使用扩展方法复制属性

可以创建一个扩展方法来复制对象的所有公共属性。

1
2
3
4
5
6
7
8
9
10
11
public static void CopyTo(this object S, object T)
{
foreach (var pS in S.GetType().GetProperties())
{
foreach (var pT in T.GetType().GetProperties())
{
if (pT.Name != pS.Name) continue;
(pT.GetSetMethod()).Invoke(T, new object[] { pS.GetGetMethod().Invoke(S, null) });
}
}
}

5. 使用 CloneExtensions

CloneExtensions 库使用表达式树在运行时生成代码,实现快速、深度克隆。

1
var newInstance = source.GetClone();

6. 使用第三方库

可以使用第三方库如 ValueInjecterAutoMapper 来实现对象的克隆。

1
2
3
4
5
6
7
8
9
// 使用 ValueInjecter
MyObject oldObj;
MyObject newObj = new MyObject();
newObj.InjectFrom(oldObj);

// 使用 AutoMapper
MyType source = new MyType();
Mapper.CreateMap<MyType, MyType>();
MyType target = Mapper.Map<MyType, MyType>(source);

7. 使用 DeepCloner NuGet 包

DeepCloner 是一个快速、简单、有效的 NuGet 包,可用于解决克隆问题。

1
2
var deepClone = new { Id = 1, Name = "222" }.DeepClone();
var shallowClone = new { Id = 1, Name = "222" }.ShallowClone();

8. 使用嵌套 MemberwiseClone 进行深拷贝

嵌套 MemberwiseClone 可以实现非常快速的深拷贝,但需要手动为每个嵌套级别实现 ShallowCopy 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[Serializable]
public class Person
{
public Person(int age, string description)
{
this.Age = age;
this.Purchase.Description = description;
}

[Serializable]
public class PurchaseType
{
public string Description;
public PurchaseType ShallowCopy()
{
return (PurchaseType)this.MemberwiseClone();
}
}

public PurchaseType Purchase = new PurchaseType();
public int Age;

public Person ShallowCopy()
{
return (Person)this.MemberwiseClone();
}

public Person DeepCopy()
{
Person other = (Person)this.MemberwiseClone();
other.Purchase = this.Purchase.ShallowCopy();
return other;
}
}

核心代码

以下是使用序列化方法实现深克隆的核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

public static class ObjectCopier
{
public static T Clone<T>(T source)
{
if (!typeof(T).IsSerializable)
{
throw new ArgumentException("The type must be serializable.", nameof(source));
}

if (ReferenceEquals(source, null)) return default;

using var stream = new MemoryStream();
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, source);
stream.Seek(0, SeekOrigin.Begin);
return (T)formatter.Deserialize(stream);
}
}

最佳实践

  • 选择合适的方法:根据对象的复杂度、性能要求和可维护性选择合适的深克隆方法。
  • 避免过度优化:在没有性能瓶颈的情况下,优先选择简单、易维护的方法。
  • 测试和验证:在使用深克隆方法之前,进行充分的测试和验证,确保克隆的对象符合预期。

常见问题

  • ICloneable 接口的问题ICloneable 接口没有明确说明是浅拷贝还是深拷贝,具体实现取决于开发者,容易引起混淆。
  • 序列化的限制:使用序列化方法需要对象实现 [Serializable] 接口,并且某些类型可能无法序列化。
  • 性能问题:序列化和反射方法通常较慢,在性能敏感的场景中需要考虑其他方法。