C#中使用Source Generators实现深度Clone

前言

在开发中,我们经常需要创建某个类型实例的副本。

常用的方式,是继承ICloneable接口,然后自行实现Clone(),这会耗费一定的开发时间;或者使用序列化/反序列化方式变相实现,但是性能不高。

现在,可以尝试用Source Generators实现。.

实现思路

首先,需要Clone的类必须声明一个特定的CloneableAttribute,这样Source Generators才知道为谁实现Clone方法。

然后,Source Generators遍历该类型的所有属性,为其编写属性赋值代码。

如果属性本身也是Cloneable类型,那就调用属性对应类型的Clone方法,实现深度克隆。

具体代码

1.添加CloneableAttribute

向待编译项目加入CloneableAttribute代码:

const string cloneableAttributeText = @"using System;
namespace CloneableDemo
{
    public sealed class CloneableAttribute : Attribute
    {
        public CloneableAttribute()
        {
        }
    }
}
";
context.AddSource("CloneableAttribute", SourceText.From(cloneableAttributeText, Encoding.UTF8));

2.遍历CloneableAttribute声明类

找到声明了CloneableAttribute的所有类型:

var cloneableAttribute = compilation.GetTypeByMetadataName("CloneableDemo.CloneableAttribute");
foreach (var classSymbol in classSymbols)
{
    if (!classSymbol.TryGetAttribute(cloneableAttribute, out var attributes))
        continue;
    context.AddSource($"{classSymbol.Name}_clone.cs", SourceText.From(CreateCloneCode(classSymbol), Encoding.UTF8));
}

3.生成Clone代码

遍历属性,生成Clone方法:

private string CreateCloneableCode(INamedTypeSymbol classSymbol)
{
    string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
    var propertyNames = classSymbol.GetMembers().OfType<IPropertySymbol>();
    var codes = new StringBuilder();
    foreach (var propertyName in propertyNames)
    {
        if (isCloneable(propertyName))
        {
            codes.AppendLine($@"{propertyName} = obj.{propertyName}?.Clone(),");
        }
        else
        {
            codes.AppendLine($@"{propertyName} = obj.{propertyName},");
        }
    }
    return $@"using System.Collections.Generic;

    namespace {namespaceName}
    {{
        public static class {classSymbol.Name}Extentions
        {{
            public static {classSymbol.Name} Clone(this {classSymbol.Name} obj)
            {{
                return new {classSymbol.Name}
                {{
     {codes.ToString()}
                }};
            }}
        }}
    }}";
}

4.使用

现在,就可以在目标项目中使用Clone方法了:

[Cloneable]
public class Class1
{
    public string A { get; set; }
    public Class2 B { get; set; }
}

[Cloneable]
public class Class2
{
    public string A { get; set; }
}

var obj = new Class2()
{
    A = "My IO",
};
var deep = new Class1()
{
    A = "My IO",
    B = obj
};
var clone = deep.Clone();

结论

有了Source Generators,可以让编译器帮我们自动实现Clone方法,既节约了开发时间,又保证了性能!