使用c#强大的表达式树实现对象的深克隆之解决循环引用的问题

在上一期博客里,我们提到使用使用c#强大的表达式树实现对象的深克隆,文章地址:https://www.cnblogs.com/gmmy/p/18186750。但是文章里没有解决如何实现循环引用的问题。

循环引用

在C#中,循环引用通常发生在两个或更多的对象相互持有对方的引用,从而形成一个闭环。这种情况在使用面向对象编程时比较常见,尤其是在处理复杂的数据结构如图或树时。当我们使用表达式树进行对象创建时,如果遇到循环引用,很有可能导致表达式树无限递归直至超出最大递归限制而引发溢出。

以之前的代码为例,这次我们引入一个循环引用的案例,其中类型定义如下:

复制代码
public class TestDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Dictionary<string,int> Record { get; set; }
    public double[] Scores { get; set; }
    public ChildTestDto Child { get; set; }
}
public class ChildTestDto
{
    public string Name { get; set; }
    public TestDto Father { get; set; }
}

我们可以观察到当ChildTestDto的Father被指向TestDto时,一个环状结构就出现了。当我们使用序列化和反序列化时,很容易导致框架抛出异常或者忽略引用(根据框架特性和配置来决定框架的行为)。那么在表达式树中要解决这个问题要如何处理呢?核心其实就是当我们遇到属性指向一个类型时,我们需要检测这个类型是否被创建过了,如果没有被创建,我们new一个。如果已经被创建,则我们可以直接返回被创建的对象。这里的核心关键是,当我们new的对象,我们需要引入【延迟】策略来进行赋值,否则创建一个新对象没有拷贝原始对象的属性,也不符合我们的要求。

那么接下来就是如何实现【延迟】策略了,首先我们需要改造我们的DeepClone函数,因为DeepClone是外部调用的入口,而为了【检测】对象,我们需要维护一个字典,所以只有在内部实现新的深克隆函数通过传递字典进行递归调用来实现检测。

首先是重新定义一个新的线程安全字典集合用于存储【延迟赋值】的表达式树

复制代码
public static class DeepCloneExtension
{
    //创建一个线程安全的缓存字典,复用表达式树
    private static readonly ConcurrentDictionary<Type, Delegate> cloneDelegateCache = new ConcurrentDictionary<Type, Delegate>();
    //创建一个线程安全的缓存字典,复用字典延迟赋值表达式树
    private static readonly ConcurrentDictionary<Type, Delegate> dictCopyDelegateCache = new ConcurrentDictionary<Type, Delegate>();
    //定义所有可处理的类型,通过策略模式实现了可扩展
    private static readonly List<ICloneHandler> handlers = new List<ICloneHandler>
    ....
}

接着我们需要从DeepClone扩展一个新的可以接受字典参数的内部克隆函数,定义如下:

复制代码
public static T DeepClone<T>(this T original)
{
    if (original == null)
        return default;
    Dictionary<object, object> dict = new Dictionary<object, object>();
    T target = original.DeepCloneWithTracking(dict);
    return target;
}
public static T DeepCloneWithTracking<T>(this T original, Dictionary<object, object> dict)
{
    T clonedObject = Activator.CreateInstance<T>();
    var testfunc = CreateDeepCopyAction<T>();
    if (original == null)
        return default;
    if (dict.ContainsKey(original))
    {
        return (T)dict[original];
    }
    dict.Add(original, clonedObject);
    var cloneFunc = (Func<T, Dictionary<object, object>, T>)cloneDelegateCache.GetOrAdd(typeof(T), t => CreateCloneExpression<T>().Compile());
    var obj = cloneFunc(original, dict);
    var dictCopyFunc = (Action<T, T>)dictCopyDelegateCache.GetOrAdd(typeof(T), t => CreateDeepCopyAction<T>());
    dictCopyFunc(obj, clonedObject);
    return clonedObject;
}

DeepCloneWithTracking的作用就是接受一个字典,通过字典来控制对象的引用,从而实现【延迟】赋值的操作。其中的第二个关键点在于CreateDeepCopyAction,这个函数将创建一个浅拷贝,用于从深拷贝创建的对象中进行属性赋值。注意这里为什么不直接对clonedObject进行赋值呢?这是因为当我这里进行赋值时,是对当前clonedObject做了新的引用,而字典中保存的是旧的引用。这就会导致【延迟】策略失效。

复制代码
var a = new TestDto();
var b = a;
a = new TestDto();
a==b // false

复制代码
var a = new TestDto();
var b = a;
a.Name = "xxx";
a==b //true

所以我们只能通过CreateDeepCopyAction进行浅拷贝操作,而不能直接进行赋值,这里是关键。CreateDeepCopyAction的实现很简单,就是创建一个表达式,通过对新旧两个对象进行属性的浅拷贝赋值,代码不复杂:

复制代码
public static Action<T, T> CreateDeepCopyAction<T>()
{
    var sourceParameter = Expression.Parameter(typeof(T), "source");
    var targetParameter = Expression.Parameter(typeof(T), "target");
    var bindings = new List<Expression>();
    foreach (var property in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance))
    {
        if (property.CanRead && property.CanWrite)
        {
            var sourceProperty = Expression.Property(sourceParameter, property);
            var targetProperty = Expression.Property(targetParameter, property);
            var assign = Expression.Assign(targetProperty, sourceProperty);
            bindings.Add(assign);
        }
    }

    var body = Expression.Block(bindings);
    var lambda = Expression.Lambda<Action<T, T>>(body, sourceParameter, targetParameter);
    return lambda.Compile();
}

接着就是我们需要对构建表达式树的主体逻辑进行改造,让它支持传递字典,从而实现引用类型进行检测时,传递字典进去,改造后的代码如下:

复制代码
private static Expression<Func<T, Dictionary<object,object>,T>> CreateCloneExpression<T>()
{
    //反射获取类型
    var type = typeof(T);
    // 创建一个类型为T的参数表达式 'x'
    var parameterExpression = Expression.Parameter(type, "x");
    var parameterDictExpresson = Expression.Parameter(typeof(Dictionary<object,object>), "dict");
    // 创建一个成员绑定列表,用于稍后存放属性绑定
    var bindings = new List<MemberBinding>();
    // 遍历类型T的所有属性,选择可读写的属性
    foreach (var property in type.GetProperties().Where(prop => prop.CanRead && prop.CanWrite))
    {
        // 获取原始属性值的表达式
        var originalValue = Expression.Property(parameterExpression, property);
        // 初始化一个表达式用于存放可能处理过的属性值
        Expression valueExpression = null;
        // 标记是否已经处理过此属性
        bool handled = false;
        // 遍历所有处理器,查找可以处理当前属性类型的处理器
        foreach (var handler in handlers)
        {
            // 如果找到合适的处理器,使用它来创建克隆表达式
            if (handler.CanHandle(property.PropertyType))
            {
                valueExpression = handler.CreateCloneExpression(originalValue, parameterDictExpresson);
                handled = true;
                break;
            }
        }
        // 如果没有找到处理器,则使用原始属性值
        if (!handled)
        {
            valueExpression = originalValue;
        }
        // 创建属性的绑定
        var binding = Expression.Bind(property, valueExpression);
        // 将绑定添加到绑定列表中
        bindings.Add(binding);
    }
    // 使用所有的属性绑定来初始化一个新的T类型的对象
    var memberInitExpression = Expression.MemberInit(Expression.New(type), bindings);
    // 创建并返回一个表达式树,它表示从输入参数 'x' 到新对象的转换
    return Expression.Lambda<Func<T, Dictionary<object,object>, T>>(memberInitExpression, parameterExpression, parameterDictExpresson);
}

这里的核心就是Lambda表达式从Func<T, T>修改成了Func<T, Dictionary<object,object>, T>,从而实现对字典的输入。那么同样的,我们在具体的handler上也需要传递字典,如下:

复制代码
interface ICloneHandler
{
    bool CanHandle(Type type);
    Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset);
}

在具体的handler编写时,就可以传递这个字典:

复制代码
class ClassCloneHandler : ICloneHandler
{
    Type elementType;
    public bool CanHandle(Type type)
    {
        this.elementType = type;
        return type.IsClass && type != typeof(string);
    }

    public Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset)
    {
        var deepCloneMethod = typeof(DeepCloneExtension).GetMethod(nameof(DeepCloneWithTracking), BindingFlags.Public | BindingFlags.Static).MakeGenericMethod(elementType);
        return Expression.Call(deepCloneMethod, original, parameterHashset);
    }
}

其他的handler也同样进行相关改造,比如数组handler:

复制代码
class ArrayCloneHandler : ICloneHandler
{
    Type elementType;
    public bool CanHandle(Type type)
    {
        //数组类型要特殊处理获取其内部类型
        this.elementType = type.GetElementType();
        return type.IsArray;
    }

    public Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset)
    {
        //值类型或字符串,通过值类型数组赋值
        if (elementType.IsValueType || elementType == typeof(string))
        {
            return Expression.Call(GetType().GetMethod(nameof(DuplicateArray), BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(elementType), original);
        }
        //否则使用引用类型赋值
        else
        {
            var arrayCloneMethod = GetType().GetMethod(nameof(CloneArray), BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(elementType);
            return Expression.Call(arrayCloneMethod, original, parameterHashset);
        }
    }
    //引用类型数组赋值
    static T[] CloneArray<T>(T[] originalArray, Dictionary<object,object> dict) where T : class, new()
    {
        if (originalArray == null)
            return null;

        var length = originalArray.Length;
        var clonedArray = new T[length];
        for (int i = 0; i < length; i++)
        {
            clonedArray[i] = DeepCloneWithTracking(originalArray[i], dict);//调用该类型的深克隆表达式
        }
        return clonedArray;
    }
    //值类型数组赋值
    static T[] DuplicateArray<T>(T[] originalArray)
    {
        if (originalArray == null)
            return null;

        T[] clonedArray = new T[originalArray.Length];
        Array.Copy(originalArray, clonedArray, originalArray.Length);
        return clonedArray;
    }
}

最后实操一下,运行测试代码,可以看到b和b.child.father已经正确的被指向同一个引用了,和a与a.child.father一样效果: