對象屬性和字段拷貝的幾種方式
微軟提供了淺拷貝
- 對於值類型,修改拷貝的值不會影響源對象
- 對於引用類型,修改拷貝后的值會影響源對象,但string特殊,它會拷貝一個副本,互相不會影響
自己實現深拷貝,我了解到的有這幾種方法
- 硬核編碼,每一個屬性和字段都寫一遍賦值,這種方法運行速度最快
- 通過反射,最常見的方法,但每次都需要反射
- 通過序列化,需要給類加上[Serializable]標簽
- C# 快速高效率復制對象另一種方式 表達式樹
測試例子
例子代碼在文章未尾,這里先展示測試結果。
最開始創建對象的字段值為: Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:死亡騎士
1.原始值變化后,使用深淺兩種拷貝的結果
//原始值:Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:死亡騎士,
//修改原始值的Id和Name,Skin字段之后,輸出如下:
//原始值:Id:1005,Name:蘭陵王,Hp:3449,Prof:刺客,Skin:隱刃,
//淺拷貝:Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:隱刃,
//深拷貝:Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:死亡騎士
2.修改淺拷貝的值,再打印看看結果
//輸出:修改淺拷貝的Id,Name,Prof,Skin,輸出如下:
//原始值:Id:1005,Name:蘭陵王,Hp:3449,Prof:刺客,Skin:隱刃,
//淺拷貝:Id:1008,Name:李白,Hp:3449,Prof:刺客,Skin:鳳求凰,
//深拷貝:Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:死亡騎士
㳀拷貝
MemberwiseClone
Object.MemberwiseClone函數定義:
/// <summary>
/// 創建當前 <see cref="T:System.Object" /> 的淺表副本。
/// </summary>
/// <returns>
/// 當前 <see cref="T:System.Object" /> 的淺表副本。
/// </returns>
protected extern object MemberwiseClone();
結論
MemberwiseClone理論上滿足常見的需求,包括string這種特殊類型,拷貝后的副本與原始值是斷開聯系,修改不會相互影響。
反射對於List、Hashtable等復雜結構需要特殊處理
例子
[Serializable]
class XEngine : ICloneable
{
public object Clone()
{
return this.MemberwiseClone();
}
}
深拷貝
比較常見的就是通過反射對所有字段和屬性進行賦值,還可以通過序列化也是可以對所有字段和屬性賦值。
序列化
public XEngine DeepClone()
{
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(0, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as XEngine;
}
}
反射拷貝
反射所有的屬性和字段,進行賦值,但對於hashtable和list等復雜結構是不好處理的。
public void ReflectClone(object from, object to)
{
if (from == null || to == null)
{
Debug.LogError($"拷貝失敗,from is null:{from == null},to is null:{to == null}");
return;
}
var fromType = from.GetType();
var toType = to.GetType();
//拷貝屬性
var properties = fromType.GetProperties();
foreach (PropertyInfo prop in properties)
{
var toProp = toType.GetProperty(prop.Name);
if (toProp != null)
{
var val = prop.GetValue(from);
if (prop.PropertyType == toProp.PropertyType)
{
toProp.SetValue(to, val, null);
}
else if (prop.PropertyType.ToString().IndexOf("List") >= 0 || prop.PropertyType.ToString().IndexOf("Hashtable") >= 0)
{
Debug.LogError($"屬性:{prop.Name},不支持List和Hashtable的拷貝,請使用序列化");
}
}
}
//拷貝字段
var fields = fromType.GetFields();
foreach (FieldInfo field in fields)
{
var toField = toType.GetField(field.Name);
if (toField != null)
{
var val = field.GetValue(from);
if (field.FieldType == toField.FieldType)
{
toField.SetValue(to, val);
}
else if (field.FieldType.ToString().IndexOf("List") >= 0 || field.FieldType.ToString().IndexOf("Hashtable") >= 0)
{
Debug.LogError($"字段:{field.Name},不支持List和Hashtable的拷貝,請使用序列化");
}
}
}
}
在Unity中的例子
unity引擎版本:2019.3.7f1,完整代碼如下:
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
using Object = System.Object;
/// <summary>
/// Author:qingqing.zhao (569032731@qq.com)
/// Date:2021/5/18 10:54
/// Desc:在Unity中測試幾種對象拷貝的方法
/// 1.微軟提供的淺拷貝
/// 2.序列化
/// 3.反射拷貝
///結論:int,bool等值類型和string淺拷貝之后修改原始值不會影響clone值,但引用類型會影響
/// </summary>
public class CloneDemo : MonoBehaviour
{
private void Start()
{
#region 例子1
//測試修改一個只有基礎數據結構的類,結論:int和string淺拷貝之后修改源始值不會影響clone值
XCharacter role = new XCharacter() {Id = 1001, Name = "亞瑟", Hp = 3449, Prof = "戰士", Skin = new XSkin() {Name = "死亡騎士"}};
Debug.Log($"原始值:{role.ToString()}");
XCharacter simpleClone = role.Clone() as XCharacter;
XCharacter deepClone = role.DeepClone();
role.Id = 1005;
role.Name = "蘭陵王";
role.Prof = "刺客";
role.Skin.Name = "影刃";
Debug.Log($"修改原始值,原始值:{role.ToString()},淺拷貝:{simpleClone.ToString()},深拷貝:{deepClone.ToString()}");
//輸出:修改原始值,
//原始值:Id:1005,Name:蘭陵王,Hp:3449,Prof:刺客,Skin:影刃,
//淺拷貝:Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:影刃,
//深拷貝:Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:死亡騎士
simpleClone.Id = 1008;
simpleClone.Prof = "刺客";
simpleClone.Name = "李白";
Debug.Log($"修改淺拷貝的值,原始值:{role.ToString()},淺拷貝:{simpleClone.ToString()},深拷貝:{deepClone.ToString()}");
//輸出:修改淺拷貝的值,
//原始值:Id:1005,Name:蘭陵王,Hp:3449,Prof:刺客,Skin:影刃,
//淺拷貝:Id:1008,Name:李白,Hp:3449,Prof:刺客,Skin:影刃,
//深拷貝:Id:1001,Name:亞瑟,Hp:3449,Prof:戰士,Skin:死亡騎士
#endregion
#region 通過反射拷貝
XCharacter reflectClone = new XCharacter();
ReflectClone(role, reflectClone);
Debug.Log($"反射拷貝,原始值:{role.ToString()},反射拷貝:{reflectClone.ToString()}");
//輸出:反射拷貝,
//原始值:Id:1005,Name:蘭陵王,Hp:3449,Prof:刺客,Skin:影刃,
//反射拷貝:Id:1005,Name:蘭陵王,Hp:3449,Prof:刺客,Skin:影刃
#endregion
}
}
[Serializable]
class XCharacter : ICloneable
{
public int Id { get; set; }
public string Name { get; set; }
public int Hp;
public string Prof;
public XSkin Skin { get; set; }
public override string ToString()
{
return $"Id:{Id},Name:{Name},Hp:{Hp},Prof:{Prof},Skin:{Skin?.ToString()}";
}
public object Clone()
{
return this.MemberwiseClone();
}
public XCharacter DeepClone()
{
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(0, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as XCharacter;
}
}
}
[Serializable]
class XSkin
{
public string Name { get; set; }
public override string ToString()
{
return this.Name;
}
}