14

クラス階層のディープコピーをC#で実装したい

public Class ParentObj : ICloneable
{
    protected int   myA;
    public virtual Object Clone ()
        {
             ParentObj newObj = new ParentObj();
             newObj.myA = theObj.MyA;
             return newObj;
        }
}

public Class ChildObj : ParentObj
{
    protected int   myB;
    public override Object Clone ( )
        {
             Parent newObj = this.base.Clone();
             newObj.myB = theObj.MyB;

             return newObj;
        }
}

これは、子のクローンを作成する場合、親のみが新規作成された場合のようには機能しません。私のコードでは、いくつかのクラスに大きな階層があります。

これを行うための推奨される方法は何ですか?基本クラスを呼び出さずに各レベルですべてを複製するのは間違っているように思われますか?この問題にはいくつかのきちんとした解決策があるはずです、それらは何ですか?

みんなの答えに感謝してもいいですか。いくつかのアプローチを見るのは本当に面白かったです。誰かが完全性のための反省の答えの例を挙げたらいいと思います。+1待ち!

4

8 に答える 8

32

典型的なアプローチは、C++ の「コピー コンストラクター」パターンを使用することです。

 class Base : ICloneable
 { 
     int x;

     protected Base(Base other)
     {
         x = other.x;
     }

     public virtual object Clone()
     {
         return new Base(this);
     }
 }

 class Derived : Base
 { 
     int y;

     protected Derived(Derived other)
          : Base(other)
     {
         y = other.y;
     }

     public override object Clone()
     {
         return new Derived(this);
     }
 }

もう 1 つのアプローチは、次Object.MemberwiseCloneの実装で使用Cloneすることです。これにより、結果が常に正しい型であることが保証され、オーバーライドを拡張できるようになります。

 class Base : ICloneable
 { 
     List<int> xs;

     public virtual object Clone()
     {
         Base result = this.MemberwiseClone();

         // xs points to same List object here, but we want
         // a new List object with copy of data
         result.xs = new List<int>(xs);

         return result;
     }
 }

 class Derived : Base
 { 
     List<int> ys;

     public override object Clone()
     {
         // Cast is legal, because MemberwiseClone() will use the
         // actual type of the object to instantiate the copy.
         Derived result = (Derived)base.Clone();

         // ys points to same List object here, but we want
         // a new List object with copy of data
         result.ys = new List<int>(ys);

         return result;
     }
 }

どちらの方法でも、階層内のすべてのクラスがパターンに従う必要があります。どちらを使用するかは好みの問題です。

実装が保証されていないランダムなクラスを実装ICloneableしている場合 (文書化された のセマンティクスに従うことを除けばICloneable)、それを拡張する方法はありません。

于 2009-10-15T16:40:55.230 に答える
7

警告:

このコードは、細心の注意を払って使用する必要があります。自己責任。この例は現状のまま提供され、いかなる種類の保証もありません。


オブジェクト グラフでディープ クローンを実行する別の方法があります。このサンプルの使用を検討する際には、次の点に注意することが重要です。

短所:

  1. これらの参照が Clone(object, ...) メソッドに提供されない限り、外部クラスへの参照も複製されます。
  2. 複製されたオブジェクトに対してコンストラクターは実行されず、そのまま正確に再現されます。
  3. ISerializable またはシリアル化コンストラクターは実行されません。
  4. 特定の型でこのメソッドの動作を変更する方法はありません。
  5. ストリーム、AppDomain、フォームなど、すべてを複製します。これらは、アプリケーションを恐ろしい方法で破壊する可能性があります。
  6. シリアライゼーション方法を使用すると、動作し続ける可能性がはるかに高くなりますが、壊れる可能性があります。
  7. 以下の実装では再帰を使用しているため、オブジェクト グラフが深すぎると、スタック オーバーフローが簡単に発生する可能性があります。

では、なぜそれを使いたいのでしょうか?

長所:

  1. オブジェクトにコーディングを必要とせずに、すべてのインスタンス データの完全なディープ コピーを実行します。
  2. 再構成されたオブジェクト内のすべてのオブジェクト グラフ参照 (循環も含む) を保持します。
  3. メモリ消費量が少なく、バイナリ フォーマッタよりも 20 倍以上高速に実行されます。
  4. 何も、属性も、実装されたインターフェイスも、パブリック プロパティも、何も必要としません。

コードの使用法:

オブジェクトで呼び出すだけです:

Class1 copy = Clone(myClass1);

または、子オブジェクトがあり、そのイベントにサブスクライブしているとしましょう...今度は、その子オブジェクトを複製したいとします。複製しないオブジェクトのリストを提供することで、オブジェクト グラフの一部を保持できます。

Class1 copy = Clone(myClass1, this);

実装:

それでは、最初に簡単なことから始めましょう... ここにエントリ ポイントがあります。

public static T Clone<T>(T input, params object[] stableReferences)
{
    Dictionary<object, object> graph = new Dictionary<object, object>(new ReferenceComparer());
    foreach (object o in stableReferences)
        graph.Add(o, o);
    return InternalClone(input, graph);
}

これで十分簡単に​​なりました。複製中にオブジェクトのディクショナリ マップを作成し、複製してはならないオブジェクトを入力するだけです。ディクショナリに提供される比較子は ReferenceComparer であることに注意してください。それが何をするかを見てみましょう。

class ReferenceComparer : IEqualityComparer<object>
{
    bool IEqualityComparer<object>.Equals(object x, object y)
    { return Object.ReferenceEquals(x, y); }
    int IEqualityComparer<object>.GetHashCode(object obj)
    { return RuntimeHelpers.GetHashCode(obj); }
}

それはとても簡単で、System.Object の get ハッシュと参照の等価性を強制的に使用する比較子だけでした。次は大変な作業です。

private static T InternalClone<T>(T input, Dictionary<object, object> graph)
{
    if (input == null || input is string || input.GetType().IsPrimitive)
        return input;

    Type inputType = input.GetType();

    object exists;
    if (graph.TryGetValue(input, out exists))
        return (T)exists;

    if (input is Array)
    {
        Array arItems = (Array)((Array)(object)input).Clone();
        graph.Add(input, arItems);

        for (long ix = 0; ix < arItems.LongLength; ix++)
            arItems.SetValue(InternalClone(arItems.GetValue(ix), graph), ix);
        return (T)(object)arItems;
    }
    else if (input is Delegate)
    {
        Delegate original = (Delegate)(object)input;
        Delegate result = null;
        foreach (Delegate fn in original.GetInvocationList())
        {
            Delegate fnNew;
            if (graph.TryGetValue(fn, out exists))
                fnNew = (Delegate)exists;
            else
            {
                fnNew = Delegate.CreateDelegate(input.GetType(), InternalClone(original.Target, graph), original.Method, true);
                graph.Add(fn, fnNew);
            }
            result = Delegate.Combine(result, fnNew);
        }
        graph.Add(input, result);
        return (T)(object)result;
    }
    else
    {
        Object output = FormatterServices.GetUninitializedObject(inputType);
        if (!inputType.IsValueType)
            graph.Add(input, output);
        MemberInfo[] fields = inputType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        object[] values = FormatterServices.GetObjectData(input, fields);

        for (int i = 0; i < values.Length; i++)
            values[i] = InternalClone(values[i], graph);

        FormatterServices.PopulateObjectMembers(output, fields, values);
        return (T)output;
    }
}

配列とデリゲート コピーの特殊なケースにすぐに気付くでしょう。それぞれに独自の理由があります。まず、配列には複製できる「メンバー」がないため、これを処理し、浅い Clone() メンバーに依存してから、各要素を複製する必要があります。デリゲートに関しては、特別なケースがなくても機能する場合があります。ただし、これは RuntimeMethodHandle などを複製しないため、はるかに安全です。コア ランタイム (System.Type など) から階層に他のものを含める場合は、同様の方法で明示的に処理することをお勧めします。

最後のケースで最も一般的なのは、BinaryFormatter で使用されるルーチンとほぼ同じルーチンを単純に使用することです。これらにより、元のオブジェクトからすべてのインスタンス フィールド (パブリックまたはプライベート) をポップし、それらを複製して、空のオブジェクトに貼り付けることができます。ここで良いことは、GetUninitializedObject が、ctor が実行されていない新しいインスタンスを返すことです。これにより、問題が発生し、パフォーマンスが低下する可能性があります。

上記が機能するかどうかは、特定のオブジェクト グラフとその中のデータに大きく依存します。グラフ内のオブジェクトを制御し、それらが Thread のような愚かなものを参照していないことがわかっている場合、上記のコードは非常にうまく機能するはずです。

テスト:

これを最初にテストするために書いたものは次のとおりです。

class Test
{
    public Test(string name, params Test[] children)
    {
        Print = (Action<StringBuilder>)Delegate.Combine(
            new Action<StringBuilder>(delegate(StringBuilder sb) { sb.AppendLine(this.Name); }),
            new Action<StringBuilder>(delegate(StringBuilder sb) { sb.AppendLine(this.Name); })
        );
        Name = name;
        Children = children;
    }
    public string Name;
    public Test[] Children;
    public Action<StringBuilder> Print;
}

static void Main(string[] args)
{
    Dictionary<string, Test> data2, data = new Dictionary<string, Test>(StringComparer.OrdinalIgnoreCase);

    Test a, b, c;
    data.Add("a", a = new Test("a", new Test("a.a")));
    a.Children[0].Children = new Test[] { a };
    data.Add("b", b = new Test("b", a));
    data.Add("c", c = new Test("c"));

    data2 = Clone(data);
    Assert.IsFalse(Object.ReferenceEquals(data, data2));
    //basic contents test & comparer
    Assert.IsTrue(data2.ContainsKey("a"));
    Assert.IsTrue(data2.ContainsKey("A"));
    Assert.IsTrue(data2.ContainsKey("B"));
    //nodes are different between data and data2
    Assert.IsFalse(Object.ReferenceEquals(data["a"], data2["a"]));
    Assert.IsFalse(Object.ReferenceEquals(data["a"].Children[0], data2["a"].Children[0]));
    Assert.IsFalse(Object.ReferenceEquals(data["B"], data2["B"]));
    Assert.IsFalse(Object.ReferenceEquals(data["B"].Children[0], data2["B"].Children[0]));
    Assert.IsFalse(Object.ReferenceEquals(data["B"].Children[0], data2["A"]));
    //graph intra-references still in tact?
    Assert.IsTrue(Object.ReferenceEquals(data["B"].Children[0], data["A"]));
    Assert.IsTrue(Object.ReferenceEquals(data2["B"].Children[0], data2["A"]));
    Assert.IsTrue(Object.ReferenceEquals(data["A"].Children[0].Children[0], data["A"]));
    Assert.IsTrue(Object.ReferenceEquals(data2["A"].Children[0].Children[0], data2["A"]));
    data2["A"].Name = "anew";
    StringBuilder sb = new StringBuilder();
    data2["A"].Print(sb);
    Assert.AreEqual("anew\r\nanew\r\n", sb.ToString());
}

最後の注意:

正直なところ、当時は楽しいエクササイズでした。一般に、データ モデルでディープ クローンを作成することは非常に便利です。今日の現実は、ほとんどのデータ モデルが生成され、生成されたディープ クローン ルーチンで上記のハッカーの有用性が失われていることです。上記のコードを使用するのではなく、データ モデルを生成することを強くお勧めします。これにより、ディープ クローンを実行できます。

于 2009-10-15T23:31:25.453 に答える
7

シリアル化のトリックを試してください:

public object Clone(object toClone)
{
    BinaryFormatter bf = new BinaryFormatter();
    MemoryStream ms= new MemoryStream();
    bf.Serialize(ms, toClone);
    ms.Flush();
    ms.Position = 0;
    return bf.Deserialize(ms);
}
于 2009-10-15T16:33:29.600 に答える
2

最善の方法は、オブジェクトをシリアル化し、逆シリアル化されたコピーを返すことです。シリアル化不可としてマークされたものを除いて、オブジェクトに関するすべてを取得し、シリアル化の継承を容易にします。

[Serializable]
public class ParentObj: ICloneable
{
    private int myA;
    [NonSerialized]
    private object somethingInternal;

    public virtual object Clone()
    {
        MemoryStream ms = new MemoryStream();
        BinaryFormatter formatter = new BinaryFormatter();
        formatter.Serialize(ms, this);
        object clone = formatter.Deserialize(ms);
        return clone;
    }
}

[Serializable]
public class ChildObj: ParentObj
{
    private int myB;

    // No need to override clone, as it will still serialize the current object, including the new myB field
}

それは最もパフォーマンスの高いものではありませんが、代替手段でもありません。反射です。このオプションの利点は、シームレスに継承されることです。

于 2009-10-15T16:36:09.123 に答える
0

以下を使用してみてください [キーワード「new」を使用]

public class Parent
{
  private int _X;
  public int X{ set{_X=value;} get{return _X;}}
  public Parent copy()
  {
     return new Parent{X=this.X};
  }
}
public class Child:Parent
{
  private int _Y;
  public int Y{ set{_Y=value;} get{return _Y;}}
  public new Child copy()
  {
     return new Child{X=this.X,Y=this.Y};
  }
}
于 2010-06-11T17:51:23.473 に答える
0
  1. リフレクションを使用してすべての変数をループし、それらをコピーできます。(遅い) ソフトウェアが遅い場合は、DynamicMethod を使用して il を生成できます。
  2. オブジェクトをシリアライズし、再度デシリアライズします。
于 2009-10-15T16:24:18.837 に答える
0

ここで ICloneable を正しく実装しているとは思いません。パラメータのない Clone() メソッドが必要です。私がお勧めするのは次のようなものです:

public class ParentObj : ICloneable
{
    public virtual Object Clone()
    {
        var obj = new ParentObj();

        CopyObject(this, obj);
    }

    protected virtual CopyObject(ParentObj source, ParentObj dest)
    {
        dest.myA = source.myA;
    }
}

public class ChildObj : ParentObj
{
    public override Object Clone()
    {
        var obj = new ChildObj();
        CopyObject(this, obj);
    }

    public override CopyObject(ChildObj source, ParentObj dest)
    {
        base.CopyObject(source, dest)
        dest.myB = source.myB;
    }
}

CopyObject() は基本的に Object.MemberwiseClone() であることに注意してください。おそらく、値をコピーするだけでなく、クラスであるメンバーを複製することもあるでしょう。

于 2009-10-15T16:32:28.227 に答える
-1

MemberwiseClone代わりに次のメソッドを使用する必要があります。

public class ParentObj : ICloneable
{
    protected int myA;
    public virtual Object Clone()
    {
        ParentObj newObj = this.MemberwiseClone() as ParentObj;
        newObj.myA = this.MyA; // not required, as value type (int) is automatically already duplicated.
        return newObj;
    }
}

public class ChildObj : ParentObj
{
    protected int myB;
    public override Object Clone()
        {
             ChildObj newObj = base.Clone() as ChildObj;
             newObj.myB = this.MyB; // not required, as value type (int) is automatically already duplicated

             return newObj;
        }
}
于 2009-10-15T16:50:54.433 に答える