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

背景

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

过程

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

using Systeusing 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 if (objPropertyValue.GetType().IsPrimitive || objPropertyValue.GetType().IsEnum || objPropertyValue.GetType() == typeof(string))
                    {
                        property.SetValue(objTarget, objPropertyValue, 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#中的类型系统,其次要从框架的角度思考如何去实现这个功能,因为我们实现的这个方法最终不能假设外部到底怎么传入,所以只能通过反射这种方法去逐一实现,通过这篇文章也能更加深入理解迭代的思想,这个都是在开发过程中需要去反复考虑的。

posted @ 2021-08-27 15:25  Hello——寻梦者!  阅读(2187)  评论(8编辑  收藏  举报