在上一期博客里,我们提到使用使用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. Qt信号槽

    Qt 信号槽 Qt的信号槽,除了使用信号连接槽,还可以信号连接信号,断开信号,发送(转发)信号. 一个信号可以连接一个槽,也可以一个信号连接多个槽,还可以多个信号连接一个槽.   // 信号连接槽 c ...

  2. 设置 BCompare 打开文件时的默认字符编码

    每次比对 .h .cpp 文件,BCompare总是默认以西欧字符编码打开文件,导致中文都变成乱码了,还需要手动的修改文件字符编码,这样才能正常的显示.非常的不方便 然后我们就需要设置默认的字符编码, ...

  3. 可视化库 pygal 无法保存成本地文件

    问题:在使用可视化库 pygal 保存图像到本地时,出现报错 第一次报错是,提示没有  cairosvg  这个模块,所以直接通过 pip 安装 pip install cairosvg 安装完了以后 ...

  4. openGauss数据与PostgreSQL的差异对比

    openGauss 数据与 PostgreSQL 的差异对比 前言 openGauss 数据库已经发布 2.0.1 版本了,中启乘数科技是一家专业的专注于极致性能的数据库服务提供商,所以也关注 ope ...

  5. SEO — 搜索引擎优化

    一.多词排名标题设置 SEO(Search Engine Optimization)是指搜索引擎优化,即利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名 优化目的是:让网站在搜索引擎上的曝光率达到 ...

  6. Ez_pycode_dis qsnctfwp

    Python字节码基础 下载相关文件并打开,其中为 Python 字节码. 字节码格式为 源码行号 | 指令在函数中的偏移 | 指令符号 | 指令参数 | 实际参数值 根据上述字节码格式以及文件内容开 ...

  7. 物联网浏览器(IoTBrowser)-Java快速对接施耐德网络IO网关

    前一段时间有个Java技术栈的朋友联系到我,需要快速对接现有的无人值守称重系统,这里的对接是指替代现有系统,而非软件层面的对接,也就是利用现有的硬件开发一套替代现有软件的自动化系统.主要设备包括地磅秤 ...

  8. leetcode:655. 输出二叉树

    655. 输出二叉树 在一个 m*n 的二维字符串数组中输出二叉树,并遵守以下规则: 1> 行数 m 应当等于给定二叉树的高度. 2> 列数 n 应当总是奇数. 3> 根节点的值(以 ...

  9. gensim的word2vec的简单使用

    from gensim.models import Word2Vec as wtv import jieba s1 = "刘新宇是一个自然语言处理算法工程师" s2 = " ...

  10. 力扣1768(java&python)-交替合并字符串(简单)

    题目: 给你两个字符串 word1 和 word2 .请你从 word1 开始,通过交替添加字母来合并字符串.如果一个字符串比另一个字符串长,就将多出来的字母追加到合并后字符串的末尾. 返回 合并后的 ...