应用中我们经常使用到数据的复制,在.NET中有多种方式可以实现复制数据或对象。选择哪种方式、是浅拷贝还是深拷贝,具体需求场景可以取决于对象的复杂性、数据量等,本文我们介绍主要的拷贝方式以及相对高性能的方案。
1. MemberwiseClone拷贝
浅拷贝 Object.MemberwiseClone 方法 (System) | Microsoft Learn,指针对对象执行非静态字段的浅复制操作
- 字段是基础类型如string、int,会全部复制过来,是全新的值
- 字段是引用类型,则会则复制对象的引用,而不复制对象,二者对象是一个内存地址
深拷贝,则不管是字段还是引用类型,均完全实现全新的复现。
一般深拷贝可以手动实现,对象类内部添加Clone方法(也可以实现内置的统一接口ICloneable),将所有字段重新赋值一遍、返回一个新对象。那也可以基于MemberwiseClone方案之上,对引用类型重新赋值一个新对象,实现深拷贝
深拷贝,内部克隆的对象字段可以修改,不会影响原来对象的值。
参考如下代码:
1 public class MemberwiseCloneModel 2 { 3 public int Age { get; set; } 4 public string Name { get; set; } 5 public TestMode Mode { get; set; } 6 public MemberwiseCloneModel ShallowClone() 7 { 8 return (MemberwiseCloneModel)this.MemberwiseClone(); 9 } 10 public MemberwiseCloneModel DeepCopy() 11 { 12 var clone = (MemberwiseCloneModel)this.MemberwiseClone(); 13 clone.Mode = new TestMode() { Data = this.Mode?.Data ?? string.Empty }; 14 return clone; 15 } 16 }
2.Record的with数据拷贝
这是针对Record数据类的一类拷贝方式,只在C#9以上支持,详见Record - C# reference | Microsoft Learn
record因为是标记数据类,可以只有属性,所以RecordModel可以简写为RecordModel1结构:
1 public record class RecordModel 2 { 3 public string Name { get; set; } 4 public int Age { get; set; } 5 public TestMode Mode { get; set; } 6 } 7 public record RecordModel1(string Name, int Age, TestMode Mode);
with相当于MemberwiseClone浅拷贝,对值类型字段可以全新复制,但引用类型操作后还是同一对象 with 表达式 - 创建新对象,这些对象是现有对象的修改副本 - C# reference | Microsoft Learn
写个demo:
1 public static void TestRecordWith() 2 { 3 var original = new RecordModel() { Name = "Test", Age = 20, Mode = new TestMode() { Data = "data" } }; 4 var clone = original with { }; 5 Debug.WriteLine($"referenceEquals:{ReferenceEquals(original, clone)}"); 6 Debug.WriteLine($"clone:{clone.Name},{clone.Age},{clone.Mode.Data}"); 7 clone.Name = "Test1"; 8 clone.Age = 21; 9 clone.Mode.Data = "data1"; 10 Debug.WriteLine($"original after modified clone:{original.Name},{original.Age},{original.Mode.Data}"); 11 }
上面demo输出结果,基础类型不会被修改:
另外,with也可以同时给属性赋新值,var clone = original with { Name = "Test0" };
3. 序列化实现数据拷贝
1 public static T DeepCopy<T>(T obj) 2 { 3 using (MemoryStream memoryStream = new MemoryStream()) 4 { 5 IFormatter formatter = new BinaryFormatter(); 6 formatter.Serialize(memoryStream, obj); 7 memoryStream.Seek(0, SeekOrigin.Begin); 8 return (T)formatter.Deserialize(memoryStream); 9 } 10 }
但BinaryFormatter在.NET5之后标记废弃了,原因是安全漏洞:使用 BinaryFormatter 和相关类型时的反序列化风险 - .NET | Microsoft Learn。官方推荐使用XML以及Json序列化等
2)XML序列化需要添加属性标记DataContract、DataMember(推荐Json序列化也添加此标记)
1 [DataContract] 2 public class SerializerModel 3 { 4 [DataMember] 5 public string Name { get; set; } 6 [DataMember] 7 public int Age { get; set; } 8 [DataMember] 9 public TestMode Mode { get; set; } 10 }
DataContractSerializerDataContractSerializer 类 (System.Runtime.Serialization) | Microsoft Learn实现XML序列化:
1 public static T DeepCopyBySerializer<T>(T obj) 2 { 3 using var stream = new MemoryStream(); 4 var serializer = new DataContractSerializer(typeof(T)); 5 serializer.WriteObject(stream, obj); 6 stream.Position = 0; 7 return (T)serializer.ReadObject(stream); 8 }
XML序列化还有一个XmlSerializer,就不介绍了。
DataContractSerializer使用的是一种流式序列化方式,复杂对象、数据量较大时,DataContractSerializer比 XmlSerializer基于反射的序列化更快。如果是需要可视化可读性强的XML、数据量小、性能要求不高,可以使用XmlSerializer
3)再说说Json序列化
1 public static T DeepCopyByJson<T>(T obj) 2 { 3 var data = System.Text.Json.JsonSerializer.Serialize(obj); 4 return System.Text.Json.JsonSerializer.Deserialize<T>(data); 5 }
性能测试Benchmark
准备同样一个大小数据,Benchmark代码如下:
1 [MemoryDiagnoser] 2 public class BenchmarkTest 3 { 4 private readonly BenchmarkTestMode _data; 5 6 public BenchmarkTest() 7 { 8 _data = GetData(); 9 } 10 [Benchmark] 11 public void ShallowCloneByMemberwiseClone() 12 { 13 var original = _data; 14 for (int i = 0; i < 1000; i++) 15 { 16 var clone = original.ShallowClone(); 17 } 18 } 19 [Benchmark] 20 public void ShallowCloneByRecordWith() 21 { 22 var original = _data; 23 for (int i = 0; i < 1000; i++) 24 { 25 var clone = original with { }; 26 } 27 } 28 [Benchmark] 29 public void DeepCloneByManual() 30 { 31 var original = _data; 32 for (int i = 0; i < 1000; i++) 33 { 34 var benchmarkTestMode = new BenchmarkTestMode() 35 { 36 Angle = original.Angle, 37 Name = original.Name, 38 Points = original.Points.Select(i => new Point(i.X, i.Y)).ToList() 39 }; 40 } 41 } 42 [Benchmark] 43 public void DeepCloneByMemberwiseCloneManual() 44 { 45 var original = _data; 46 for (int i = 0; i < 1000; i++) 47 { 48 var clone = original.DeepClone(); 49 } 50 } 51 [Benchmark] 52 public void DeepCloneByDataContractSerializer() 53 { 54 var original = _data; 55 for (int i = 0; i < 1000; i++) 56 { 57 using var stream = new MemoryStream(); 58 var serializer = new DataContractSerializer(typeof(BenchmarkTestMode)); 59 serializer.WriteObject(stream, original); 60 stream.Position = 0; 61 var clone = (BenchmarkTestMode)serializer.ReadObject(stream); 62 } 63 } 64 [Benchmark] 65 public void DeepCloneBySystemTextJson() 66 { 67 var original = _data; 68 for (int i = 0; i < 1000; i++) 69 { 70 var data = System.Text.Json.JsonSerializer.Serialize(original); 71 var clone = System.Text.Json.JsonSerializer.Deserialize<BenchmarkTestMode>(data); 72 } 73 } 74 75 private BenchmarkTestMode GetData() 76 { 77 var original = new BenchmarkTestMode() { Name = "Test", Angle = 20 }; 78 original.Points = new List<Point>(); 79 for (int i = 0; i < 1000; i++) 80 { 81 original.Points.Add(new Point(i, 1000 - i)); 82 } 83 return original; 84 } 85 }
View Code
然后我们使用release把test跑起来
1 var summary = BenchmarkRunner.Run<BenchmarkTest>(); 2 Console.WriteLine(summary);
1. 浅拷贝,我们对比MemberwiseClone 、Record数据类With
看下面测试结果,Record-with性能强的不是一丁点:
浅拷贝推荐Record数据类With操作,所以我们可以把record使用起来,record不只是简化以及可读性好。如果追求极致性能的话,可以使用record struct结构体
2. 深拷贝,主要有MemberwiseClone结合手动复制、手动复制、XML序列化、JSON序列化
XML/JSON序列化 性能远远小于 MemberwiseClone结合手动复制、手动复制。另外,序列化操作我们可以看到内存总量增加超级多,运行期间会带来一定的内存暴涨问题
所以大量数据场景,深拷贝推荐手动复制(可以结合MemberwiseClone),可以在组件库自定义一套解析、反解析接口,在团队内统一使用。如果只是快速实现功能、性能要求不高,可以使用XML/JSON序列化