- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
在上一期博客里,我们提到使用使用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#强大的表达式树实现对象的深克隆之解决循环引用的问题的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
@Cacheable在同一类中方法调用无效 上述图片中,同一个类中genLiveBullets()方法调用同类中的queryLiveByRoom()方法,这样即便标识了Cacheable标签,
目录 @Transaction注解导致动态切换更改数据库失效 使用场景 遇到问题 解决 @Transaction
@RequestBody不能class类型匹配 在首次第一次尝试使用@RequestBody注解 开始加载字符串使用post提交(貌似只能post),加Json数据格式传输的时候,
目录 @Autowired注入static接口问题 @Autowired自动注入普通service很方便 但是如果注入static修饰的serv
目录 @RequestBody部分属性丢失 问题描述 JavaBean实现 Controller实现
目录 解决@PathVariable参数接收不完整的问题 今天遇到的问题是: 解决办法: @PathVariable接受的参
这几天在项目里面发现我使用@Transactional注解事务之后,抛了异常居然不回滚。后来终于找到了原因。 如果你也出现了这种情况,可以从下面开始排查。 1、特性 先来了解一下@Trans
概述: ? 1
场景: 在处理定时任务时,由于这几个方法都是静态方法,在aop的切面中使用@Around注解,进行监控方法调用是否有异常。 发现aop没有生效。 代码如下:
最近做项目的时候 用户提出要上传大图片 一张图片有可能十几兆 本来用的第三方的上传控件 有限制图片上传大小的设置 以前设置的是2M&nb
我已经实现了这个SCIM reference code在我们的应用程序中。 我实现的代码确实通过了此postman link中存在的所有用户测试集合。 。我的 SCIM Api 也被 Azure 接受
我一直对“然后”不被等待的行为感到困扰,我明白其原因。然而,我仍然需要绕过它。这是我的用例。 doWork(family) { return doWork1(family)
我正在尝试查找 channel 中的消息是否仍然存在,但是,我不确定如何解决 promise ,查看其他答案和文档,我可以看到它可能是通过函数实现的,但我是不完全确定如何去做。我希望能在这方面获得一些
我有以下情况: 同一工作区中的 2 个 Eclipse 项目:Apa 和 Bepa(为简洁起见,使用化名)。 Apa 项目引用(包括)Bepa 项目。 我在 Bepa 有一个类 X,具有公共(publ
这个问题已经有答案了: Why am I getting a NoClassDefFoundError in Java? (31 个回答) 已关闭 6 年前。 我正在努力学习 spring。所以我输入
我正在写一个小游戏,屏幕上有许多圆圈在移动。 我在两个线程中管理圈子,如下所示: public void run() { int stepCount = 0; int dx;
我在使用 Sympy 求解方程时遇到问题。当我运行代码时,例如: 打印(校正(10)) 我希望它打印一个数字 f。相反,它给我错误:执行中止。 def correction(r): from
好吧,我制作的每个页面都有这个问题。我不确定我做错了什么,但我所有的页面都不适用于所有分辨率。可能是因为我使用的是宽屏?大声笑我不确定,但在小于宽屏分辨率的情况下,它永远不会看起来正确。它的某些部分你
我正在尝试像这样进行一个非常简单的文化 srting 检查 if(culture.ToUpper() == "ES-ES" || "IT-IT") { //do something } else
Closed. This question is off-topic. It is not currently accepting answers. Learn more。 想改进这个问题吗?Upda
我是一名优秀的程序员,十分优秀!