C# 通過反射實現復雜對象的深拷貝(附源碼)


背景

  在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#中的類型系統,其次要從框架的角度思考如何去實現這個功能,因為我們實現的這個方法最終不能假設外部到底怎么傳入,所以只能通過反射這種方法去逐一實現,通過這篇文章也能更加深入理解迭代的思想,這個都是在開發過程中需要去反復考慮的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM