.NET 数据拷贝方案选择-大模型牛翰社区-人工智能-牛翰网

.NET 数据拷贝方案选择

 应用中我们经常使用到数据的复制,在.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. 序列化实现数据拷贝

可以通过将对象序列化为二进制、XML 或 JSON 等格式,然后再反序列化为新对象来实现深拷贝。此方法对内部引用对象字段,也适用 1)二进制格式实现比例简单,直接粘贴代码,如下:

 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序列化

已知最强的2个Json序列化器:微软的System.Text.Json和第三方成熟Newtonsoft.Json 如果是.NET版本推荐System.Text.Json,Framework版本使用Newtonsoft.Json。之前有统计过俩个方案的性能 .NET Json序列化方案选择 – 唐宋元明清2188 – 博客园 后面关注.NET8+,所以看System.Text.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]-.NET 数据拷贝方案选择-大模型牛翰社区-人工智能-牛翰网

  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    }
 86    [DataContract]
 87    public record class BenchmarkTestMode
 88    {
 89        [DataMember]
 90        public string Name { get; set; }
 91        [DataMember]
 92        public int Angle { get; set; }
 93        [DataMember]
 94        public List<Point> Points { get; set; }
 95        public BenchmarkTestMode ShallowClone()
 96        {
 97            return (BenchmarkTestMode)this.MemberwiseClone();
 98        }
 99        public BenchmarkTestMode DeepClone()
100        {
101            var clone = (BenchmarkTestMode)this.MemberwiseClone();
102            clone.Points = Points.Select(i => new Point(i.X, i.Y)).ToList();
103            return clone;
104        }
105    }

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序列化,其它的拷贝第三方库如AutoMapper、DeepCloner、FastDeepCloner

补充下第三方库的使用:

 1         [Benchmark]
 2         public void DeepCopyByAutoMapper()
 3         {
 4             var original = _data;
 5             for (int i = 0; i < 1000; i++)
 6             {
 7                 var clone = DeepCopyByAutoMapper(original);
 8             }
 9         }
10         [Benchmark]
11         public void DeepCopyByDeepCloner()
12         {
13             var original = _data;
14             for (int i = 0; i < 1000; i++)
15             {
16                 var clone = original.DeepClone();
17             }
18         }
19         private T DeepCopyByAutoMapper<T>(T original)
20         {
21             var config = new MapperConfiguration(cfg =>
22             {
23                 cfg.CreateMap<T, T>();
24             });
25             var mapper = config.CreateMapper();
26             T clone = mapper.Map<T, T>(original);
27             return clone;
28         }

执行Benchmark测试,结果如下:

XML/JSON序列化 性能远远小于 MemberwiseClone结合手动复制、手动复制。另外,序列化操作我们可以看到内存总量增加超级多,运行期间会带来一定的内存暴涨问题

所以大量数据场景,深拷贝推荐手动复制(可以结合MemberwiseClone),可以在组件库自定义一套解析、反解析接口,在团队内统一使用。

需要考虑一定性能、不想重新造轮子,可以使用第三方库DeepCloner

如果只是快速实现功能、性能要求不高,可以使用XML/JSON序列化

来源链接:https://www.cnblogs.com/kybs0/p/18669035

请登录后发表评论

    没有回复内容