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

· 浏览次数 : 9

小编点评

**类** ```csharp public class DeepCloneExtension : ICloneHandler { public Type elementType; public bool CanHandle(Type type) { //特殊处理数组类型 if (elementType.IsValueType || elementType == typeof(string)) { return true; } //其他类型进行深克隆 return type.IsClass && type != typeof(string); } public Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset) { //进行深克隆操作 if (elementType.IsValueType || elementType == typeof(string)) { return Expression.Call(GetMethod(nameof(DeepClone), BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(elementType), original, parameterHashset); } //否则使用引用类型赋值 else { return Expression.Call(GetMethod(nameof(Clone), BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(elementType), original, parameterHashset); } } } ``` **接口** ```csharp public interface ICloneHandler { bool CanHandle(Type type); Expression CreateCloneExpression(Expression original, ParameterExpression parameterHashset); } ``` **测试** ```csharp //测试类 public class TestDto { public string Name { get; set; } } //测试深克隆方法 public static void Test() { //创建两个对象,并进行深克隆操作 var a = new TestDto(); var b = a; //设置属性值 a.Name = "xxx"; //判断深克隆是否成功 Console.WriteLine(b.Name); // 输出 "xxx" } ```

正文

在上一期博客里,我们提到使用使用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一样效果:

 

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

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

在上一期博客里,我们提到使用使用c#强大的表达式树实现对象的深克隆,文章地址:https://www.cnblogs.com/gmmy/p/18186750。但是文章里没有解决如何实现循环引用的问题。 循环引用 在C#中,循环引用通常发生在两个或更多的对象相互持有对方的引用,从而形成一个闭环。这种情

使用c#强大的表达式树实现对象的深克隆

一、表达式树的基本概念 表达式树是一个以树状结构表示的表达式,其中每个节点都代表表达式的一部分。例如,一个算术表达式 a + b 可以被表示为一个树,其中根节点是加法运算符,它的两个子节点分别是 a 和 b。在 LINQ(语言集成查询)中,表达式树使得能够将 C# 中的查询转换成其他形式的查询,比如

神奇的JavaScript弱等价类型转换

JavaScript语言特性 - 类型转换 JavaScript这门语言的类型系统从来没有它表面看起来的那样和善,虽然比起Java、C#等一众强类型语言,它的弱类型使用起来似乎是如此便利,但正因为它极高的自由度,所以才会衍生出令人摸不着头脑的荒诞行为。 举个例子,虽然我们都知道一个包含内容的字符串会

从需求角度介绍PasteSpider(K8S平替部署工具适合于任何开发语言)

你是否被K8S的强大而吸引,我相信一部分人是被那复杂的配置和各种专业知识而劝退,应该还有一部分人是因为K8S太吃资源而放手! 这里介绍一款平替工具PasteSpider,PasteSpider是一款使用c#编写的linux容器部署工具(使用PasteSpider和自己用啥语言开发没关系哈!),简单易

C#开源实用的工具类库,集成超过1000多种扩展方法

前言 今天大姚给大家分享一个C#开源(MIT License)、免费、实用且强大的工具类库,集成超过1000多种扩展方法增强 .NET Framework 和 .NET Core的使用效率:Z.ExtensionMethods。 直接项目引入类库使用 在你的对应项目中NuGet包管理器中搜索:Z.E

golang的 CGO 是什么

CGO是Go(Golang)语言中的一个工具,全称为 "C-Go" 或者 "C for Go"。 它是Go标准库的一部分,允许Go代码与C语言代码进行交互。 CGO提供了在Go程序中使用C语言库的能力,同时也允许C代码调用Go的函数。 通过CGO,开发者可以利用Go语言的强类型和垃圾回收等特性,同时

基于ChatGPT的API的C#接入研究

今年开年,最火的莫过于ChatGPT的相关讨论,这个提供了非常强大的AI处理,并且整个平台也提供了很多对应的API进行接入的处理,使得我们可以在各种程序上无缝接入AI的后端处理,从而实现智能AI的各种应用。ChatGPT的API可以在前端,以及一些后端进行API的接入,本篇随笔主要介绍基于ChatGPT的API的C#接入研究。

使用c#的 async/await编写 长时间运行的基于代码的工作流的 持久任务框架

持久任务框架 (DTF) 是基于async/await 工作流执行框架。工作流的解决方案很多,包括Windows Workflow Foundation,BizTalk,Logic Apps, Workflow-Core 和 Elsa-Core。最近我在Dapr 的仓库里跟踪工作流构建块的进展时,深

使用c#实现23种常见的设计模式

# 使用c#实现23种常见的设计模式 设计模式通常分为三个主要类别: - 创建型模式 - 结构型模式 - 行为型模式。 这些模式是用于解决常见的对象导向设计问题的最佳实践。 以下是23种常见的设计模式并且提供`c#代码案例`: ## 创建型模式: ### 1. 单例模式(Singleton) ```

使用C#编写.NET分析器-第二部分

## 译者注 这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断)、IDE、诊断工具中,比如Datadog的APM,Visual Studio的分析器以及Rider和Resh