去年的时候写了一篇用使用c#强大的表达式树实现对象的深克隆. 最近又看到园子里的另外一篇吐槽automapper性能的文章。正好闲来无事,就想着看如果用Source Generator来实现深克隆,性能上会不会比表达式树更强劲呢,于是有了这篇文章。
之前使用表达式树深克隆的的代码可以实现类型相同/不同之间的克隆(UserEntity->UserDto/UserEntity->UserEntity),支持环状引用(即A的属性引用自身或者A的属性是类型B,类型B中有属性引用A)和可空->不可空转换(public int? id->public int id)。支持枚举转换(public enumXXX type->public int type/public int type->public enumXXX type),在实际生产环境中一直稳定使用,一直没有遇到过问题,但是对于性能上到底能够比手写深拷贝快多少,一直没有闲心去测,这一次正好干脆弄一个Source Generator的版本,再以手写深克隆为基准来实现。
测试环境为windows10、.net版本是9.0.8、引用的BenchmarkDotNet版本是0.15.2。
测试的Dto结构如下:- public class DtoTest
- {
- public int? Id { get; set; }
- public string Name { get; set; }
- public List<ChildDto<DtoTest>> Items { get; set; }
- public List<string> Tags { get; set; }
- public Dictionary<string, int?> Dict { get; set; }
- public TestEnum TestEnum { get; set; }
- public int? TestEnum2 { get; set; }
- public DtoTest This { get; set; }
- }
- public class DtoTest2
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public List<ChildDto<DtoTest2>?>? Items { get; set; }
- public List<string> Tags { get; set; }
- public Dictionary<string, int> Dict { get; set; }
- public int? TestEnum { get; set; }
- public TestEnum TestEnum2 { get; set; }
- public DtoTest2 This { get; set; }
- }
- public class ChildDto<T>
- {
- public string Key { get; set; }
- public int Value { get; set; }
- public T Mother { get; set; }
- }
- public enum TestEnum
- {
- Take = 0,
- Sale = 1,
- Pull = 2
- }
复制代码 可以看到这份代码基本还是覆盖了大部分常见的情况,包含泛型、字典、可空转换、枚举等等。
测试时我的类型实例构造如下:- _src = new DtoTest
- {
- Id = 123,
- Name = "hello world",
- Tags = new List<string> { "a", "b", "c" },
- Items = new List<ChildDto<DtoTest>>
- {
- new ChildDto<DtoTest> { Key = "k1", Value = 42 },
- new ChildDto<DtoTest> { Key = "k2", Value = 100 }
- },
- Dict = new Dictionary<string, int?> { ["x"] = 1, ["y"] = 2 },
- TestEnum = TestEnum.Sale,
- TestEnum2 = null
- };
- _src.This = _src;
- _src.Items.Add(_src.Items[0]);
- foreach (var item in _src.Items)
- {
- item.Mother = _src;
- }
复制代码 增加了循环引用和多次拷贝(items有三个子对象但是有两个指向同一个引用),基本上能够覆盖大部分深拷贝场景了。接下来就是运行后的截图
接下来是增加了一部分测试,确保深拷贝确实是递归到所有属性及其子属性的,而不是简单的浅拷贝(即修改原始对象属性会导致克隆的新对象的属性变化(比如属性的集合内容和属性自身的子属性随着变化)):
测试代码如下:- var _src = new DtoTest
- {
- Id = 123,
- Name = "hello world",
- Tags = new List<string> { "a", "b", "c" },
- Items = new List<ChildDto<DtoTest>>
- {
- new ChildDto<DtoTest> { Key = "k1", Value = 42 },
- new ChildDto<DtoTest> { Key = "k2", Value = 100 }
- },
- Dict = new Dictionary<string, int?> { ["x"] = 1, ["y"] = 2 },
- TestEnum = TestEnum.Sale,
- TestEnum2 = null
- };
- _src.This = _src;
- _src.Items.Add(_src.Items[0]);
- foreach (var item in _src.Items)
- {
- item.Mother = _src;
- }var aaa = DeepClone.DeepCloneHelper.CopyTo(_src);var bbb = InfrastructureBase.Object.ExtensionMapper.Map(_src);var jsonOpts = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };_src.Id = 222;_src.Name = "bbb";_src.Tags[0] = "d";_src.Items.First().Key = "k0";_src.Dict["x"] = -1;Console.WriteLine(JsonSerializer.Serialize(_src, jsonOpts));Console.WriteLine(JsonSerializer.Serialize(aaa, jsonOpts));Console.WriteLine(JsonSerializer.Serialize(bbb, jsonOpts));
复制代码 打印的json结果:
可以看到对src的属性修改并没有影响到SG和EXP生成的新对象。
最后是benchmark的情况如下:
从结果来看,Ratio这一行中以手写深拷贝(new dto2(){xxx= dto1.xxx...})作为基准值1的情况下,Source Generator大概是其2倍成本,而表达式树的版本大概在其5倍+左右的成本。cong Gen0 垃圾回收代以及Allocated每次执行方法分配的内存大小来看三者的成本差异不大,都在同一个区间。Alloc Ratio内存分配比来看SG和EXP的分配版本也落在X1~X2之间,属于基本可以接受的范畴。
具体的技术细节就不聊了,大家有兴趣可以到github上下载对应的代码测试,上面包含了完整的两种深克隆的实现:https://github.com/sd797994/SG_Extension_DeepClone
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |