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


120

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

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

Ответы:


91

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

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

Вместо этого вы должны определить свои собственные IDeepCloneable(и IShallowCloneable) интерфейсы с помощью 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 Framework и в блоге Брэда Абрамса.

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


2
Если вы собираетесь использовать интерфейс, как насчет DeepClone (of T) () и DeepClone (of T) (фиктивный как T), оба из которых возвращают T? Последний синтаксис позволит вывести T на основе аргумента.
— supercat

@supercat: Вы говорите, что у вас есть фиктивный параметр, чтобы можно было вывести тип? Полагаю, это вариант. Я не уверен, что мне нравится иметь фиктивный параметр только для автоматического определения типа. Возможно, я вас неправильно понимаю. (Возможно, опубликуйте код в новом ответе, чтобы я понял, что вы имеете в виду).
— Саймон П. Стивенс

@supercat: фиктивный параметр должен существовать как раз для того, чтобы позволить вывод типа. Бывают ситуации, когда некоторый код может захотеть клонировать что-то, не имея прямого доступа к тому, что это за тип (например, потому что это поле, свойство или функция, возвращаемая из другого класса), а фиктивный параметр позволяет правильно вывести тип. Если подумать, это, вероятно, не очень полезно, поскольку весь смысл интерфейса заключался бы в создании чего-то вроде коллекции с глубоким клонированием, и в этом случае типом должен быть общий тип коллекции.
— supercat

2
Вопрос! В какой ситуации вы когда-нибудь захотите неуниверсальную версию? Для меня имеет смысл только IDeepCloneable<T>существовать, потому что ... вы знаете, что T, если вы делаете свою собственную реализацию, то естьSomeClass : IDeepCloneable<SomeClass> { ... }
— Кайл Баран

2
@Kyle говорит, что у вас есть метод, который принимает клонируемые объекты, а MyFunc(IDeepClonable data)затем он может работать со всеми клонируемыми объектами, а не только с конкретным типом. Или если у вас есть коллекция клонируемых файлов. IEnumerable<IDeepClonable> lotsOfCloneablesтогда вы можете клонировать множество объектов одновременно. Если вам не нужны такие вещи, оставьте неуниверсальный вариант.
— Саймон П. Стивенс

33

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

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

Конструктор копирования также страдает от одной из проблем 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();
    }
}

1
Хотя я не знаю, подходит ли вариант DeepClone (), мне очень нравится ваш ответ, поскольку он подчеркивает запутанную ситуацию, которая существует в отношении, на мой взгляд, базовой функции языка программирования. Я думаю, пользователь должен выбрать, какой вариант ему больше нравится.
— Dimitri C.

11
Я не собираюсь здесь спорить, но, на мой взгляд, вызывающий не должен так сильно заботиться о глубоком или мелком, когда они вызывают Clone (). Они должны знать, что получают клон без недопустимого общего состояния. Например, вполне возможно, что в глубоком клонировании я не захочу глубоко клонировать каждый элемент. Все, о чем должен заботиться вызывающий Clone, - это получение новой копии, не имеющей недействительных и неподдерживаемых ссылок на оригинал. Вызов метода «DeepClone», кажется, передает слишком много деталей реализации вызывающему.
— zumalifeguard

1
Что не так с экземпляром объекта, который знает, как клонировать себя, а не копировать статическим методом? В реальном мире это происходит постоянно с биологическими клетками. Клетки в вашем собственном теле сейчас заняты клонированием самих себя, пока вы это читаете. ИМО, вариант статического метода более громоздок, имеет тенденцию скрывать функциональность и отклоняется от использования «наименее неожиданной» реализации в интересах других.
— Кен Беккет

8
@KenBeckett - Причина, по которой я считаю, что клонирование - это что-то, что делается с объектом, заключается в том, что объект должен «делать что-то одно и делать это хорошо». Обычно создание копий самого себя не является основной компетенцией класса, это скорее функциональность, к которой приставляют. Создание клона BankAccount - это то, что вы, возможно, захотите сделать, но создание клонов самого себя не является функцией банковского счета. Ваш пример клетки не очень поучителен, потому что воспроизводство - это именно то, для чего клетки эволюционировали. Cell.Clone был бы хорошим методом экземпляра, но это неверно для большинства других вещей.
— Джеффри Л. Уитледж

23

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

8
+1 За решение проблемы полиморфного клонирования; значительное применение клонирования.
— Samis

18

Хороший аргумент в пользу реализации clone () с использованием конструктора защищенной копии.

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

Так что это не вопрос «против». Для правильной работы вам могут понадобиться как конструкторы копирования, так и интерфейс клонирования.

(Хотя рекомендуемый открытый интерфейс - это интерфейс Clone (), а не основанный на конструкторе.)

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

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


"не должно беспокоить вызывающего абонента". Я не мог больше согласиться, но вот я пытаюсь выяснить, будет ли List <T> aList = new List <T> (aFullListOfT) делать глубокую копию (что я хочу) или мелкую копию (которая сломает мой код) и должен ли я реализовать другой способ выполнения работы!
— ThunderGr

3
Список <T> слишком общий (ха-ха) для клонирования, чтобы иметь смысл. В вашем случае это, конечно, только копия списка, а НЕ объекты, на которые указывает список. Манипулирование новым списком не повлияет на первый список, но объекты такие же, и, если они не являются неизменяемыми, они из первого набора изменятся, если вы измените объекты во втором наборе. Если бы в вашей библиотеке была операция list.Clone (), вы должны ожидать, что результатом будет полный клон, например «не изменится, когда я что-то сделаю с первым». это также применимо к содержащимся в нем объектам.
— DanO

1
List <T> не будет знать больше о правильном клонировании своего содержимого, чем вы. Если базовый объект неизменен, все в порядке. В противном случае, если у базового объекта есть метод Clone (), вам придется его использовать. List <T> aList = new List <T> (aFullListOfT.Select (t = t.Clone ())
— DanO

1
+1 за гибридный подход. У обоих подходов есть свои преимущества и недостатки, но, похоже, это дает больше преимуществ в целом.
— Кайл Баран

12

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


5
@ Грант, как конструктор передает намерение? IOW, если объект занял себя в конструкторе, будет ли копия глубокой или неглубокой? В противном случае я полностью согласен с предложением DeepCopy () (или другим).
— Marc

7
Я бы сказал, что конструктор почти так же неясен, как интерфейс ICloneable - вам нужно будет прочитать документацию / код API, чтобы узнать, делает ли он глубокий клон или нет. Я просто определяю IDeepCloneable<T>интерфейс с помощью DeepClone()метода.
— Kent Boogaart

2
@Jon - Реакция никогда не заканчивается!
— Grant Crofton

@Marc, @Kent - да, честно, конструктор, наверное, тоже не лучшая идея.
— Grant Crofton

3
Кто-нибудь видел использование, когда iCloneable использовался для объекта неизвестного типа? Вся суть интерфейсов в том, что их можно использовать на объектах неизвестного типа; в противном случае можно просто сделать Clone стандартным методом, возвращающим рассматриваемый тип.
— supercat

12

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

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 ().


2
Хороший аргумент в пользу подхода, основанного на интерфейсе.
— DanO

4

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

Я не считаю, что общедоступный конструктор копирования более ясен в этом отношении.

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


3

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

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

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
}

6
Использование сериализации как средства реализации глубокого клонирования не имеет отношения к вопросу о том, следует ли отображать глубокий клон как объект или метод.
— Kent Boogaart

1
Я думаю, что это еще одна действенная альтернатива. Я не думал, что он ограничился этими двумя методами глубокого копирования.
— Shaun Bowe

5
@Kent Boogaart - Учитывая, что OP начинается со строки «В C #, каков предпочтительный способ добавления функциональности (глубокого) копирования в класс», я думаю, что для Шона достаточно справедливо предлагать различные альтернативы. Этот трюк может быть полезен, особенно в устаревшем сценарии, когда у вас есть большое количество классов, для которых вы хотите реализовать функциональность клонирования; не такой легкий, как прямая реализация собственного клона, но тем не менее полезный. Если бы люди никогда не предлагали альтернативы «вы придумали ...» на мои вопросы, я бы не узнал почти так много, как я за эти годы.
— Роб Левин

2

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

Для меня:

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

выглядело как более четкое заявление о намерениях.


0

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

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

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

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

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

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


0

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.