首页 > 编程语言 >使用c#强大的表达式树实现对象的深克隆之解决循环引用的问题

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

时间:2024-05-15 11:41:41浏览次数:32  
标签:return 克隆 c# original var new Expression public 表达式

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

 

标签:return,克隆,c#,original,var,new,Expression,public,表达式
From: https://www.cnblogs.com/gmmy/p/18193549

相关文章

  • 在Linux中,什么是子网和CIDR?
    在网络通信中,子网(Subnet)和CIDR(无类别域间路由,ClasslessInter-DomainRouting)是两个重要的概念,它们与IP地址的分配和网络的组织方式密切相关。1.子网(Subnet)子网是将一个较大的网络划分成若干个较小的网络的过程。这样做的目的是为了更好地组织和管理网络,提高网络安全性,以及优化......
  • SSL Medium Strength Cipher Suites Supported (SWEET32)漏洞修复
    近期对公司开发环境的机器进行了安全扫描,在扫描安全报告中出现了SSLMediumStrengthCipherSuitesSupported(SWEET32)漏洞,汇报后领导表示需要进行修复,特记录此漏洞修复的过程。漏洞产生的原因漏洞的原因主要是由于SSL/TLS协议中使用的DES(DataEncryptionStandard)及Trip......
  • tcp协议
    TCP(TransmissionControlProtocol,传输控制协议)被描述为面向连接的和可靠的传输协议,这两个特性是其核心优势,下面分别解释这两个概念:###面向连接"面向连接"意味着在数据传输开始之前,通信双方必须先建立一个明确的连接。这个过程类似于打电话之前先拨号建立连接:1.**三次握手**:T......
  • linux crontab命令
    格式f1f2f3f4f5program其中f1是表示分钟,f2表示小时,f3表示一个月份中的第几日,f4表示月份,f5表示一个星期中的第几天。program表示要执行的程序。当f1为*时表示每分钟都要执行program,f2为*时表示每小时都要执行程序,其馀类推当f1为a-b时表示从第a分......
  • 使用c#强大的表达式树实现对象的深克隆
    使用c#强大的表达式树实现对象的深克隆 一、表达式树的基本概念表达式树是一个以树状结构表示的表达式,其中每个节点都代表表达式的一部分。例如,一个算术表达式 a+b 可以被表示为一个树,其中根节点是加法运算符,它的两个子节点分别是 a 和 b。在LINQ(语言集成查询)中,表达......
  • MAC下的homebrew安装及镜像调整
    一、安装未记录 二、镜像调整cd"$(brew--repo)"gitremoteset-urloriginhttps://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git#onlydothisbelowcd"$(brew--repo)/Library/Taps/homebrew/homebrew-core"gitremoteset-urloriginhttps://mi......
  • K8S之yaml 文件详解pod、deployment、service(转)
    原文:https://blog.csdn.net/footless_bird/article/details/125946101作者:墨鸦_Cormorant来源:CSDN K8S中的yaml文件yaml语法学习 Kubernetes支持YAML和JSON格式管理资源对象 JSON格式:主要用于api接口之间消息的传递 YAML格式:用于配置和管理,YAML是一种简......
  • PLC的一些基础介绍
    1、W点和D点D点信号(DataPoints):数据存储:D点通常指的是PLC中的“数据”存储区域,用于存储各种类型的数据,如整数、浮点数、字符串等。变量使用:在PLC程序中,D点可以作为变量使用,用于存储中间结果、计数器值、定时器值等。寻址方式:D点通常有连续的地址编号,如D0、D1、D2...,或者按照数......
  • [C#] [WPF] 在MVVM中实现拖拽功能 - 入门
    拖拽功能是使用频率较高的一种交互,用传统方法去写很简单,但是在mvvm规则下,也没必要弄得很麻烦我的入门案例就选择了使用mvvm重写tutorialspoint-Interaction里的调色盘案例,view如下MainWindow.xaml这里的重点是控件要允许拖拽以及对应的事件目标控件,填充色绑定,......
  • Unrecognized option: --add-opens java.base/java.lang=ALL-UNNAMED
    由于Java中有关反射相关的功能自从JDK9就开始进行了限制,因此如果还想继续在JDK9以及更高的版本中使用反射相关的功能,需要添加JVM启动参数,--add-opensjava.base/java.lang=ALL-UNNAMED。由于现在的项目都是用Docker部署的,所以紧接着在DockerFile中添加了这个JVM启动参数,如下......