c#中深拷貝和淺拷貝(表達式樹)

c#的object類提供了一個淺拷貝的方法MemberwiseClone,該方法克隆的是原始對象的獨立副本,但是這個副本和原始對象會共同指向同一個引用類型屬性的引用。比如官方提供了一個例子:

public class IdInfo
{
    public int IdNumber;
    
    public IdInfo(int IdNumber)
    {
        this.IdNumber = IdNumber;
    }
}

public class Person 
{
    public int Age;
    public string Name;
    public IdInfo IdInfo;

    public Person ShallowCopy()
    {
       return (Person) this.MemberwiseClone();
    }

    public Person DeepCopy()
    {
       Person other = (Person) this.MemberwiseClone();
       other.IdInfo = new IdInfo(IdInfo.IdNumber);
       other.Name = String.Copy(Name);
       return other;
    }
}

如果你對Person類的某個實例對象person1使用MemberwiseClone方法進行淺拷貝得到person2,那麼person1和person2的IdInfo指向的是同一個引用。

MemberwiseClone 方法創建一個淺表副本,方法是創建一個新的對象,然後將當前對象的非靜態字段複製到新的對象。 如果字段是值類型,則執行字段的逐位副本。 如果字段是引用類型,則會複製引用,但不會複製引用的對象;因此,原始對象及其複本引用相同的對象。

同時,官方文檔也提供了四種深拷貝的方案:

  • 調用要複製的對象的類構造函數,以創建具有從第一個對象獲取的屬性值的第二個對象。 這假設對象的值由其類構造函數完全定義。

  • 調用 MemberwiseClone 方法來創建對象的淺表副本,然後將原始對象的屬性深拷貝到新對象裏面。

  • 序列化要深層複製的對象,然後將序列化的數據還原到其他對象變量。

  • 使用帶有遞歸的反射來執行深層複製操作。

第一種和第二種方法,本質都是根據類的結構寫特定的拷貝方法。第三種和第四種方法則相對通用許多。

1.反射方式:

        public T DeepCopyByReflection<T>(T obj)
        {
            Type type = obj.GetType();

            if (obj is string || type.IsValueType) return obj;

            if (type.IsArray)
            {
                Type elementType = Type.GetType(type.FullName.Replace("[]", string.Empty));
                var array = obj as Array;
                Array copied = Array.CreateInstance(elementType, array.Length);
                for (int i = 0; i < array.Length; i++)
                {
                    copied.SetValue(DeepCopyByReflection(array.GetValue(i)), i);
                }

                return (T)Convert.ChangeType(copied, obj.GetType());
            }

            object retval = Activator.CreateInstance(obj.GetType());

            var fields = obj.GetType().GetFields(BindingFlags.Public | 
BindingFlags.NonPublic | BindingFlags.Instance);
            
            foreach (var field in fields)
            {
                var val = field.GetValue(obj);
                if (val == null)
                    continue;
                field.SetValue(retval, DeepCopyByReflection(val));
            }
            return (T)retval;
        }

上面的代碼是一種通過反射來進行深拷貝的方法,它的本質是遞歸一個類,把所有的字段通過反射的方式重新賦值給新的類。這裏需要注意的是要使用GetFields而不是GetProperties方法來獲取目標類裏面的所有的字段,因爲GetProperties方法只能獲取到屬性,對於私有的字段,無法獲取到,這樣,深拷貝的時候就會漏掉這個私有字段。

它會調用其中一個構造方法,所以當你使用這個方法的時候,要特別注意你的那個無參構造方法裏面的功能邏輯代碼,要考慮執行了這個構造方法之後,會產生哪些後果。

另外這個時候如果你的類裏面包含一個Action,由於這個Action沒有無參構造方法,你再用這個克隆方法就會變得很麻煩。實際上不止Action,只要被克隆的對象裏面包含一個無參構造方法的實體對象,那麼用這種反射的方式克隆就會變得很麻煩,因爲需要在實例化的時候給構造方法傳遞參數。

另外,針對循環引用的問題,這個方法解決起來也會很棘手。

2.序列化方式:

        // 利用XML序列化和反序列化實現
        public T DeepCopyWithXmlSerializer<T>(T obj)
        {
            object retval;
            using (MemoryStream ms = new MemoryStream())
            {
                XmlSerializer xml = new XmlSerializer(typeof(T));
                xml.Serialize(ms, obj);
                ms.Seek(0, SeekOrigin.Begin);
                retval = xml.Deserialize(ms);
                ms.Close();
            }

            return (T)retval;
        }

        // 利用二進制序列化和反序列實現,支持循環引用
        public T DeepCopyWithBinarySerialize<T>(T obj)
        {
            object retval;
            using (MemoryStream ms = new MemoryStream())
            {
                BinaryFormatter bf = new BinaryFormatter();
                // 序列化成流
                bf.Serialize(ms, obj);
                ms.Seek(0, SeekOrigin.Begin);
                // 反序列化成對象
                retval = bf.Deserialize(ms);
                ms.Close();
            }
            
            return (T)retval;
        }

        // 利用DataContractSerializer序列化和反序列化實現
        public T DeepCopy<T>(T obj)
        {
            object retval;
            using (MemoryStream ms = new MemoryStream())
            {
                DataContractSerializer ser = new DataContractSerializer(typeof(T));
                ser.WriteObject(ms, obj);
                ms.Seek(0, SeekOrigin.Begin);
                retval = ser.ReadObject(ms);
                ms.Close();
            }
            return (T)retval;
        }

序列化相對來說是一種不錯的方式,因爲它解決了無參構造方法的克隆問題,同時它也不會導致在克隆的時候調用構造函數,因爲克隆多調用一次構造函數,其實是我們不太希望的結果。

3.表達樹方式(Expression Trees):

這種方式的本質是動態生成一段克隆代碼,然後把被克隆對象的屬性逐個拷貝給新的對象。github上有一個開源項目,採用的就是這種方式。https://github.com/MarcinJuraszek/CloneExtensions

這個項目會根據你要拷貝的對象,利用表達樹自動生成深拷貝的代碼塊,幫你拷貝一個對象。它目前的問題是無法深拷貝私有變量。但是它解決了循環引用的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章