使用c#强大的表达式树实现对象的深克隆

· 浏览次数 : 0

小编点评

**Func<T, T> CreateCloneExpression<T>();** 该函数创建一个从输入参数 `x` 到新对象的表达式的副本。它使用反射和表达式语法来动态构建一个新的类型。 **主要步骤:** 1. **获取类型:**反射获取目标类型 `T` 的类型。 2. **创建参数表达式:**创建一个类型为 `T` 的参数表达式。 3. **创建属性绑定:**遍历类型 `T` 的所有属性,并创建相应的属性绑定。 4. **初始化新对象:**使用属性绑定初始化一个新的 `T` 类型对象。 5. **构建表达式的树:**使用属性绑定和参数表达式构建一个表达式树,表示从输入参数到新对象的转换。 **常用的类型处理器:** * **数组处理:**`ArrayCloneHandler` 用于处理数组类型。 * **引用类型处理:**`DeepCloneExtension` 用于处理引用类型。 * **自定义类型处理:**`ClassCloneHandler` 用于处理自定义类型。 **示例:** ```csharp // 定义一个深克隆示例类 public class TestDto { public int Id { get; set; } public string Name { get; set; } public TestDto Child { get; set; } public Dictionary Record { get; set; } public List Scores { get; set; } } // 创建深克隆表达式 var deepCloneExpression = CreateCloneExpression(a); // 使用表达式进行深克隆 var b = deepCloneExpression.Compile(); ``` **结论:** `CreateCloneExpression` 函数提供了一个强大的表达式树机制,允许您动态构建从输入参数到新对象的转换表达式。这使得您可以轻松实现深克隆、属性绑定、类型转换等功能,从而提高代码的效率和可维护性。

正文

一、表达式树的基本概念

表达式树是一个以树状结构表示的表达式,其中每个节点都代表表达式的一部分。例如,一个算术表达式 a + b 可以被表示为一个树,其中根节点是加法运算符,它的两个子节点分别是 ab。在 LINQ(语言集成查询)中,表达式树使得能够将 C# 中的查询转换成其他形式的查询,比如 SQL 查询。这样,同样的查询逻辑可以用于不同类型的数据源,如数据库、XML 文件等。由于表达式树可以在运行时创建和修改,同样的它们非常适合需要根据运行时数据动态生成或改变代码逻辑的场景。这对于需要重复执行的逻辑(比如本文提到的深克隆)是非常有用的,因为它们可以被优化和缓存,从而提高效率。

二、创建和使用表达式树

在 C# 中,我们可以通过 System.Linq.Expressions 命名空间中的类来创建和操作表达式树。以下是一个创建简单表达式树的示例:

        // 创建一个表达式树表示 a + b
        ParameterExpression a = Expression.Parameter(typeof(int), "a");
        ParameterExpression b = Expression.Parameter(typeof(int), "b");
        BinaryExpression body = Expression.Add(a, b);

        // 编译表达式树为可执行代码
        var add = Expression.Lambda<Func<int, int, int>>(body, a, b).Compile();

        // 使用表达式
        Console.WriteLine(add(1, 2));  // 输出 3

当我们定义了一个类型后,我们可以通过一个匿名委托进行值拷贝来实现深克隆:

//自定义类型
public class TestDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}
//匿名委托
Func<TestDto, TestDto> deepCopy = x => new TestDto()
{
    Id = x.Id,
    Name = x.Name
};
//使用它
var a =new TestDto(){//赋值};
var b = deepCopy(a);//实现深克隆

那么想要自动化的创建这一匿名委托就会用到表达式树,通过自动化的方式来实现匿名委托的自动化创建,这样就可以实现复杂的自动化表达式创建从而不必依赖反射、序列化/反序列化等等比较消耗性能的方式来实现。核心的业务逻辑部分如下:首先我们需要知道表达式树通过反射来遍历对象的属性,来实现x = old.x这样的赋值操作。而对于不同的属性比如数组、字典、值类型、自定义类、字符串,其赋值方案是不同的,简单的值类型和字符串我们可以直接通过=赋值,因为这两者的赋值都是“深”克隆。也就是赋值后的变量修改不会影响原始变量。而复杂的字典、数组、对象如果使用=赋值,则只会得到对象的引用,所以针对不同的情况需要不同的处理。

首先我们需要定义一个接口ICloneHandler,针对不同情况使用继承该接口的处理类来处理:

interface ICloneHandler
{
    bool CanHandle(Type type);//是否可以处理当前类型
    Expression CreateCloneExpression(Expression original);//生成针对当前类型的表达式树
}

接着我们定义一个扩展类和扩展函数,用于处理深拷贝:

public static class DeepCloneExtension
{
    //创建一个线程安全的缓存字典,复用表达式树
    private static readonly ConcurrentDictionary<Type, Delegate> cloneDelegateCache = new ConcurrentDictionary<Type, Delegate>();
    //定义所有可处理的类型,通过策略模式实现了可扩展
    private static readonly List<ICloneHandler> handlers = new List<ICloneHandler>
    {
       //在此处添加自定义的类型处理器
    };
    /// <summary>
    /// 深克隆函数
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="original"></param>
    /// <returns></returns>
    public static T DeepClone<T>(this T original)
    {
        if (original == null)
            return default;
        // 获取或创建克隆表达式
        var cloneFunc = (Func<T, T>)cloneDelegateCache.GetOrAdd(typeof(T), t => CreateCloneExpression<T>().Compile());
        //调用表达式,返回结果
        return cloneFunc(original);
    }
    /// <summary>
    /// 构建表达式树的主体逻辑
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    private static Expression<Func<T, T>> CreateCloneExpression<T>()
    {
        //反射获取类型
        var type = typeof(T);
        // 创建一个类型为T的参数表达式 'x'
        var parameterExpression = Expression.Parameter(type, "x");
        // 创建一个成员绑定列表,用于稍后存放属性绑定
        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);
                    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, T>>(memberInitExpression, parameterExpression);
    }
}

接下来我们就可以添加一些常见的类型处理器:

数组处理:

class ArrayCloneHandler : ICloneHandler
{
    Type elementType;
    public bool CanHandle(Type type)
    {
        //数组类型要特殊处理获取其内部类型
        this.elementType = type.GetElementType();
        return type.IsArray;
    }

    public Expression CreateCloneExpression(Expression original)
    {
        //值类型或字符串,通过值类型数组赋值
        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);
        }
    }
    //引用类型数组赋值
    static T[] CloneArray<T>(T[] originalArray) 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] = DeepClone(originalArray[i]);//调用该类型的深克隆表达式
        }
        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;
    }
}

自定义类型处理(其实就是调用该类型的深克隆):

class ClassCloneHandler : ICloneHandler
{
    Type elementType;
    public bool CanHandle(Type type)
    {
        this.elementType = type;
        return type.IsClass && type != typeof(string);
    }

    public Expression CreateCloneExpression(Expression original)
    {
        var deepCloneMethod = typeof(DeepCloneExtension).GetMethod(nameof(DeepClone), BindingFlags.Public | BindingFlags.Static).MakeGenericMethod(elementType);
        return Expression.Call(deepCloneMethod, original);
    }
}

接着我们就可以在之前的DeepCloneExtension中添加这些handles

private static readonly List<ICloneHandler> handlers = new List<ICloneHandler>
{
    new ArrayCloneHandler(),//数组
    new DictionaryCloneHandler(),//字典
    new ClassCloneHandler()//
    ...
};

最后我们可以通过简单的进行调用就可以实现深克隆了

var a = new TestDto() { Id = 1, Name = "小明", Child = new TestDto() { Id = 2, Name = "小红" }, Record = new Dictionary<string, int>() { { "1年级", 1 }, { "2年级", 2 } }, Scores = [100, 95] };
var b = a.DeepClone();

总之,C# 的表达式树提供了一个强大的机制,可以将代码以数据结构的形式表示出来,使得代码可以在运行时进行检查、修改或执行。这为动态查询生成、代码优化和动态编程提供了很多可能性。

 

与使用c#强大的表达式树实现对象的深克隆相似的内容:

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

在上一期博客里,我们提到使用使用c#强大的表达式树实现对象的深克隆,文章地址:https://www.cnblogs.com/gmmy/p/18186750。但是文章里没有解决如何实现循环引用的问题。 循环引用 在C#中,循环引用通常发生在两个或更多的对象相互持有对方的引用,从而形成一个闭环。这种情

使用c#强大的表达式树实现对象的深克隆

一、表达式树的基本概念 表达式树是一个以树状结构表示的表达式,其中每个节点都代表表达式的一部分。例如,一个算术表达式 a + b 可以被表示为一个树,其中根节点是加法运算符,它的两个子节点分别是 a 和 b。在 LINQ(语言集成查询)中,表达式树使得能够将 C# 中的查询转换成其他形式的查询,比如

神奇的JavaScript弱等价类型转换

JavaScript语言特性 - 类型转换 JavaScript这门语言的类型系统从来没有它表面看起来的那样和善,虽然比起Java、C#等一众强类型语言,它的弱类型使用起来似乎是如此便利,但正因为它极高的自由度,所以才会衍生出令人摸不着头脑的荒诞行为。 举个例子,虽然我们都知道一个包含内容的字符串会

从需求角度介绍PasteSpider(K8S平替部署工具适合于任何开发语言)

你是否被K8S的强大而吸引,我相信一部分人是被那复杂的配置和各种专业知识而劝退,应该还有一部分人是因为K8S太吃资源而放手! 这里介绍一款平替工具PasteSpider,PasteSpider是一款使用c#编写的linux容器部署工具(使用PasteSpider和自己用啥语言开发没关系哈!),简单易

C#开源实用的工具类库,集成超过1000多种扩展方法

前言 今天大姚给大家分享一个C#开源(MIT License)、免费、实用且强大的工具类库,集成超过1000多种扩展方法增强 .NET Framework 和 .NET Core的使用效率:Z.ExtensionMethods。 直接项目引入类库使用 在你的对应项目中NuGet包管理器中搜索:Z.E

golang的 CGO 是什么

CGO是Go(Golang)语言中的一个工具,全称为 "C-Go" 或者 "C for Go"。 它是Go标准库的一部分,允许Go代码与C语言代码进行交互。 CGO提供了在Go程序中使用C语言库的能力,同时也允许C代码调用Go的函数。 通过CGO,开发者可以利用Go语言的强类型和垃圾回收等特性,同时

基于ChatGPT的API的C#接入研究

今年开年,最火的莫过于ChatGPT的相关讨论,这个提供了非常强大的AI处理,并且整个平台也提供了很多对应的API进行接入的处理,使得我们可以在各种程序上无缝接入AI的后端处理,从而实现智能AI的各种应用。ChatGPT的API可以在前端,以及一些后端进行API的接入,本篇随笔主要介绍基于ChatGPT的API的C#接入研究。

使用c#的 async/await编写 长时间运行的基于代码的工作流的 持久任务框架

持久任务框架 (DTF) 是基于async/await 工作流执行框架。工作流的解决方案很多,包括Windows Workflow Foundation,BizTalk,Logic Apps, Workflow-Core 和 Elsa-Core。最近我在Dapr 的仓库里跟踪工作流构建块的进展时,深

使用c#实现23种常见的设计模式

# 使用c#实现23种常见的设计模式 设计模式通常分为三个主要类别: - 创建型模式 - 结构型模式 - 行为型模式。 这些模式是用于解决常见的对象导向设计问题的最佳实践。 以下是23种常见的设计模式并且提供`c#代码案例`: ## 创建型模式: ### 1. 单例模式(Singleton) ```

使用C#编写.NET分析器-第二部分

## 译者注 这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断)、IDE、诊断工具中,比如Datadog的APM,Visual Studio的分析器以及Rider和Resh