Мне нужно реализовать конструкторы глубокой копии С# с наследованием. Какие шаблоны существуют на выбор?

Я хочу реализовать глубокую копию иерархии классов в С#

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 ждет!

Ответы

Ответ 1

Типичный подход заключается в использовании шаблона "copy constructor" a la С++:

 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);
     }
 }

Другим подходом является использование 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), нет возможности расширить его.

Ответ 2

попробуйте выполнить сериализацию:

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);
}

Ответ 3

Внимание:

Этот код следует использовать с большой осторожностью. Используйте на свой риск. Этот пример предоставляется как есть и без каких-либо гарантий.


Существует еще один способ выполнения глубокого клона на графе объектов. При рассмотрении этого примера важно знать следующее:

Минусы:

  • Любые ссылки на внешние классы также будут клонированы, если эти ссылки не будут предоставлены методу Clone (object,...).
  • Никакие конструкторы не будут выполняться на клонированных объектах, которые они воспроизводятся ТОЧНО, как они есть.
  • Никакие конструкторы ISERializable или сериализации не будут выполнены.
  • Невозможно изменить поведение этого метода для определенного типа.
  • Он будет клонировать все, Stream, AppDomain, Form, что угодно, а те , скорее всего, нарушают ваше приложение ужасно.
  • Он может сломаться, тогда как использование метода сериализации гораздо более вероятно продолжит работу.
  • Нижеприведенная реализация использует рекурсию и может легко вызвать переполнение стека, если граф объекта слишком глубок.

Итак, зачем вы хотите его использовать?

Плюсы:

  • Он выполняет полную глубокую копию всех данных экземпляра без необходимости кодирования в объекте.
  • Он сохраняет все ссылки на объектные графы (даже круговые) в восстановленном объекте.
  • Он выполняет более, чем 20 раз больше, чем двоичный форматтер с меньшим потреблением памяти.
  • Он не требует ничего, никаких атрибутов, реализованных интерфейсов, общедоступных свойств, ничего.

Использование кода:

Вы просто вызываете его с помощью объекта:

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 hash и ссылочное равенство... теперь идет тяжелая работа:

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());
}

Заключительное примечание:

Честно говоря, это было веселое упражнение в то время. Как правило, очень важно иметь глубокое клонирование модели данных. Сегодня реальность такова, что генерируются большинство моделей данных, которые устаревают полезность хакера выше с помощью сгенерированной процедуры глубокого клонирования. Я настоятельно рекомендую создать вашу модель данных и способность выполнять глубокие клоны, а не использовать вышеприведенный код.

Ответ 4

Лучший способ - сериализация вашего объекта, а затем возврат десериализованной копии. Он будет собирать все о вашем объекте, кроме тех, которые помечены как несериализуемые, и упрощает наследование сериализации.

[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
}

Это не самая результативная вещь, но ни одна из них не является альтернативой: отбор. Преимущество этого варианта заключается в том, что он наследует всевозможные наборы.

Ответ 5

  • Вы можете использовать отражение, чтобы закодировать все переменные и скопировать их. (Slow), если его замедлить для вас программное обеспечение, вы можете использовать DynamicMethod и генерировать il.
  • сериализуйте объект и снова десериализуйте его.

Ответ 6

Я не думаю, что вы правильно реализуете 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(), предположительно вы бы делали больше, чем просто копирование значений, вы также клонировали бы любые члены, которые являются классами.

Ответ 7

Попробуйте использовать следующее [используйте ключевое слово "новый" ]

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};
  }
}

Ответ 8

Вместо этого вы должны использовать метод 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;
        }
}