Трябва да внедря конструктори за дълбоко копиране на C# с наследяване. Какви модели можете да избирате?

Искам да внедря дълбоко копие на йерархията на моите класове в 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;
        }
}

Това няма да работи, тъй като при Cloning the Child само един родител е нов. В моя код някои класове имат големи йерархии.

Какъв е препоръчителният начин за това? Клонирането на всичко на всяко ниво без извикване на базовия клас изглежда грешно? Трябва да има някои чисти решения на този проблем, какви са те?

Мога ли да благодаря на всички за отговорите. Беше наистина интересно да се видят някои от подходите. Мисля, че би било добре, ако някой даде примерен отговор за размисъл за пълнота. +1 в очакване!


person AnthonyLambert    schedule 15.10.2009    source източник
comment
Защо вашите Clone методи приемат аргумент (който не използват)? Правилният подпис е object ICloneable.Clone() - без аргументи.   -  person Pavel Minaev    schedule 15.10.2009
comment
съжалявам моя грешка... въведен код в движение, прав си!   -  person AnthonyLambert    schedule 15.10.2009
comment
Microsoft препоръчва ICloneable да не се внедрява, тъй като договорът не посочва типа на клонинга (дълбоко/плитко). От Framework Design Guidelines 2-ро изд.   -  person TrueWill    schedule 15.10.2009


Отговори (8)


Типичният подход е да се използва модел на "конструктор за копиране" а ла 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);
     }
 }

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

person Pavel Minaev    schedule 15.10.2009
comment
Важна забележка: Тъй като методът MemberwiseClone води до плитко копие, ще трябва да създадете нови екземпляри на всички членове на референтен тип. Вие също така ще искате да нулирате всички делегати/събития. - person Matt Brunell; 15.10.2009
comment
Не всички членове от референтен тип - само тези, за които наистина имате нужда. Например String е референтен тип, но не мисля, че бихте искали да създавате нови екземпляри от него дори в дълбок клонинг. Същото важи и за всеки друг клас на стойност като Uri или XName. - person Pavel Minaev; 15.10.2009
comment
Конструкторът за копиране не е част от най-добрите практики на C#. Вместо това трябва да се използват ICloneable и MemberwiseClone. - person tofi9; 15.10.2009
comment
@taoufik, моля, предоставете препратка за това твърдение (че внедряването на Clone чрез MemberwiseClone е за предпочитане пред конструктора за копиране - не съм запознат с общопризнати най-добри практики по този конкретен въпрос). Също така ще съм благодарен, ако хората, които са гласували против, обяснят отрицателните гласове. - person Pavel Minaev; 15.10.2009
comment
+1 Конструкторът на копиране осигурява доста чиста реализация за ICloneable. - person Chris Shaffer; 15.10.2009

опитайте трика за сериализация:

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);
}
person Fernando    schedule 15.10.2009
comment
+1, единственият недостатък е, че вашият обект трябва да бъде маркиран с SerializableAttribute. - person Darin Dimitrov; 15.10.2009
comment
По-голям недостатък е, че изисква всички обекти в графиката да могат да се сериализират, а не само вашият клас. Това може да не винаги е осъществимо и някои съществуващи типове, които могат да се клонират, не могат да се сериализират (напр. XPathNavigator имплементира ICloneable, но не може да се сериализира). - person Pavel Minaev; 15.10.2009
comment
Неуспешно за тези свойства: частен низ a; публичен низ A { get { return a; } set { value = a; } - person Kiquenet; 20.09.2013

ВНИМАНИЕ:

Този код трябва да се използва с голяма доза внимание. Използвайте на свой собствен риск. Този пример се предоставя такъв, какъвто е и без никаква гаранция.


Има още един начин за извършване на дълбоко клониране на обектна графика. Важно е да имате предвид следното, когато обмисляте използването на тази проба:

Недостатъци:

  1. Всички препратки към външни класове също ще бъдат клонирани, освен ако тези препратки не са предоставени на метода Clone(object, ...).
  2. Няма да се изпълняват конструктори върху клонирани обекти, те се възпроизвеждат ТОЧНО такива, каквито са.
  3. Няма да се изпълняват конструктори ISerializable или конструктори за сериализация.
  4. Няма начин да се промени поведението на този метод върху определен тип.
  5. ЩЕ клонира всичко, Stream, App Domain, Form, каквото и да е, и те вероятно ще разбият приложението ви по ужасяващи начини.
  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... сега идва тежката работа:

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

Веднага ще забележите специалния случай за копиране на масив и делегат. Всеки има свои собствени причини, първо Array няма „членове“, които могат да бъдат клонирани, така че трябва да се справите с това и да зависи от плиткия Clone() член и след това да клонирате всеки елемент. Що се отнася до делегата, той може да работи без специалния случай; това обаче ще бъде много по-безопасно, тъй като не дублира неща като RuntimeMethodHandle и други подобни. Ако възнамерявате да включите други неща във вашата йерархия от основната среда за изпълнение (като System.Type), предлагам да се справите с тях изрично по подобен начин.

Последният и най-често срещан случай е просто да се използват приблизително същите процедури, които се използват от BinaryFormatter. Те ни позволяват да извадим всички полета на екземпляра (публични или частни) от оригиналния обект, да ги клонираме и да ги поставим в празен обект. Хубавото тук е, че GetUninitializedObject връща нов екземпляр, който не е изпълнявал ctor, което може да причини проблеми и да забави производителността.

Дали горното работи или не ще зависи до голяма степен от вашата конкретна обектна графика и данните в нея. Ако контролирате обектите в графиката и знаете, че те не препращат към глупави неща като нишка, тогава горният код трябва да работи много добре.

Тестване:

Ето какво написах, за да тествам първоначално това:

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

Последна бележка:

Честно казано, това беше забавно упражнение по това време. Като цяло е страхотно да имате дълбоко клониране върху модел на данни. Днешната реалност е, че повечето модели на данни се генерират, което обезценява полезността на хакерството по-горе с генерирана рутина за дълбоко клониране. Силно препоръчвам да генерирате своя модел на данни и неговата способност да извършва дълбоки клонинги, вместо да използвате кода по-горе.

person csharptest.net    schedule 15.10.2009
comment
Благодаря за обширния отговор. +1 само за усилия! Ще трябва да разгледам това в детайли (за да го направя справедливо), когато стигна до прилагането на моето решение. Мерси. - person AnthonyLambert; 16.10.2009

Най-добрият начин е като сериализирате вашия обект, след което върнете десериализираното копие. Той ще вземе всичко за вашия обект, с изключение на тези, които са маркирани като несериализирани, и ще улесни наследяването на сериализацията.

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

Това не е най-ефективното нещо, но не е и алтернативата: отражението. Предимството на тази опция е, че тя безпроблемно наследява.

person jrista    schedule 15.10.2009

  1. Можете да използвате отражение, за да завъртите в цикъл всички променливи и да ги копирате. (Бавно), ако това е бавен за вашия софтуер, можете да използвате DynamicMethod и да генерирате il.
  2. сериализирайте обекта и го десериализирайте отново.
person Peter    schedule 15.10.2009

Не мисля, че внедрявате 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(), вероятно ще правите повече от просто копиране на стойности, вие също ще клонирате всички членове, които са класове.

person Chris Shaffer    schedule 15.10.2009

Опитайте се да използвате следното [използвайте ключовата дума "ново"]

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};
  }
}
person Waleed A.K.    schedule 11.06.2010

Вместо това трябва да използвате метода 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;
        }
}
person tofi9    schedule 15.10.2009
comment
е newObj.myA = theObj.myA; memberwiseclone в този случай не го ли копира, тъй като не е обект? - person AnthonyLambert; 15.10.2009
comment
Ще работи добре. По принцип всеки ChildObj обект е едновременно ParentObj и ChildObj. Ако имате препратка към ChildObj и бихте извикали MemberwiseClone (независимо откъде го извиквате) върху него, ще получите обект, който е както ParentObj, така и ChildObj. Следователно newObj.myA = this.myA; ще бъде напълно валиден. - person tofi9; 15.10.2009
comment
1) Какво е this.base? 2) Защо да използвате as, когато знаете, че актьорският състав винаги ще успее? 3) Защо да копирате стойности на полета, когато MemberwiseClone вече прави точно това? - person Pavel Minaev; 15.10.2009
comment
4) Какво е theObj? Имате предвид this? - person Pavel Minaev; 15.10.2009
comment
@Pavel: идеята е да се отговори на въпроса, а не да се пренапише неговия код или да се отговори на незададен въпрос. this.base =› база; дали използвате as или (ChildObj) е предпочитание на разработчика. Мисля, че (ChildObj) изглежда ужасно. и да, не е необходимо да копирате никакви полета от тип стойност. Но още веднъж, това не е част от неговия въпрос. - person tofi9; 15.10.2009
comment
-1 Заглавието конкретно казва, че иска дълбоко копие, MemberwiseClone е конкретно плитко копие. - person Chris Shaffer; 15.10.2009
comment
@Chris, идеята е да използвате MemberwiseCopy, за да правите копиране поле по поле и също така да гарантирате, че клонираният обект има същия правилен тип, и след това ръчно да правите дълбоко копиране за всички полета, които го изискват вътре в Clone. Този отговор (нито моят отговор, който също предполага същата техника) по никакъв начин не означава, че само MemberwiseClone е отговорът на неговия проблем, а само че е част от решението. - person Pavel Minaev; 15.10.2009