在上一期博客里,我们提到使用使用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#强大的表达式树实现对象的深克隆之解决循环引用的问题的更多相关文章

  1. LinQ实战学习笔记(三) 序列,查询操作符,查询表达式,表达式树

    序列 延迟查询执行 查询操作符 查询表达式 表达式树 (一) 序列 先上一段代码, 这段代码使用扩展方法实现下面的要求: 取进程列表,进行过滤(取大于10M的进程) 列表进行排序(按内存占用) 只保留 ...

  2. 转载:C#特性-表达式树

    原文地址:http://www.cnblogs.com/tianfan/ 表达式树基础 刚接触LINQ的人往往觉得表达式树很不容易理解.通过这篇文章我希望大家看到它其实并不像想象中那么难.您只要有普通 ...

  3. C#特性-表达式树

    表达式树ExpressionTree   表达式树基础 转载需注明出处:http://www.cnblogs.com/tianfan/ 刚接触LINQ的人往往觉得表达式树很不容易理解.通过这篇文章我希 ...

  4. 干货!表达式树解析"框架"(2)

    最新设计请移步 轻量级表达式树解析框架Faller http://www.cnblogs.com/blqw/p/Faller.html 为了过个好年,我还是赶快把这篇完成了吧 声明 本文内容需要有一定 ...

  5. 表达式树解析"框架"

    干货!表达式树解析"框架"(2)   为了过个好年,我还是赶快把这篇完成了吧 声明 本文内容需要有一定基础的开发人员才可轻松阅读,如果有难以理解的地方可以跟帖询问,但我也不一定能回 ...

  6. Lambda表达式和Lambda表达式树

    LINQ的基本功能就是创建操作管道,以及这些操作需要的任何状态. 为了富有效率的使用数据库和其他查询引擎,我们需要一种不同的方式表示管道中的各个操作.即把代码当作可在编程中进行检查的数据. Lambd ...

  7. 小解系列-自关联对象.Net MVC中 json序列化循环引用问题

    自关联对象在实际开发中用的还是比较多,例如常见的树形菜单.本文是自己实际的一个小测试,可以解决循环引用对象的json序列化问题,文笔不好请多见谅,如有错误请指出,希望有更好的解决方案,一起进步. 构造 ...

  8. Python对象的循环引用问题

    目录 Python对象循环引用 循环引用垃圾回收算法 容器对象 生成容器对象 追踪容器对象 结束追踪容器对象 分代容器对象链表 何时执行循环引用垃圾回收 循环引用的垃圾回收 循环引用中的终结器 pyt ...

  9. C#3.0新特性:隐式类型、扩展方法、自动实现属性,对象/集合初始值设定、匿名类型、Lambda,Linq,表达式树、可选参数与命名参数

    一.隐式类型var 从 Visual C# 3.0 开始,在方法范围中声明的变量可以具有隐式类型var.隐式类型可以替代任何类型,编译器自动推断类型. 1.var类型的局部变量必须赋予初始值,包括匿名 ...

  10. C# 快速高效率复制对象另一种方式 表达式树

    1.需求 在代码中经常会遇到需要把对象复制一遍,或者把属性名相同的值复制一遍. 比如: public class Student { public int Id { get; set; } publi ...

随机推荐

  1. #李超线段树,树链剖分#洛谷 4069 [SDOI2016]游戏

    题目 分析 就是把线段扔到了树上,注意区间查询要比较两个端点的函数值, 把区间赋值转换成两部分,从起点到LCA的区间是斜率为负数的线段, 从终点到LCA的区间是斜率为正数的线段. 代码 #includ ...

  2. Pandas通用函数和运算

    Pandas继承了Numpy的运算功能,可以快速对每个元素进行运算,即包括基本运算(加减乘除等),也包括复杂运算(三角函数.指数函数和对数函数等). 通用函数使用 apply和applymap app ...

  3. Python查询上周五是多少号

    使用Python可以轻松的查询出上周几是多少号,这周几是什么多少号,以下是查询上周五的示例: import datetime, calendar last = datetime.date.today( ...

  4. 美团一面,面试官让介绍AQS原理并手写一个同步器,直接凉了

    写在开头 今天在牛客上看到了一个帖子,一个网友吐槽美团一面上来就让手撕同步器,没整出来,结果面试直接凉凉. 就此联想到一周前写的一篇关于AQS知识点解析的博文,当时也曾埋下伏笔说后面会根据AQS的原理 ...

  5. HDC2022 开发者亮点抢先看,线上线下精彩活动等你探索!

    原文:https://mp.weixin.qq.com/s/A2sfpPKBvF6zwinbUsgwiw,点击链接查看更多技术内容.

  6. 面试连环炮系列(二十六):什么情况下JVM频繁发生full GC

    1. 什么情况下JVM频繁发生full GC? full gc触发条件是老年代空间不足,具体原因有四个: 系统并发高.执行耗时长或者创建对象过多,导致 young gc频繁,且gc后存活对象太多,但是 ...

  7. 给蚂蚁金服 antv 提个 PR, 以为是改个错别字, 未曾想背后的原因竟如此复杂!

    前言 什么? 你不了解G2Plot? 没关系, 今天咱们要分享的内容和G2Plot的关系, 就像雷锋和雷峰塔的关系. 因此, 不必担心听不懂. 我一直觉得, 如果我写的文章有人看不懂, 那一定是我写的 ...

  8. ASP.NET MVC 性能优化和调试

    学习 .NET Core 应用程序的调试技术可以分为以下步骤: 理解基础概念:首先,你需要理解什么是调试以及为什么我们需要调试.理解断点.单步执行.变量监视等基本调试概念. 学习 Visual Stu ...

  9. 剑指offer56(Java)-数组中出现的次数Ⅰ(中等)

    题目: 一个整型数组 nums 里除两个数字之外,其他数字都出现了两次.请写程序找出这两个只出现一次的数字.要求时间复杂度是O(n),空间复杂度是O(1). 示例 1: 输入:nums = [4,1, ...

  10. OpenYurt v1.1.0: 新增 DaemonSet 的 OTA 和 Auto 升级策略

    简介: 在 OpenYurt v1.1.0 版本中,我们提供了 Auto 和 OTA 的升级策略.Auto 的升级策略重点解决由于节点 NotReady 而导致 DaemonSet升级阻塞的问题,OT ...