在上一期博客里,我们提到使用使用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. #扩展域并查集,线段树分治#CF576E Painting Edges

    题目链接 题目翻译 给定一张 \(n\) 个点 \(m\) 条边的无向图. 一共有 \(k\) 种颜色,一开始,每条边都没有颜色. 定义合法状态为仅保留染成 \(k\) 种颜色中的任何一种颜色的边,图 ...

  2. CSP-S2021江西自评分数(10-26)

    娱乐性质,不负责任 在机房大佬的努力下,评测完了 总表 姓名 编号 总分 airport bracket palin traffic JX-00001 JX-00001 0 0 0 0 0 JX-00 ...

  3. Qt调用摄像头一,基础版

    本示例,为纯Qt调用摄像头,功能比较简单,打开摄像头,设置参数,拍照 涉及到的功能有: 获取摄像头列表 获取摄像头分辨率 获取摄像头帧率 获取摄像头支持的视频模式 设置摄像头参数 拍照 此版本的缺点是 ...

  4. Drop 、Delete、Truncate的区别是什么

    Drop .Delete.Truncate 的区别是什么? DROP 删除表结构和数据,truncate 和 delete 只删除数据 truncate 操作,表和索引所占用的空间会恢复到初始大小:d ...

  5. HarmonyOS自定义抽奖转盘开发(ArkTS)

      介绍 本篇Codelab是基于画布组件.显式动画,实现的一个自定义抽奖圆形转盘.包含如下功能: 1.  通过画布组件Canvas,画出抽奖圆形转盘. 2.  通过显式动画启动抽奖功能. 3.  通 ...

  6. MMDeploy部署实战系列【第一章】:Docker,Nvidia-docker安装

    MMDeploy部署实战系列[第一章]:Docker,Nvidia-docker安装 这个系列是一个随笔,是我走过的一些路,有些地方可能不太完善.如果有那个地方没看懂,评论区问就可以,我给补充. 版权 ...

  7. 吴恩达机器学习课后作业ex1

    题目大体意思就是输入的是某地的人口,输出的是某地方的收益. 题目及数据集下载: https://wwa.lanzous.com/b054sprza 密码:ba3w 大体模型如下图:现在X前边加一列值为 ...

  8. c#程序员必学清单

    必读书目:1. "Effective C#: 50 Specific Ways to Improve Your C#" by Bill Wagner2. "CLR via ...

  9. vuex合作怎么用仓库

  10. 中国大陆地区维护的Linux操作系统

    Linux开源生态丰富,中国大陆地区基于CentOS停服,依托阿里云.腾讯云.华为云三大私营企业,相继发布了自己的开源Linux定制版,很高兴的是他们只是改个名字并没有选择闭门造车,只是官网还是很不耻 ...