C# 通过反射实现复杂对象的深拷贝(附源码)

摘要:
在分析过程中,让我们首先看看CloneUtil的实现,它是该过程的核心。让我们先看看源代码,然后一步一步地分析流程。

背景

  在C#中我们很多时候需要对一个对象进行深拷贝,当然如果已知当前对象类型的时候我们当然可以通过创建新对象逐一进行赋值的方式来进行操作,但是这种操作非常繁琐而且如果你在做一个顶层框架的时候要实现这样一个功能,并且深拷贝的方式复制的对象是一个object类型,这个时候这个方式就不再适用了,可能还有很多说可以通过序列化和反序列化的方式进行对象的深拷贝但还是回到之前的话题,如果你现在开发的是一个顶层框架,你并不知道传过来的对象是否添加【Serialize】标签,即对象是否支持序列化这个时候你又无能为力了,今天的这篇文章主要通过使用反射这一个技术来实现对一个object类型的深度拷贝,当然我们需要考虑到很多种情况,在这个过程中通过递归一层层调用就避免不了了,因为你不知道这个对象到底有什么样的嵌套层次,所以想写好这样一个通用的功能也是不太容易的。

过程

  在分析的过程中我们先来看看这个里面最核心的CloneUtil的实现,我们先来看源码,然后在来一步步分析这个过程。

using System;
using System.Collections;
using System.Diagnostics;
using System.Reflection;

namespace DeepCopyConsoleApp
{
    public sealed class CloneUtil
    {
        public static object CloneObject(object objSource)
        {
            //Get the type of source object and create a new instance of that type
            Type typeSource = objSource.GetType();
            object objTarget = Activator.CreateInstance(typeSource);
            //Get all the properties of source object type
            PropertyInfo[] propertyInfo = typeSource.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            //Assign all source property to taget object 's properties
            foreach (PropertyInfo property in propertyInfo)
            {
                //Check whether property can be written to
                if (!property.CanWrite) continue;
                //check whether property type is value type, enum or string type
                if (property.PropertyType.IsPrimitive || property.PropertyType.IsEnum || property.PropertyType == typeof(string))
                {
                    property.SetValue(objTarget, property.GetValue(objSource, null), null);
                }
                else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
                {
                    //Include List and Dictionary and......
                    if (property.PropertyType.IsGenericType)
                    {
                        var cloneObj = Activator.CreateInstance(property.PropertyType);

                        var addMethod = property.PropertyType.GetMethod("Add", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

                        Debug.Assert(null != addMethod);

                        var currentValues = property.GetValue(objSource, null) as IEnumerable;
                        if (currentValues == null)
                        {
                            property.SetValue(objTarget, null, null);
                        }
                        else
                        {
                            if (cloneObj is IDictionary)
                            {
                                cloneObj = cloneObj as IDictionary;
                                foreach (var currentValue in currentValues)
                                {
                                    var propInfoKey = currentValue.GetType().GetProperty("Key");
                                    var propInfoValue = currentValue.GetType().GetProperty("Value");
                                    if (propInfoKey != null && propInfoValue != null)
                                    {
                                        var keyValue = propInfoKey.GetValue(currentValue, null);
                                        var valueValue = propInfoValue.GetValue(currentValue, null);

                                        object finalKeyValue, finalValueValue;

                                        //Get finalKeyValue
                                        var currentKeyType = keyValue.GetType();
                                        if (currentKeyType.IsPrimitive || currentKeyType.IsEnum || currentKeyType == typeof(string))
                                        {
                                            finalKeyValue = keyValue;
                                        }
                                        else
                                        {
                                            //Reference type
                                            var copyObj = CloneObject(keyValue);
                                            finalKeyValue = copyObj;
                                        }

                                        //Get finalValueValue
                                        var currentValueType = valueValue.GetType();
                                        if (currentValueType.IsPrimitive || currentValueType.IsEnum || currentValueType == typeof(string))
                                        {
                                            finalValueValue = valueValue;
                                        }
                                        else
                                        {
                                            //Reference type
                                            var copyObj = CloneObject(valueValue);
                                            finalValueValue = copyObj;
                                        }

                                        addMethod.Invoke(cloneObj, new[] { finalKeyValue, finalValueValue });
                                    }

                                }
                                property.SetValue(objTarget, cloneObj, null);
                            }
                            //Common IList type
                            else
                            {
                                foreach (var currentValue in currentValues)
                                {
                                    var currentType = currentValue.GetType();
                                    if (currentType.IsPrimitive || currentType.IsEnum || currentType == typeof(string))
                                    {
                                        addMethod.Invoke(cloneObj, new[] { currentValue });
                                    }
                                    else
                                    {
                                        //Reference type
                                        var copyObj = CloneObject(currentValue);
                                        addMethod.Invoke(cloneObj, new[] { copyObj });
                                    }
                                }
                                property.SetValue(objTarget, cloneObj, null);
                            }
                        }
                    }
                    //Array type
                    else
                    {
                        var currentValues = property.GetValue(objSource, null) as Array;
                        if (null == currentValues)
                        {
                            property.SetValue(objTarget, null, null);
                        }
                        else
                        {
                            var cloneObj = Activator.CreateInstance(property.PropertyType, currentValues.Length) as Array;
                            var arrayList = new ArrayList();
                            for (var i = 0; i < currentValues.Length; i++)
                            {
                                var currentType = currentValues.GetValue(i).GetType();
                                if (currentType.IsPrimitive || currentType.IsEnum || currentType == typeof(string))
                                {
                                    arrayList.Add(currentValues.GetValue(i));
                                }
                                else
                                {
                                    //Reference type
                                    var copyObj = CloneObject(currentValues.GetValue(i));
                                    arrayList.Add(copyObj);
                                }
                            }
                            arrayList.CopyTo(cloneObj, 0);
                            property.SetValue(objTarget, cloneObj, null);
                        }
                    }
                }
                //else property type is object/complex types, so need to recursively call this method until the end of the tree is reached
                else
                {
                    object objPropertyValue = property.GetValue(objSource, null);
                    if (objPropertyValue == null)
                    {
                        property.SetValue(objTarget, null, null);
                    }
                    else
                    {
                        property.SetValue(objTarget, CloneObject(objPropertyValue), null);
                    }
                }
            }
            return objTarget;
        }
    }
}

  首先设计这个函数的时候这个函数的输入是一个object对象,然后输出是这个对象的copy,这个对象是确定的,所以进入这个函数第一步就是获取源对象的类型然后通过Activator创建一个实例,然后通过反射获取这个对象的所有Properties,然后将数据源的Property逐一赋值的Copy对象的Property里面,所以这里进来就是一个大循环,后面就涉及到属性的赋值,当然属性包括简单属性以及复杂属性,我们可以通过下面的代码来对一个基础类型进行赋值,这个过程直接将数据源的Property赋值到目标的Property上面,这里的简单类型我们通过通过PropertyType的IsPrimitive、IsEnum以及字符串类型这三种,这些都是可以彼此之间相互赋值的类型,可以通过下面的代码实现。

//check whether property type is value type, enum or string type
                if (property.PropertyType.IsPrimitive || property.PropertyType.IsEnum || property.PropertyType == typeof(string))
                {
                    property.SetValue(objTarget, property.GetValue(objSource, null), null);
                }

  后面的一个循环是整个过程中最复杂的类型,即当前类型从IEnumerable接口继承下来的对象,包括数组、List,Stack、HashSet、Dictionary类型等等,这每种情况都比较特殊,再进入这个循环的时候通过当前类型是否是泛型类型来确定是否Array类型,我们先来看看这个泛型里面比较特殊的一个类型Dictionary,这个可以发挥的东西就比较多了所以通过判断当前对象是否是IDictionary接口来确定是否是字典类型,如果是字典类型我们首先来看获取这个Dictionary的值,记住获取到这个值以后需要将其转换为IEnumerable类型从而方便后面进行遍历,在遍历这个值的时候,我们再通过反射获取其Key和Value,然后我们分别来复制这个key和value,在这个过程中我们需要分别判断key和value的类型,如果是简单类型直接赋值,如果key和value仍是复杂类型,下一次迭代就又开始了,直到把最终的值类型全部Copy出来,在这个过程中我们获取到数据源的一组Key和Value的Copy对象之后,我们需要将这组copy值添加到objTarget的对应类型中去,这里的Add方法又是通过反射GetMethod方法获取的,如果当前对象是List就获取List的Add方法,如果当前过程是Dictionary那么久获取Dictionary类型的Add方法,获取了这个对应addMethod方法之后我们就能够把当前的copy对象添加到最终的objTarget类型中去。说完了Dictionary类型我们再来看看类似List这种类型,包括List、Stack、HashSet这些类型,这些相对Dictionary来说要更简单一些因为他们的泛型参数只有一个,处理的方法也类似那就是遍历原始数据,后面看当前泛型类型是否是基础类型,如果是基础类型直接获取,如果是复杂类型则计入到下一个迭代,整个过程再走一遍。

  说完了泛型类型我们来看一个比较大的数组类型,我们也先来看看这个部分的代码。

var currentValues = property.GetValue(objSource, null) as Array;
                        if (null == currentValues)
                        {
                            property.SetValue(objTarget, null, null);
                        }
                        else
                        {
                            var cloneObj = Activator.CreateInstance(property.PropertyType, currentValues.Length) as Array;
                            var arrayList = new ArrayList();
                            for (var i = 0; i < currentValues.Length; i++)
                            {
                                var currentType = currentValues.GetValue(i).GetType();
                                if (currentType.IsPrimitive || currentType.IsEnum || currentType == typeof(string))
                                {
                                    arrayList.Add(currentValues.GetValue(i));
                                }
                                else
                                {
                                    //Reference type
                                    var copyObj = CloneObject(currentValues.GetValue(i));
                                    arrayList.Add(copyObj);
                                }
                            }
                            arrayList.CopyTo(cloneObj, 0);
                            property.SetValue(objTarget, cloneObj, null);
                        }

  对于基类是Array类型的,我们这里使用一个ArrayList来暂存我们复制的对象,最后我们再通过ArrayList的CopyTo方法来将复制的对象赋值到objTarget中去,在对每一个值进行赋值的时候我们同样需要判断每一个值的类型是简单类型还是复杂类型,如果是简单类型直接添加到ArrayList里面,如果是复杂类型则再次调用CloneObject方法进行迭代,进行下一个完整过程,至此Array的过程也分析完毕。

  也许看了这个代码后对第一层循环最后一个else有些疑问,这个主要是一个对象中是另外一个自定义的对象,这个对象是引用类型但是是自定义的class类型,所以这个不符合上面的每一个分支,这个也是必须考虑的一个大类,这个时候复制的时候就要直接进行下一次迭代循环了,到了这里基本上所涉及到的主要类型都考虑到了,写这段代码的核心就是充分理解一次迭代要处理的过程,当然这个要考虑的地方还是比较多的,在写代码的时候还是需要特别注意。

测试

  代码写好了我们需要写一个测试的TestCloneSeed的测试数据,当然这里也只是测试了一些常用的类型,这个种子数据可以考虑更加复杂从而验证这个CloneObject的方法正确性,我们来看看这个测试的用例。 

using System;
using System.Collections.Generic;

namespace DeepCopyConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var testData = new TestCloneSeed
            {
                Age = 18,
                Name = "Jack",
                GoodsEx = new Dictionary<string, string> { { "A", "1" }, { "B", "2" } },
                Goods = new List<string>() { "Chicken", "Milk", "Rice", },
                Season = SeasonEnum.Winter,
                Numbers = new int[] { 1, 2, 3, 4, 5 },
                Children = new System.Collections.ObjectModel.ObservableCollection<TestCloneSeed>()
                {
                    new TestCloneSeed
                    {
                        Age = 8,
                        Name = "York",
                        Goods = new List<string> { "Oil", "Milk", "Water" },
                        Season = SeasonEnum.Summer,
                        Numbers = new int[5] { 11, 12, 13, 14, 15 },
                    }
                }
            };

            var clone = CloneUtil.CloneObject(testData);

            Console.ReadKey();
        }
    }
}

  这个里面用到的TestCloneSeed代码如下,可以根据自己的需要进行扩展。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace DeepCopyConsoleApp
{
    public enum SeasonEnum
    {
        Spring,
        Summer,
        Autumn,
        Winter
    }

    [Serializable]
    public class TestCloneSeed
    {
        public int Age { get; set; }

        public string Name { get; set; }

        public Dictionary<string, string> GoodsEx { get; set; }

        public List<string> Goods { get; set; }

        public SeasonEnum Season { get; set; }

        public int[] Numbers { get; set; }

        public ObservableCollection<TestCloneSeed> Children { get; set; }
    }
}

总结

  这篇文章的主要目的在于分享一个完全基于反射的对一个对象进行深度拷贝的方法,当然要想理解好这个必须充分理解C#中的类型系统,其次要从框架的角度思考如何去实现这个功能,因为我们实现的这个方法最终不能假设外部到底怎么传入,所以只能通过反射这种方法去逐一实现,通过这篇文章也能更加深入理解迭代的思想,这个都是在开发过程中需要去反复考虑的。

免责声明:文章转载自《C# 通过反射实现复杂对象的深拷贝(附源码)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇初识安卓之利用Handler进行线程间数据传递Jmeter入门(5)- jmeter取样器的HTTP请求下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

lua的模式匹配

模式: 字符类:(character classes) . all characters %a letters %c control characters %d digits %l lower -case letters %p punctuation characters %s space characters %u upper-case letters...

net core 模型绑定与之前版本的不同-FromBody 必须对应Json格式

之前有一个用于七牛上传图片的Callback Url的WebAPI (之前是用.net4.0,运行正常) 代码如下: //七牛CallBack地址,CallbackBody内容name=upload/member/1.jpg&hash=Fn6qeQi4VDLQ347NiRm-RlQx_4O2 public objec...

C# oracle 参数传递的多种方式 留着复习

ORA-01036 非法的变量名/编号,(解决) 博客分类:oracle SQL 下边的代码就会造成 ORA-01036 非法的变量名/编号 C#代码 cmd.CommandText="SELECT*FROMkk.kkyhWHEREid=@comboBox1andpassword=@textBox1"; cmd.Parameters.Add(...

MyBatis基础总结

1.1什么是MyBatis MyBatis(前身是iBatis)是一个支持普通SQL查询、存储过程以及高级映射的持久层框架, 它消除了几乎所有的JDBC代码和参数的手动设置以及对结果集的检索,并使用简单的XML或注解进行配置和原始映射, 用以将接口和Java的POJO(Plain Old Java Object,普通Java对象)映射成数据库中的记录,使得...

delphi 7 mdi子窗体。。。无法更改以命令对象为源的记录集对象的 ActiveConnection 属性。

问题是这样的 我做了一个小程序 把 adoconnection放到了主窗体  连接的是access数据库; 新建了一个子窗体继承自FBase  新建了一个pubulic方法 qrySearch 实现了打开表; formCreate调用了qrySearch方法 ; public procedure qrySearch(cLiuShui: stri...

RSA加密和数字签名在Java中常见应用【原创】

相关术语解释: RSA,参考: https://en.wikipedia.org/wiki/RSA_(cryptosystem) 非对称加密算法 ,参考:https://baike.baidu.com/item/%E9%9D%9E%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95/1208...