Копировать конструктор против Clone()

В С#, каков предпочтительный способ добавления (глубокой) функции копирования в класс? Следует ли реализовать конструктор копирования или, скорее, получить из ICloneable и реализовать метод Clone()?

Примечание: Я написал "глубоко" в скобках, потому что думал, что это не имеет значения. По-видимому, другие не согласны, поэтому я спросил должен ли конструктор/оператор/функция копирования указывать, какой вариант копирования он реализует.

Ответ 1

Вы не должны извлекаться из ICloneable.

Причина в том, что когда Microsoft разрабатывала инфраструктуру .net, они никогда не указывали, должен ли метод Clone() на ICloneable быть глубоким или мелким клоном, поэтому интерфейс семантически нарушен, так как ваши вызывающие стороны не будут знать, вызов будет глубоким или мелким клонировать объект.

Вместо этого вы должны определить свои собственные интерфейсы IDeepCloneableIShallowCloneable) с помощью методов DeepClone()ShallowClone()).

Вы можете определить два интерфейса: один с общим параметром для поддержки строго типизированного клонирования и один без сохранения слабо типизированной способности клонирования, когда вы работаете с коллекциями различных типов клонируемых объектов:

public interface IDeepCloneable
{
    object DeepClone();
}
public interface IDeepCloneable<T> : IDeepCloneable
{
    T DeepClone();
}

Что бы вы тогда реализовали следующим образом:

public class SampleClass : IDeepCloneable<SampleClass>
{
    public SampleClass DeepClone()
    {
        // Deep clone your object
        return ...;
    }
    object IDeepCloneable.DeepClone()   
    {
        return this.DeepClone();
    }
}

Как правило, я предпочитаю использовать описанные интерфейсы, а не конструктор копий, который полностью сохраняет намерение. Конструктор копирования, вероятно, предположил бы, что это глубокий клон, но это, конечно же, не столько явное намерение, как использование интерфейса IDeepClonable.

Это обсуждается в .net Руководство по дизайну каркаса и на Brad Блог Abrams

(Я полагаю, что если вы пишете приложение (в отличие от фреймворка/библиотеки), вы можете быть уверены, что никто за пределами вашей команды не вызовет ваш код, это не имеет большого значения, и вы может назначить семантический смысл "deepclone" для интерфейса .net ICloneable, но вы должны убедиться, что это хорошо документировано и хорошо понято внутри вашей команды. Лично я придерживаюсь руководящих принципов структуры.)

Ответ 2

В С#, каков предпочтительный способ добавления (глубокой) функции копирования в класс? Если реализовать конструктор копирования, или, скорее, получить от ICloneable и реализовать метод Clone()?

Проблема с ICloneable заключается в том, что она не указывает, является ли она глубокой или мелкой копией, что делает ее практически не используемой и на практике редко используется. Он также возвращает object, что является болью, поскольку для этого требуется много кастинга. (И хотя вы конкретно упомянули классы в вопросе, реализация ICloneable на struct требует бокса.)

Копирование constrctor также страдает одной из проблем с ICloneable. Неясно, делает ли конструктор копии глубокую или мелкую копию.

Account clonedAccount = new Account(currentAccount); // Deep or shallow?

Лучше всего было бы создать метод DeepClone(). Таким образом, намерение совершенно ясно.

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

Account clonedAccount = currentAccount.DeepClone();  // instance method

или

Account clonedAccount = Account.DeepClone(currentAccount); // static method

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

class CheckingAccount : Account
{
    CheckAuthorizationScheme checkAuthorizationScheme;

    public override Account DeepClone()
    {
        CheckingAccount clone = new CheckingAccount();
        DeepCloneFields(clone);
        return clone;
    }

    protected override void DeepCloneFields(Account clone)
    {
        base.DeepCloneFields(clone);

        ((CheckingAccount)clone).checkAuthorizationScheme = this.checkAuthorizationScheme.DeepClone();
    }
}

Ответ 3

Я рекомендую использовать конструктор копирования поверх метода клонирования прежде всего потому, что метод клонирования не позволит вам создавать поля только для readonly что могло бы быть, если бы вместо этого вы использовали конструктор.

Если вам требуется полиморфное клонирование, вы можете добавить abstract или virtual метод Clone() в базовый класс, который вы реализуете, с помощью вызова конструктора копирования.

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

Пример:

public class BaseType {
   readonly int mBaseField;

   public BaseType(BaseType pSource) =>
      mBaseField = pSource.mBaseField;

   public virtual BaseType Clone() =>
      new BaseType(this);
}

public class SubType : BaseType {
   readonly int mSubField;

   public SubType(SubType pSource)
   : base(pSource) =>
      mSubField = pSource.mSubField;

   public override BaseType Clone() =>
      new SubType(this);
}

Ответ 4

Существует большой аргумент, что вы должны реализовать clone() с помощью защищенного конструктора копии

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

Итак, это не вопрос "против". Вам может понадобиться как конструктор копирования, так и интерфейс клона, чтобы сделать это правильно.

(Хотя рекомендуемым общедоступным интерфейсом является интерфейс Clone(), а не конструктор.)

Не увлекайтесь явным глубоким или мелким аргументом в других ответах. В реальном мире это почти всегда что-то среднее - и в любом случае - не должно быть проблемой вызывающего абонента.

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

Ответ 5

Внедрение ICloneable не рекомендуется из-за того, что он не указал, является ли это глубокой или мелкой копией, поэтому я бы пошел на конструктор, или просто реализовать что-то самостоятельно. Возможно, назовите его DeepCopy(), чтобы сделать его действительно очевидным!

Ответ 6

У вас возникнут проблемы с конструкторами копирования и абстрактными классами. Представьте, что вы хотите сделать следующее:

abstract class A
{
    public A()
    {
    }

    public A(A ToCopy)
    {
        X = ToCopy.X;
    }
    public int X;
}

class B : A
{
    public B()
    {
    }

    public B(B ToCopy) : base(ToCopy)
    {
        Y = ToCopy.Y;
    }
    public int Y;
}

class C : A
{
    public C()
    {
    }

    public C(C ToCopy)
        : base(ToCopy)
    {
        Z = ToCopy.Z;
    }
    public int Z;
}

class Program
{
    static void Main(string[] args)
    {
        List<A> list = new List<A>();

        B b = new B();
        b.X = 1;
        b.Y = 2;
        list.Add(b);

        C c = new C();
        c.X = 3;
        c.Z = 4;
        list.Add(c);

        List<A> cloneList = new List<A>();

        //Won't work
        //foreach (A a in list)
        //    cloneList.Add(new A(a)); //Not this time batman!

        //Works, but is nasty for anything less contrived than this example.
        foreach (A a in list)
        {
            if(a is B)
                cloneList.Add(new B((B)a));
            if (a is C)
                cloneList.Add(new C((C)a));
        }
    }
}

Сразу после выполнения вышеизложенного вы начинаете желать, чтобы вы либо использовали интерфейс, либо установили для реализации DeepCopy()/ICloneable.Clone().

Ответ 7

Проблема с ICloneable - это как намерение, так и последовательность. Он никогда не проясняет, является ли это глубокой или мелкой копией. Из-за этого он, вероятно, никогда не использовался только так или иначе.

Я не нахожу, чтобы конструктор публичных копий был более ясным в этом вопросе.

Тем не менее, я бы представил систему методов, которая работает для вас и реле намерения (a'la несколько самостоятельного документирования)

Ответ 8

Если объект, который вы пытаетесь скопировать, является Serializable, вы можете клонировать его, сериализируя его и десериализируя. Тогда вам не нужно писать конструктор копирования для каждого класса.

У меня нет доступа к коду прямо сейчас, но это что-то вроде этого

public object DeepCopy(object source)
{
   // Copy with Binary Serialization if the object supports it
   // If not try copying with XML Serialization
   // If not try copying with Data contract Serailizer, etc
}

Ответ 9

Это зависит от семантики копии рассматриваемого класса, которую вы должны определить как разработчика. Выбранный метод обычно основан на предполагаемых вариантах использования класса. Может быть, будет смысл реализовать оба метода. Но оба имеют сходный недостаток - не совсем ясно, какой метод копирования они реализуют. Это должно быть четко указано в документации для вашего класса.

Для меня есть:

// myobj is some transparent proxy object
var state = new ObjectState(myobj.State);

// do something

myobject = GetInstance();
var newState = new ObjectState(myobject.State);

if (!newState.Equals(state))
    throw new Exception();

вместо:

// myobj is some transparent proxy object
var state = myobj.State.Clone();

// do something

myobject = GetInstance();
var newState = myobject.State.Clone();

if (!newState.Equals(state))
    throw new Exception();

выглядел более ясным выражением о намерениях.

Ответ 10

Я думаю, что должен быть стандартный шаблон для клонируемых объектов, хотя я не уверен, какой именно шаблон должен быть. Что касается клонирования, то, по-видимому, существуют три типа классов:

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

Насколько я могу судить, единственный способ (по крайней мере, в .net 2.0) получить новый объект того же класса, что и существующий объект, - использовать MemberwiseClone. Хороший шаблон, похоже, должен иметь функцию "новый" / "Тени" Clone, которая всегда возвращает текущий тип, чье определение всегда вызывает MemberwiseClone, а затем вызывает защищенную виртуальную подпрограмму CleanupClone (originalObject). Процедура CleanupCode должна вызвать base.Cleanupcode для обработки запросов клонирования базового типа, а затем добавить свою собственную очистку. Если в процедуре клонирования должен использоваться исходный объект, это должно быть typecast, но в противном случае единственным типом будет только вызов MemberwiseClone.

К сожалению, самый низкий уровень класса, который имел тип (1) выше, а не тип (2), должен был быть закодирован, чтобы предположить, что его более низкие типы не нуждаются в явной поддержке клонирования. Я действительно не вижу никакого способа обойти это.

Тем не менее, я думаю, что определенный шаблон будет лучше, чем ничего.

Кстати, если известно, что один базовый тип поддерживает iCloneable, но не знает имя используемой функции, есть ли способ ссылаться на функцию iCloneable.Clone одного базового типа?

Ответ 11

Если вы прочитаете все интересные ответы и обсуждения, вы все равно можете спросить себя, как именно вы копируете свойства - все они явно, или есть более элегантный способ сделать это? Если это ваш оставшийся вопрос, взгляните на это (в StackOverflow):

Как я могу "глубоко" клонировать свойства сторонних классов с использованием универсального метода расширения?

В нем описывается, как реализовать метод расширения CreateCopy(), который создает "глубокую" копию объекта, включая все свойства (без необходимости вручную копировать свойство по свойству).