真のディープコピーが欲しい。Java ではこれは簡単でしたが、C# ではどのように行うのでしょうか?
10 に答える
重要な注意点
BinaryFormatterは廃止され、2023 年 11 月以降は .NET で使用できなくなります。BinaryFormatter 廃止戦略を参照してください。
これにはいくつかの異なるアプローチを見てきましたが、私は一般的なユーティリティ メソッドを使用しています。
public static T DeepClone<T>(this T obj)
{
using (var ms = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(ms, obj);
ms.Position = 0;
return (T) formatter.Deserialize(ms);
}
}
ノート:
これが機能するには、クラスが as としてマークされている必要があり
[Serializable]
ます。ソース ファイルには、次のコードが含まれている必要があります。
using System.Runtime.Serialization.Formatters.Binary; using System.IO;
再帰的な"MemberwiseClone"に基づいて、ディープ オブジェクト コピー拡張メソッドを作成しました。これは高速 ( BinaryFormatterよりも3 倍高速) で、あらゆるオブジェクトで動作します。デフォルトのコンストラクターやシリアライズ可能な属性は必要ありません。
ソースコード:
using System.Collections.Generic;
using System.Reflection;
using System.ArrayExtensions;
namespace System
{
public static class ObjectExtensions
{
private static readonly MethodInfo CloneMethod = typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance);
public static bool IsPrimitive(this Type type)
{
if (type == typeof(String)) return true;
return (type.IsValueType & type.IsPrimitive);
}
public static Object Copy(this Object originalObject)
{
return InternalCopy(originalObject, new Dictionary<Object, Object>(new ReferenceEqualityComparer()));
}
private static Object InternalCopy(Object originalObject, IDictionary<Object, Object> visited)
{
if (originalObject == null) return null;
var typeToReflect = originalObject.GetType();
if (IsPrimitive(typeToReflect)) return originalObject;
if (visited.ContainsKey(originalObject)) return visited[originalObject];
if (typeof(Delegate).IsAssignableFrom(typeToReflect)) return null;
var cloneObject = CloneMethod.Invoke(originalObject, null);
if (typeToReflect.IsArray)
{
var arrayType = typeToReflect.GetElementType();
if (IsPrimitive(arrayType) == false)
{
Array clonedArray = (Array)cloneObject;
clonedArray.ForEach((array, indices) => array.SetValue(InternalCopy(clonedArray.GetValue(indices), visited), indices));
}
}
visited.Add(originalObject, cloneObject);
CopyFields(originalObject, visited, cloneObject, typeToReflect);
RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect);
return cloneObject;
}
private static void RecursiveCopyBaseTypePrivateFields(object originalObject, IDictionary<object, object> visited, object cloneObject, Type typeToReflect)
{
if (typeToReflect.BaseType != null)
{
RecursiveCopyBaseTypePrivateFields(originalObject, visited, cloneObject, typeToReflect.BaseType);
CopyFields(originalObject, visited, cloneObject, typeToReflect.BaseType, BindingFlags.Instance | BindingFlags.NonPublic, info => info.IsPrivate);
}
}
private static void CopyFields(object originalObject, IDictionary<object, object> visited, object cloneObject, Type typeToReflect, BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy, Func<FieldInfo, bool> filter = null)
{
foreach (FieldInfo fieldInfo in typeToReflect.GetFields(bindingFlags))
{
if (filter != null && filter(fieldInfo) == false) continue;
if (IsPrimitive(fieldInfo.FieldType)) continue;
var originalFieldValue = fieldInfo.GetValue(originalObject);
var clonedFieldValue = InternalCopy(originalFieldValue, visited);
fieldInfo.SetValue(cloneObject, clonedFieldValue);
}
}
public static T Copy<T>(this T original)
{
return (T)Copy((Object)original);
}
}
public class ReferenceEqualityComparer : EqualityComparer<Object>
{
public override bool Equals(object x, object y)
{
return ReferenceEquals(x, y);
}
public override int GetHashCode(object obj)
{
if (obj == null) return 0;
return obj.GetHashCode();
}
}
namespace ArrayExtensions
{
public static class ArrayExtensions
{
public static void ForEach(this Array array, Action<Array, int[]> action)
{
if (array.LongLength == 0) return;
ArrayTraverse walker = new ArrayTraverse(array);
do action(array, walker.Position);
while (walker.Step());
}
}
internal class ArrayTraverse
{
public int[] Position;
private int[] maxLengths;
public ArrayTraverse(Array array)
{
maxLengths = new int[array.Rank];
for (int i = 0; i < array.Rank; ++i)
{
maxLengths[i] = array.GetLength(i) - 1;
}
Position = new int[array.Rank];
}
public bool Step()
{
for (int i = 0; i < Position.Length; ++i)
{
if (Position[i] < maxLengths[i])
{
Position[i]++;
for (int j = 0; j < i; j++)
{
Position[j] = 0;
}
return true;
}
}
return false;
}
}
}
}
Kilhofferのソリューションに基づいて構築...
C# 3.0 では、次のように拡張メソッドを作成できます。
public static class ExtensionMethods
{
// Deep clone
public static T DeepClone<T>(this T a)
{
using (MemoryStream stream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, a);
stream.Position = 0;
return (T) formatter.Deserialize(stream);
}
}
}
[Serializable] としてマークされたクラスを DeepClone メソッドで拡張します
MyClass copy = obj.DeepClone();
Nested MemberwiseCloneを使用して、ディープコピーを実行できます。値の構造体をコピーするのとほぼ同じ速度であり、(a)リフレクションまたは(b)シリアル化(このページの他の回答で説明されている)よりも1桁高速です。
ディープコピーにNestedMemberwiseCloneを使用する場合は、クラス内のネストされたレベルごとに手動でShallowCopyを実装し、すべてのShallowCopyメソッドを呼び出して完全なクローンを作成するDeepCopyを実装する必要があることに注意してください。これは簡単です。合計で数行だけです。以下のデモコードを参照してください。
これは、相対的なパフォーマンスの違いを示すコードの出力です(深くネストされたMemberwiseCopyの場合は4.77秒、シリアル化の場合は39.93秒)。ネストされたMemberwiseCopyの使用は、構造体のコピーとほぼ同じ速度であり、構造体のコピーは、.NETが可能な理論上の最大速度にかなり近く、おそらくCまたはC ++の同じものの速度にかなり近いです(ただし、この主張を確認するには、同等のベンチマークを実行する必要があります)。
Demo of shallow and deep copy, using classes and MemberwiseClone:
Create Bob
Bob.Age=30, Bob.Purchase.Description=Lamborghini
Clone Bob >> BobsSon
Adjust BobsSon details
BobsSon.Age=2, BobsSon.Purchase.Description=Toy car
Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:
Bob.Age=30, Bob.Purchase.Description=Lamborghini
Elapsed time: 00:00:04.7795670,30000000
Demo of shallow and deep copy, using structs and value copying:
Create Bob
Bob.Age=30, Bob.Purchase.Description=Lamborghini
Clone Bob >> BobsSon
Adjust BobsSon details:
BobsSon.Age=2, BobsSon.Purchase.Description=Toy car
Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:
Bob.Age=30, Bob.Purchase.Description=Lamborghini
Elapsed time: 00:00:01.0875454,30000000
Demo of deep copy, using class and serialize/deserialize:
Elapsed time: 00:00:39.9339425,30000000
MemberwiseCopyを使用してディープコピーを実行する方法を理解するために、デモプロジェクトを次に示します。
// Nested MemberwiseClone example.
// Added to demo how to deep copy a reference class.
[Serializable] // Not required if using MemberwiseClone, only used for speed comparison using serialization.
public class Person
{
public Person(int age, string description)
{
this.Age = age;
this.Purchase.Description = description;
}
[Serializable] // Not required if using MemberwiseClone
public class PurchaseType
{
public string Description;
public PurchaseType ShallowCopy()
{
return (PurchaseType)this.MemberwiseClone();
}
}
public PurchaseType Purchase = new PurchaseType();
public int Age;
// Add this if using nested MemberwiseClone.
// This is a class, which is a reference type, so cloning is more difficult.
public Person ShallowCopy()
{
return (Person)this.MemberwiseClone();
}
// Add this if using nested MemberwiseClone.
// This is a class, which is a reference type, so cloning is more difficult.
public Person DeepCopy()
{
// Clone the root ...
Person other = (Person) this.MemberwiseClone();
// ... then clone the nested class.
other.Purchase = this.Purchase.ShallowCopy();
return other;
}
}
// Added to demo how to copy a value struct (this is easy - a deep copy happens by default)
public struct PersonStruct
{
public PersonStruct(int age, string description)
{
this.Age = age;
this.Purchase.Description = description;
}
public struct PurchaseType
{
public string Description;
}
public PurchaseType Purchase;
public int Age;
// This is a struct, which is a value type, so everything is a clone by default.
public PersonStruct ShallowCopy()
{
return (PersonStruct)this;
}
// This is a struct, which is a value type, so everything is a clone by default.
public PersonStruct DeepCopy()
{
return (PersonStruct)this;
}
}
// Added only for a speed comparison.
public class MyDeepCopy
{
public static T DeepCopy<T>(T obj)
{
object result = null;
using (var ms = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(ms, obj);
ms.Position = 0;
result = (T)formatter.Deserialize(ms);
ms.Close();
}
return (T)result;
}
}
次に、メインからデモを呼び出します。
void MyMain(string[] args)
{
{
Console.Write("Demo of shallow and deep copy, using classes and MemberwiseCopy:\n");
var Bob = new Person(30, "Lamborghini");
Console.Write(" Create Bob\n");
Console.Write(" Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);
Console.Write(" Clone Bob >> BobsSon\n");
var BobsSon = Bob.DeepCopy();
Console.Write(" Adjust BobsSon details\n");
BobsSon.Age = 2;
BobsSon.Purchase.Description = "Toy car";
Console.Write(" BobsSon.Age={0}, BobsSon.Purchase.Description={1}\n", BobsSon.Age, BobsSon.Purchase.Description);
Console.Write(" Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:\n");
Console.Write(" Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);
Debug.Assert(Bob.Age == 30);
Debug.Assert(Bob.Purchase.Description == "Lamborghini");
var sw = new Stopwatch();
sw.Start();
int total = 0;
for (int i = 0; i < 100000; i++)
{
var n = Bob.DeepCopy();
total += n.Age;
}
Console.Write(" Elapsed time: {0},{1}\n", sw.Elapsed, total);
}
{
Console.Write("Demo of shallow and deep copy, using structs:\n");
var Bob = new PersonStruct(30, "Lamborghini");
Console.Write(" Create Bob\n");
Console.Write(" Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);
Console.Write(" Clone Bob >> BobsSon\n");
var BobsSon = Bob.DeepCopy();
Console.Write(" Adjust BobsSon details:\n");
BobsSon.Age = 2;
BobsSon.Purchase.Description = "Toy car";
Console.Write(" BobsSon.Age={0}, BobsSon.Purchase.Description={1}\n", BobsSon.Age, BobsSon.Purchase.Description);
Console.Write(" Proof of deep copy: If BobsSon is a true clone, then adjusting BobsSon details will not affect Bob:\n");
Console.Write(" Bob.Age={0}, Bob.Purchase.Description={1}\n", Bob.Age, Bob.Purchase.Description);
Debug.Assert(Bob.Age == 30);
Debug.Assert(Bob.Purchase.Description == "Lamborghini");
var sw = new Stopwatch();
sw.Start();
int total = 0;
for (int i = 0; i < 100000; i++)
{
var n = Bob.DeepCopy();
total += n.Age;
}
Console.Write(" Elapsed time: {0},{1}\n", sw.Elapsed, total);
}
{
Console.Write("Demo of deep copy, using class and serialize/deserialize:\n");
int total = 0;
var sw = new Stopwatch();
sw.Start();
var Bob = new Person(30, "Lamborghini");
for (int i = 0; i < 100000; i++)
{
var BobsSon = MyDeepCopy.DeepCopy<Person>(Bob);
total += BobsSon.Age;
}
Console.Write(" Elapsed time: {0},{1}\n", sw.Elapsed, total);
}
Console.ReadKey();
}
繰り返しになりますが、ディープコピーにNested MemberwiseCloneを使用する場合は、クラス内のネストされたレベルごとに手動でShallowCopyを実装し、すべてのShallowCopyメソッドを呼び出して完全なクローンを作成するDeepCopyを実装する必要があることに注意してください。これは簡単です。合計で数行だけです。上記のデモコードを参照してください。
オブジェクトのクローンを作成する場合、「構造体」と「クラス」には大きな違いがあることに注意してください。
- 「構造体」がある場合、それは値型なので、コピーするだけで、内容が複製されます。
- 「クラス」がある場合、それは参照型であるため、それをコピーする場合は、ポインターをそのクラスにコピーするだけです。真のクローンを作成するには、より創造的であり、メモリ内に元のオブジェクトの別のコピーを作成するメソッドを使用する必要があります。
- オブジェクトのクローンを誤って作成すると、特定が非常に難しいバグが発生する可能性があります。本番コードでは、チェックサムを実装して、オブジェクトが適切に複製されており、オブジェクトへの別の参照によって破損していないことを再確認する傾向があります。このチェックサムは、リリースモードでオフに切り替えることができます。
- このメソッドは非常に便利だと思います。多くの場合、オブジェクト全体ではなく、オブジェクトの一部のみを複製する必要があります。また、オブジェクトを変更してから、変更されたコピーをキューにフィードするユースケースでも不可欠です。
アップデート
リフレクションを使用してオブジェクトグラフを再帰的にウォークスルーし、ディープコピーを実行することはおそらく可能です。WCFはこの手法を使用して、すべての子を含むオブジェクトをシリアル化します。秘訣は、すべての子オブジェクトに、検出可能にする属性で注釈を付けることです。ただし、パフォーマンス上の利点が失われる可能性があります。
アップデート
独立した速度テストについての引用(以下のコメントを参照):
Neilのシリアル化/逆シリアル化拡張メソッド、ContangoのNested MemberwiseClone、Alex Burtsevのリフレクションベースの拡張メソッド、およびAutoMapperをそれぞれ100万回使用して、独自の速度テストを実行しました。シリアル化-逆シリアル化は最も遅く、15.7秒かかりました。次にAutoMapperが登場し、10.1秒かかりました。2.4秒かかった反射ベースの方法の方がはるかに高速でした。群を抜いて最速だったのはNestedMemberwiseCloneで、0.1秒かかりました。パフォーマンスと、クローンを作成するために各クラスにコードを追加する手間がかかります。パフォーマンスが問題にならない場合は、AlexBurtsevの方法を使用してください。– Simon Tewsi
BinaryFormatter のアプローチは比較的遅いと思います (これには驚きました!)。一部のオブジェクトが ProtoBuf の要件を満たしていれば、ProtoBuf .NET を使用できる場合があります。ProtoBuf 入門ページ ( http://code.google.com/p/protobuf-net/wiki/GettingStarted ) から:
サポートされるタイプに関する注意事項:
次のようなカスタム クラス:
- データ契約としてマークされています
- パラメータなしのコンストラクタを持つ
- Silverlight の場合: パブリックです
- 多くの一般的なプリミティブなど
- 1次元配列: T[]
- List<T> / IList<T>
- Dictionary<TKey, TValue> / IDictionary<TKey, TValue>
- IEnumerable<T> を実装し、Add(T) メソッドを持つ任意の型
このコードは、選出されたメンバーの周りで型が変更可能であることを前提としています。したがって、カスタム構造体は不変である必要があるため、サポートされていません。
クラスがこれらの要件を満たしている場合は、次を試すことができます。
public static void deepCopy<T>(ref T object2Copy, ref T objectCopy)
{
using (var stream = new MemoryStream())
{
Serializer.Serialize(stream, object2Copy);
stream.Position = 0;
objectCopy = Serializer.Deserialize<T>(stream);
}
}
これは確かに非常に高速です...
編集:
これを変更するための作業コードを次に示します (.NET 4.6 でテスト済み)。System.Xml.Serialization と System.IO を使用します。クラスをシリアライズ可能としてマークする必要はありません。
public void DeepCopy<T>(ref T object2Copy, ref T objectCopy)
{
using (var stream = new MemoryStream())
{
var serializer = new XS.XmlSerializer(typeof(T));
serializer.Serialize(stream, object2Copy);
stream.Position = 0;
objectCopy = (T)serializer.Deserialize(stream);
}
}
これを試すことができます
public static object DeepCopy(object obj)
{
if (obj == null)
return null;
Type type = obj.GetType();
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(DeepCopy(array.GetValue(i)), i);
}
return Convert.ChangeType(copied, obj.GetType());
}
else if (type.IsClass)
{
object toret = Activator.CreateInstance(obj.GetType());
FieldInfo[] fields = type.GetFields(BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Instance);
foreach (FieldInfo field in fields)
{
object fieldValue = field.GetValue(obj);
if (fieldValue == null)
continue;
field.SetValue(toret, DeepCopy(fieldValue));
}
return toret;
}
else
throw new ArgumentException("Unknown type");
}
コード プロジェクトに関するDetoX83 の記事に感謝します。
最良の方法は次のとおりです。
public interface IDeepClonable<T> where T : class
{
T DeepClone();
}
public class MyObj : IDeepClonable<MyObj>
{
public MyObj Clone()
{
var myObj = new MyObj();
myObj._field1 = _field1;//value type
myObj._field2 = _field2;//value type
myObj._field3 = _field3;//value type
if (_child != null)
{
myObj._child = _child.DeepClone(); //reference type .DeepClone() that does the same
}
int len = _array.Length;
myObj._array = new MyObj[len]; // array / collection
for (int i = 0; i < len; i++)
{
myObj._array[i] = _array[i];
}
return myObj;
}
private bool _field1;
public bool Field1
{
get { return _field1; }
set { _field1 = value; }
}
private int _field2;
public int Property2
{
get { return _field2; }
set { _field2 = value; }
}
private string _field3;
public string Property3
{
get { return _field3; }
set { _field3 = value; }
}
private MyObj _child;
private MyObj Child
{
get { return _child; }
set { _child = value; }
}
private MyObj[] _array = new MyObj[4];
}
浅いコピーだけが必要な場合もあります。その場合はObject.MemberWiseClone()
.
MemberWiseClone()
ドキュメントには、ディープ コピーの戦略に関する適切な推奨事項があります。
http://msdn.microsoft.com/en-us/library/system.object.memberwiseclone.aspx
public static object CopyObject(object input)
{
if (input != null)
{
object result = Activator.CreateInstance(input.GetType());
foreach (FieldInfo field in input.GetType().GetFields(Consts.AppConsts.FullBindingList))
{
if (field.FieldType.GetInterface("IList", false) == null)
{
field.SetValue(result, field.GetValue(input));
}
else
{
IList listObject = (IList)field.GetValue(result);
if (listObject != null)
{
foreach (object item in ((IList)field.GetValue(input)))
{
listObject.Add(CopyObject(item));
}
}
}
}
return result;
}
else
{
return null;
}
}
この方法は、ANDよりも数倍高速であり、属性BinarySerialization
は必要ありません。[Serializable]