我想做一些事情,比如:
MyObject myObj = GetMyObj(); // Create and fill a new object
MyObject newObj = myObj.Clone();
然后对未反映在原始对象中的新对象进行更改。
我通常不需要这个功能,所以当有必要时,我会创建一个新对象,然后单独复制每个属性,但这总是让我觉得有更好或更优雅的方法来处理这种情况。
如何克隆或深度复制对象,以便在原始对象中不反映任何更改的情况下修改克隆的对象?
我想做一些事情,比如:
MyObject myObj = GetMyObj(); // Create and fill a new object
MyObject newObj = myObj.Clone();
然后对未反映在原始对象中的新对象进行更改。
我通常不需要这个功能,所以当有必要时,我会创建一个新对象,然后单独复制每个属性,但这总是让我觉得有更好或更优雅的方法来处理这种情况。
如何克隆或深度复制对象,以便在原始对象中不反映任何更改的情况下修改克隆的对象?
当前回答
通常,您实现ICloneable接口并自己实现克隆。C#对象有一个内置的MemberwiseColone方法,该方法执行浅层复制,可以帮助您处理所有原语。
对于深度复制,它无法知道如何自动执行。
其他回答
这将把一个对象的所有可读写财产复制到另一个对象。
public class PropertyCopy<TSource, TTarget>
where TSource: class, new()
where TTarget: class, new()
{
public static TTarget Copy(TSource src, TTarget trg, params string[] properties)
{
if (src==null) return trg;
if (trg == null) trg = new TTarget();
var fulllist = src.GetType().GetProperties().Where(c => c.CanWrite && c.CanRead).ToList();
if (properties != null && properties.Count() > 0)
fulllist = fulllist.Where(c => properties.Contains(c.Name)).ToList();
if (fulllist == null || fulllist.Count() == 0) return trg;
fulllist.ForEach(c =>
{
c.SetValue(trg, c.GetValue(src));
});
return trg;
}
}
这是你使用它的方式:
var cloned = Utils.PropertyCopy<TKTicket, TKTicket>.Copy(_tmp, dbsave,
"Creation",
"Description",
"IdTicketStatus",
"IdUserCreated",
"IdUserInCharge",
"IdUserRequested",
"IsUniqueTicketGenerated",
"LastEdit",
"Subject",
"UniqeTicketRequestId",
"Visibility");
或复制所有内容:
var cloned = Utils.PropertyCopy<TKTicket, TKTicket>.Copy(_tmp, dbsave);
使用IClonable接口可以花费多少精力是令人难以置信的,尤其是当您有大量的类层次结构时。MemberwiseColone的工作方式也很奇怪——它甚至不能完全克隆普通的List类型的结构。
当然,串行化最有趣的困境是串行化反向引用——例如,具有子-父关系的类层次结构。我怀疑二进制序列化程序能否在这种情况下帮助您。(最终将导致递归循环+堆栈溢出)。
不知怎么的,我喜欢这里提出的解决方案:如何在.NET(特别是C#)中对对象进行深度复制?
然而,它不支持Lists,并补充说,该支持还考虑到了重新养育子女的问题。对于我制定的仅为父项的规则,该字段或属性应命名为“parent”,则DeepClone将忽略它。您可能需要决定自己的反向引用规则——对于树层次结构,它可能是“左/右”等。。。
以下是包含测试代码的完整代码片段:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
namespace TestDeepClone
{
class Program
{
static void Main(string[] args)
{
A a = new A();
a.name = "main_A";
a.b_list.Add(new B(a) { name = "b1" });
a.b_list.Add(new B(a) { name = "b2" });
A a2 = (A)a.DeepClone();
a2.name = "second_A";
// Perform re-parenting manually after deep copy.
foreach( var b in a2.b_list )
b.parent = a2;
Debug.WriteLine("ok");
}
}
public class A
{
public String name = "one";
public List<String> list = new List<string>();
public List<String> null_list;
public List<B> b_list = new List<B>();
private int private_pleaseCopyMeAsWell = 5;
public override string ToString()
{
return "A(" + name + ")";
}
}
public class B
{
public B() { }
public B(A _parent) { parent = _parent; }
public A parent;
public String name = "two";
}
public static class ReflectionEx
{
public static Type GetUnderlyingType(this MemberInfo member)
{
Type type;
switch (member.MemberType)
{
case MemberTypes.Field:
type = ((FieldInfo)member).FieldType;
break;
case MemberTypes.Property:
type = ((PropertyInfo)member).PropertyType;
break;
case MemberTypes.Event:
type = ((EventInfo)member).EventHandlerType;
break;
default:
throw new ArgumentException("member must be if type FieldInfo, PropertyInfo or EventInfo", "member");
}
return Nullable.GetUnderlyingType(type) ?? type;
}
/// <summary>
/// Gets fields and properties into one array.
/// Order of properties / fields will be preserved in order of appearance in class / struct. (MetadataToken is used for sorting such cases)
/// </summary>
/// <param name="type">Type from which to get</param>
/// <returns>array of fields and properties</returns>
public static MemberInfo[] GetFieldsAndProperties(this Type type)
{
List<MemberInfo> fps = new List<MemberInfo>();
fps.AddRange(type.GetFields());
fps.AddRange(type.GetProperties());
fps = fps.OrderBy(x => x.MetadataToken).ToList();
return fps.ToArray();
}
public static object GetValue(this MemberInfo member, object target)
{
if (member is PropertyInfo)
{
return (member as PropertyInfo).GetValue(target, null);
}
else if (member is FieldInfo)
{
return (member as FieldInfo).GetValue(target);
}
else
{
throw new Exception("member must be either PropertyInfo or FieldInfo");
}
}
public static void SetValue(this MemberInfo member, object target, object value)
{
if (member is PropertyInfo)
{
(member as PropertyInfo).SetValue(target, value, null);
}
else if (member is FieldInfo)
{
(member as FieldInfo).SetValue(target, value);
}
else
{
throw new Exception("destinationMember must be either PropertyInfo or FieldInfo");
}
}
/// <summary>
/// Deep clones specific object.
/// Analogue can be found here: https://stackoverflow.com/questions/129389/how-do-you-do-a-deep-copy-an-object-in-net-c-specifically
/// This is now improved version (list support added)
/// </summary>
/// <param name="obj">object to be cloned</param>
/// <returns>full copy of object.</returns>
public static object DeepClone(this object obj)
{
if (obj == null)
return null;
Type type = obj.GetType();
if (obj is IList)
{
IList list = ((IList)obj);
IList newlist = (IList)Activator.CreateInstance(obj.GetType(), list.Count);
foreach (object elem in list)
newlist.Add(DeepClone(elem));
return newlist;
} //if
if (type.IsValueType || type == typeof(string))
{
return obj;
}
else 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(DeepClone(array.GetValue(i)), i);
return Convert.ChangeType(copied, obj.GetType());
}
else if (type.IsClass)
{
object toret = Activator.CreateInstance(obj.GetType());
MemberInfo[] fields = type.GetFieldsAndProperties();
foreach (MemberInfo field in fields)
{
// Don't clone parent back-reference classes. (Using special kind of naming 'parent'
// to indicate child's parent class.
if (field.Name == "parent")
{
continue;
}
object fieldValue = field.GetValue(obj);
if (fieldValue == null)
continue;
field.SetValue(toret, DeepClone(fieldValue));
}
return toret;
}
else
{
// Don't know that type, don't know how to clone it.
if (Debugger.IsAttached)
Debugger.Break();
return null;
}
} //DeepClone
}
}
对@Konrad和@craastad的添加,使用内置的System.Text.Json for.NET>5
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-5-0
方法:
public static T Clone<T>(T source)
{
var serialized = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(serialized);
}
扩展方法:
public static class SystemExtension
{
public static T Clone<T>(this T source)
{
var serialized = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(serialized);
}
}
C#9.0引入了需要记录的with关键字(谢谢Mark Nading)。这应该允许非常简单的对象克隆(如果需要,还可以进行突变),只需要很少的样板,但只需要一个记录。
您似乎无法通过将类放入通用记录来克隆(按值)类;
using System;
public class Program
{
public class Example
{
public string A { get; set; }
}
public record ClonerRecord<T>(T a)
{
}
public static void Main()
{
var foo = new Example {A = "Hello World"};
var bar = (new ClonerRecord<Example>(foo) with {}).a;
foo.A = "Goodbye World :(";
Console.WriteLine(bar.A);
}
}
这写着“再见世界:()”-字符串是通过引用复制的(不需要)。https://dotnetfiddle.net/w3IJgG
(令人难以置信的是,上面的方法可以正确地用于结构!https://dotnetfiddle.net/469NJv)
但克隆一条记录似乎确实可以按值进行缩进和克隆。
using System;
public class Program
{
public record Example
{
public string A { get; set; }
}
public static void Main()
{
var foo = new Example {A = "Hello World"};
var bar = foo with {};
foo.A = "Goodbye World :(";
Console.WriteLine(bar.A);
}
}
这将返回“Hello World”,字符串是按值复制的!https://dotnetfiddle.net/MCHGEL
更多信息可以在博客文章中找到:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/with-expression
我更喜欢复制构造函数而不是克隆。意图更加明确。