Правильный способ реализации ICloneable

Как правильно реализовать ICloneable в иерархии классов? Скажем, у меня есть абстрактный класс DrawingObject, Еще один абстрактный класс RectangularObject наследуется от DrawingObject, Тогда есть несколько конкретных классов, таких как Shape, Text, Circle и т.д., что все наследуют от RectangularObject, Я хочу реализовать ICloneable на DrawingObject а затем перенести его вниз по иерархии, копируя доступные свойства на каждом уровне и вызывая родительский Clone на следующем уровне.

Проблема, однако, заключается в том, что, поскольку первые два класса являются абстрактными, я не могу создать их объекты в Clone() метод. Таким образом, я должен продублировать процедуру копирования свойств в каждом конкретном классе. Или есть лучший способ?

7 ответов

Вы можете легко создать поверхностный клон с objectЗащищенный метод MemberwiseClone.

Пример:

   public abstract class AbstractCloneable : ICloneable
   {
      public object Clone()
      {
         return this.MemberwiseClone();
      }
   }

Если вам ничего не нужно, как глубокая копия, вам не нужно ничего делать в дочерних классах.

Метод MemberwiseClone создает поверхностную копию, создавая новый объект, а затем копируя нестатические поля текущего объекта в новый объект. Если поле является типом значения, выполняется побитовая копия поля. Если поле является ссылочным типом, ссылка копируется, а ссылочный объект - нет; следовательно, исходный объект и его клон ссылаются на один и тот же объект.

Если вам нужно больше интеллекта в логике клонирования, вы можете добавить виртуальный метод для обработки ссылок:

   public abstract class AbstractCloneable : ICloneable
   {
      public object Clone()
      {
         var clone = (AbstractCloneable) this.MemberwiseClone();
         HandleCloned(clone);
         return clone;
      }

      protected virtual HandleCloned(AbstractCloneable clone)
      {
         //Nothing particular in the base class, but maybe usefull for childs.
         //Not abstract so childs may not implement this if they don't need to.
      }

   }


   public class ConcreteCloneable : AbstractCloneable
   {
       protected override HandleCloned(AbstractCloneable clone)
       {
           //Get wathever magic a base class could have implemented.
           base.HandleCloned(clone);

           //Clone is of the current type.
           ConcreteCloneable obj = (ConcreteCloneable) clone;

           //Here you have a superficial copy of "this". You can do wathever 
           //specific task you need to do.
           //e.g.:
           obj.SomeReferencedPropertie = this.SomeReferencedPropertie.Clone();
       }
   }

Дайте вашему базовому классу защищенный и переопределенный CreateClone() метод, который создает новый (пустой) экземпляр текущего класса. Тогда есть Clone() метод базового класса вызывает этот метод для полиморфного создания нового экземпляра, в который базовый класс может затем скопировать значения своего поля.

Производные неабстрактные классы могут переопределять CreateClone() метод для создания экземпляра соответствующего класса, и все производные классы, которые вводят новые поля, могут переопределять Clone() скопировать их дополнительные значения полей в новый экземпляр после вызова унаследованной версии Clone(),

public CloneableBase : ICloneable
{
    protected abstract CloneableBase CreateClone();

    public virtual object Clone()
    {
        CloneableBase clone = CreateClone();
        clone.MyFirstProperty = this.MyFirstProperty;
        return clone;
    }

    public int MyFirstProperty { get; set; }
}

public class CloneableChild : CloneableBase
{
    protected override CloneableBase CreateClone()
    {
        return new CloneableChild();
    }

    public override object Clone()
    {
        CloneableChild clone = (CloneableChild)base.Clone();
        clone.MySecondProperty = this.MySecondProperty;
        return clone;
    }

    public int MySecondProperty { get; set; }
}

Если вы хотите пропустить первый шаг переопределения (по крайней мере, в случае по умолчанию), вы можете также предположить сигнатуру конструктора по умолчанию (например, без параметров) и попытаться создать экземпляр экземпляра клона, используя эту сигнатуру конструктора с отражением. Таким образом, только классы, конструкторы которых не соответствуют сигнатуре по умолчанию, должны будут переопределять CreateClone(),

Очень простая версия этого по умолчанию CreateClone() Реализация может выглядеть так:

protected virtual CloneableBase CreateClone()
{
    return (CloneableBase)Activator.CreateInstance(GetType());
}

Чтобы создать объект глубокого клонирования с новыми ссылками и избежать мутаций ваших объектов в самых неожиданных местах, используйте Serialize/Deserialize.

Это позволит полностью контролировать то, что можно клонировать (используя атрибут игнорирования). Вот пример с System.Text.Json и Newtonsoft.

      // System.Text.Json
public object Clone()
{
    // setup
    var json = JsonSerializer.Serialize(this);

    // get
    return JsonSerializer.Deserialize<MyType>(json);
}

// Newtonsoft
public object Clone()
{
    // setup
    var json = JsonConvert.SerializeObject(this);

    // get
    return JsonConvert.DeserializeObject<MyType>(json);
}

// Usage
MyType clonedMyType = myType.Clone();

Я считаю, что у меня есть улучшение по сравнению с превосходным ответом @johnny5. Просто определите конструкторы копирования во всех классах, а в базовом классе используйте отражение в методе Clone, чтобы найти конструктор копирования и выполнить его. Я думаю, что это немного чище, так как вам не нужен стек переопределений клонов дескрипторов, и вам не нужен MemberwiseClone(), который во многих ситуациях просто слишком тупой инструмент.

public abstract class AbstractCloneable : ICloneable
    {
        public int BaseValue { get; set; }
        protected AbstractCloneable()
        {
            BaseValue = 1;
        }
        protected AbstractCloneable(AbstractCloneable d)
        {
            BaseValue = d.BaseValue;
        }

        public object Clone()
        {
            var clone = ObjectSupport.CloneFromCopyConstructor(this);
            if(clone == null)throw new ApplicationException("Hey Dude, you didn't define a copy constructor");
            return clone;
        }

    }


    public class ConcreteCloneable : AbstractCloneable
    {
        public int DerivedValue { get; set; }
        public ConcreteCloneable()
        {
            DerivedValue = 2;
        }

        public ConcreteCloneable(ConcreteCloneable d)
            : base(d)
        {
            DerivedValue = d.DerivedValue;
        }
    }

    public class ObjectSupport
    {
        public static object CloneFromCopyConstructor(System.Object d)
        {
            if (d != null)
            {
                Type t = d.GetType();
                foreach (ConstructorInfo ci in t.GetConstructors())
                {
                    ParameterInfo[] pi = ci.GetParameters();
                    if (pi.Length == 1 && pi[0].ParameterType == t)
                    {
                        return ci.Invoke(new object[] { d });
                    }
                }
            }

            return null;
        }
    }

Наконец, позвольте мне высказаться в пользу ICloneable. Если вы используете этот интерфейс, вы будете побеждены полицией стилей, потому что в Руководстве по дизайну.NET Framework сказано, что не следует его реализовывать, потому что, цитируя рекомендации, "при использовании объекта, реализующего тип с ICloneable, вы никогда не узнаете, что вы собираются получить. Это делает интерфейс бесполезным." Подразумевается, что вы не знаете, получаете ли вы глубокую или мелкую копию. Ну, это просто софистика. Означает ли это, что конструкторы копирования никогда не должны использоваться, потому что "вы никогда не знаете, что собираетесь получить?" Конечно, нет. Если вы не знаете, что собираетесь получить, это просто проблема с дизайном класса, а не с интерфейсом.

На мой взгляд, самый простой способ - применить двоичную сериализацию с BinaryFormatter в MemoryStream,

Существует ветка MSDN о глубоком клонировании в C#, где предлагается вышеуказанный подход.

Как минимум, вы позволяете только конкретным классам обрабатывать клонирование, а абстрактные классы имеют protected Копировать конструкторы. Теперь помимо этого вы хотите иметь возможность принимать переменную DrawingObject и клонировать это так:

class Program
{
    static void Main(string[] args)
    {
        DrawingObject obj1=new Circle(Color.Black, 10);
        DrawingObject obj2=obj1.Clone();
    }
}

Вы могли бы рассмотреть этот обман, но я бы использовал методы расширения и отражения:

public abstract class DrawingObject
{
    public abstract void Draw();
    public Color Color { get; set; }
    protected DrawingObject(DrawingObject other)
    {
        this.Color=other.Color;
    }
    protected DrawingObject(Color color) { this.Color=color; }
}

public abstract class RectangularObject : DrawingObject
{
    public int Width { get; set; }
    public int Height { get; set; }
    protected RectangularObject(RectangularObject other)
        : base(other)
    {
        Height=other.Height;
        Width=other.Width;
    }
    protected RectangularObject(Color color, int width, int height)
        : base(color)
    {
        this.Width=width;
        this.Height=height;
    }
}

public class Circle : RectangularObject, ICloneable
{
    public int Diameter { get; set; }
    public override void Draw()
    {
    }
    public Circle(Circle other)
        : base(other)
    {
        this.Diameter=other.Diameter;
    }
    public Circle(Color color, int diameter)
        : base(color, diameter, diameter)
    {
        Diameter=diameter;
    }

    #region ICloneable Members
    public Circle Clone() { return new Circle(this); }
    object ICloneable.Clone()
    {
        return Clone();
    }
    #endregion

}

public class Square : RectangularObject, ICloneable
{
    public int Side { get; set; }
    public override void Draw()
    {
    }
    public Square(Square other)
        : base(other)
    {
        this.Side=other.Side;
    }
    public Square(Color color, int side)
        : base(color, side, side)
    {
        this.Side=side;
    }

    #region ICloneable Members
    public Square Clone() { return new Square(this); }
    object ICloneable.Clone()
    {
        return Clone();
    }
    #endregion

}

public static class Factory
{
    public static T Clone<T>(this T other) where T : DrawingObject
    {
        Type t = other.GetType();
        ConstructorInfo ctor=t.GetConstructor(new Type[] { t });
        if (ctor!=null)
        {
            ctor.Invoke(new object[] { other });
        }
        return default(T);
    }
}

Редактировать 1

Если вы помните о скорости (каждый раз делая отражение), вы можете: а) Кешировать конструктор в статическом словаре.

public static class Factory
{
    public static T Clone<T>(this T other) where T : DrawingObject
    {
        return Dynamic<T>.CopyCtor(other);
    }
}

public static class Dynamic<T> where T : DrawingObject
{
    static Dictionary<Type, Func<T, T>> cache = new Dictionary<Type,Func<T,T>>();

    public static T CopyCtor(T other)
    {
        Type t=other.GetType();
        if (!cache.ContainsKey(t))
        {
            var ctor=t.GetConstructor(new Type[] { t });
            cache.Add(t, (x) => ctor.Invoke(new object[] { x }) as T);
        }
        return cache[t](other);
    }
}

Вот копия-вставка некоторого примера кода, который я лежал без дела, который я написал несколько лет назад.

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

Сказав это, вот образец шаблона клонирования:

using System;
using System.IO;
using System.Diagnostics;

/*

This code demonstrates a cloning pattern that you can use for class hierarchies.

The abstract base class specifies an abstract Clone() method which must be implemented by all derived classes.
Every class except the abstract base class must have a protected copy constructor. 

This protected copy constructor will:

(1) call the base class' copy constructor, and 
(2) set any new fields introduced in the derived class.

This code also demonstrates an implementation of Equals() and CopyFrom().

*/

namespace CloningPattern
{
    //—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

    static class Program
    {
        static void Main()
        {
            Derived2 test = new Derived2()
            {
                IntValue = 1,
                StringValue = "s",
                DoubleValue = 2,
                ShortValue = 3
            };

            Derived2 copy = Clone(test);
            Console.WriteLine(copy);
        }

        static Derived2 Clone(AbstractBase item)
        {
            AbstractBase abstractBase = (AbstractBase) item.Clone();
            Derived2 result = abstractBase as Derived2;
            Debug.Assert(result != null);
            return result;
        }
    }

    //—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

    public abstract class AbstractBase: ICloneable
    {
        // Sample data field.

        public int IntValue { get; set; }

        // Canonical way of providing a Clone() operation
        // (except that this is abstract rather than virtual, since this class
        // is itself abstract).

        public abstract object Clone();

        // Default constructor.

        protected AbstractBase(){}

        // Copy constructor.

        protected AbstractBase(AbstractBase other)
        {
            if (other == null)
            {
                throw new ArgumentNullException("other");
            }

            this.copyFrom(other);
        }

        // Copy from another instance over the top of an already existing instance.

        public virtual void CopyFrom(AbstractBase other)
        {
            if (other == null)
            {
                throw new ArgumentNullException("other");
            }

            this.copyFrom(other);
        }

        // Equality check.

        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }

            if (object.ReferenceEquals(this, obj))
            {
                return true;
            }

            if (this.GetType() != obj.GetType())
            {
                return false;
            }

            AbstractBase other = (AbstractBase)obj;

            return (this.IntValue == other.IntValue);
        }

        // Get hash code.

        public override int GetHashCode()
        {
            return this.IntValue.GetHashCode();
        }

        // ToString() for debug purposes.

        public override string ToString()
        {
            return "IntValue = " + IntValue;
        }

        // Implement copying fields in a private non-virtual method, called from more than one place.

        private void copyFrom(AbstractBase other)  // 'other' cannot be null, so no check for nullness is made.
        {
            this.IntValue = other.IntValue;
        }
    }

    //—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

    public abstract class AbstractDerived: AbstractBase
    {
        // Sample data field.

        public short ShortValue{ get; set; }

        // Default constructor.

        protected AbstractDerived(){}

        // Copy constructor.

        protected AbstractDerived(AbstractDerived other): base(other)
        {
            this.copyFrom(other);
        }

        // Copy from another instance over the top of an already existing instance.

        public override void CopyFrom(AbstractBase other)
        {
            base.CopyFrom(other);
            this.copyFrom(other as AbstractDerived);
        }

        // Comparison.

        public override bool Equals(object obj)
        {
            if (object.ReferenceEquals(this, obj))
            {
                return true;
            }

            if (!base.Equals(obj))
            {
                return false;
            }

            AbstractDerived other = (AbstractDerived)obj;  // This must succeed because if the types are different, base.Equals() returns false.

            return (this.IntValue == other.IntValue);
        }

        // Get hash code.

        public override int GetHashCode()
        {
            // "Standard" way of combining hash codes from subfields.

            int hash = 17;

            hash = hash * 23 + base.GetHashCode();
            hash = hash * 23 + this.ShortValue.GetHashCode();

            return hash;
        }

        // ToString() for debug purposes.

        public override string ToString()
        {
            return base.ToString() + ", ShortValue = " + ShortValue;
        }

        // This abstract class doesn't need to implement Clone() because no instances of it
        // can ever be created, on account of it being abstract and all that.
        // If you COULD, it would look like this (but you can't so this won't compile):

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

        // Implement copying fields in a private non-virtual method, called from more than one place.

        private void copyFrom(AbstractDerived other)  // Other could be null, so check for nullness.
        {
            if (other != null)
            {
                this.ShortValue = other.ShortValue;
            }
        }
    }

    //—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

    public class Derived1: AbstractDerived
    {
        // Must declare a default constructor.

        public Derived1(){}

        // Sample data field.

        public string StringValue{ get; set; }

        // Implement Clone() by simply using this class' copy constructor.

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

        // Copy from another instance over the top of an already existing instance.

        public override void CopyFrom(AbstractBase other)
        {
            base.CopyFrom(other);
            this.copyFrom(other as Derived1);
        }

        // Equality check.

        public override bool Equals(object obj)
        {
            if (object.ReferenceEquals(this, obj))
            {
                return true;
            }

            if (!base.Equals(obj))
            {
                return false;
            }

            Derived1 other = (Derived1)obj;  // This must succeed because if the types are different, base.Equals() returns false.

            return (this.StringValue == other.StringValue);
        }

        // Get hash code.

        public override int GetHashCode()
        {
            // "Standard" way of combining hash codes from subfields.

            int hash = 17;

            hash = hash * 23 + base.GetHashCode();
            hash = hash * 23 + this.StringValue.GetHashCode();

            return hash;
        }

        // ToString() for debug purposes.

        public override string ToString()
        {
            return base.ToString() + ", StringValue = " + StringValue;
        }

        // Protected copy constructor. Used to implement Clone().
        // Also called by a derived class' copy constructor.

        protected Derived1(Derived1 other): base(other)
        {
            this.copyFrom(other);
        }

        // Implement copying fields in a private non-virtual method, called from more than one place.

        private void copyFrom(Derived1 other)  // Other could be null, so check for nullness.
        {
            if (other != null)
            {
                this.StringValue = other.StringValue;
            }
        }
    }

    //—————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

    public class Derived2: Derived1
    {
        // Must declare a default constructor.

        public Derived2(){}

        // Sample data field.

        public double DoubleValue{ get; set; }

        // Implement Clone() by simply using this class' copy constructor.

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

        // Copy from another instance over the top of an already existing instance.

        public override void CopyFrom(AbstractBase other)
        {
            base.CopyFrom(other);
            this.copyFrom(other as Derived2);
        }

        // Equality check.

        public override bool Equals(object obj)
        {
            if (object.ReferenceEquals(this, obj))
            {
                return true;
            }

            if (!base.Equals(obj))
            {
                return false;
            }

            Derived2 other = (Derived2)obj;  // This must succeed because if the types are different, base.Equals() returns false.

            return (this.DoubleValue == other.DoubleValue);
        }

        // Get hash code.

        public override int GetHashCode()
        {
            // "Standard" way of combining hash codes from subfields.

            int hash = 17;

            hash = hash * 23 + base.GetHashCode();
            hash = hash * 23 + this.DoubleValue.GetHashCode();

            return hash;
        }

        // ToString() for debug purposes.

        public override string ToString()
        {
            return base.ToString() + ", DoubleValue = " + DoubleValue;
        }

        // Protected copy constructor. Used to implement Clone().
        // Also called by a derived class' copy constructor.

        protected Derived2(Derived2 other): base(other)
        {
            // Canonical implementation: use ":base(other)" to copy all
            // the base fields (which recursively applies all the way to the ultimate base)
            // and then explicitly copy any of this class' fields here:

            this.copyFrom(other);
        }

        // Implement copying fields in a private non-virtual method, called from more than one place.

        private void copyFrom(Derived2 other)  // Other could be null, so check for nullness.
        {
            if (other != null)
            {
                this.DoubleValue = other.DoubleValue;
            }
        }
    }
}
Другие вопросы по тегам