使用c#强大的SourceGenerator现对象的深克隆

去年的时候写了一篇用使用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结构如下:

csharp 复制代码
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
}

可以看到这份代码基本还是覆盖了大部分常见的情况,包含泛型、字典、可空转换、枚举等等。

测试时我的类型实例构造如下:

ini 复制代码
_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有三个子对象但是有两个指向同一个引用),基本上能够覆盖大部分深拷贝场景了。接下来就是运行后的截图

接下来是增加了一部分测试,确保深拷贝确实是递归到所有属性及其子属性的,而不是简单的浅拷贝(即修改原始对象属性会导致克隆的新对象的属性变化(比如属性的集合内容和属性自身的子属性随着变化)):

测试代码如下:

ini 复制代码
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<DtoTest, DtoTest2>(_src);
var bbb = InfrastructureBase.Object.ExtensionMapper<DtoTest, DtoTest2>.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));

www.hefeilaws.com/

打印的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上下载对应的代码测试,上面包含了完整的两种深克隆的实现:github.com/sd797994/SG...

相关推荐
小码编匠4 小时前
手把手教会设计 WinForm 高DPI兼容程序,告别字体模糊与控件乱飞(.NET 4.6.1/.NET 6.0)
后端·c#·.net
钩鸿踏月5 小时前
复盘一个诡异的Bug之FileNotFoundException
c#·bug·.net
INSO6 小时前
查漏补缺之Autofac
c#
INSO6 小时前
查漏补缺之Autofac生命周期
c#
小乖兽技术6 小时前
C#与C++交互开发系列(三十):C#非托管内存分配大比拼,哪种方式才是真正的性能王者?
c++·c#·交互
CodeCraft Studio7 小时前
PPT处理控件Aspose.Slides教程:使用 C# 编程将 PPTX 转换为 XML
xml·c#·powerpoint·aspose·ppt转xml·ppt文档开发
好望角雾眠8 小时前
第二阶段WinForm-11:自定义控件
笔记·c#·#笔记·#自定义控件
wuk9988 小时前
C#开发OPC UA客户端
开发语言·c#
CodeCraft Studio15 小时前
PPT处理控件Aspose.Slides教程:在 C# 中将 PPTX 转换为 Markdown
开发语言·c#·powerpoint·markdown·ppt·aspose·ai大模型