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


119

У C #, який є кращим способом додати (глибоку) функцію копіювання до класу? Чи варто реалізовувати конструктор копій, а точніше випливати з ICloneableта застосовувати Clone()метод?

Зауваження : Я написав "глибоко" в дужках, тому що вважав це неактуальним. Мабуть, інші не погоджуються, тому я запитав, чи потрібно конструктору / оператору / функції копію зрозуміти, який варіант копіювання він реалізує .

Відповіді:


91

Ви не повинні виходити з цього 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 Framework Design Design Guidelines та у блозі Бреда Абрамса

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


2
Якщо ви збираєтесь інтерфейс, як щодо DeepClone (з T) () і DeepClone (з 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 (), мені дуже подобається ваша відповідь, оскільки це підкреслює заплутану ситуацію, що існує щодо моєї основної функції мови програмування. Я думаю, що користувач повинен вибрати той варіант, який йому найбільше подобається.
Димитрій К.

11
Я не збираюся тут сперечатися з пунктами, але, на мою думку, абонент не повинен так сильно піклуватися про глибоке чи мілководне, коли вони дзвонять Клону (). Вони повинні знати, що вони отримують клон без недійсного загального стану. Наприклад, цілком можливо, що в глибокому клоні я не захочу глибоко клонувати кожен елемент. Все, що повинен потурбувати абонента Clone, - це те, що вони отримують нову копію, яка не містить недійсних та непідтримуваних посилань на оригінал. Виклик методу "DeepClone", здається, передає абоненту занадто багато деталей реалізації.
zumalifeguard

1
Що поганого в тому, щоб об’єкт об'єкта знав, як клонувати себе, а не копіювати статичним методом? Це відбувається в реальному світі весь час з біологічними клітинами. Клітини у вашому власному тілі зайняті клонування себе прямо зараз, коли ви це читаєте. IMO, параметр статичного методу є більш громіздким, як правило, приховує функціональність та відхиляється від використання "найменш дивної" реалізації на користь інших.
Кен Бекетт

8
@KenBeckett - причина, по якій я вважаю, що клонування - це щось, що робиться для об'єкта, - це тому, що об’єкт повинен "робити одну справу і робити це добре". Зазвичай виготовлення копій не є основною компетенцією класу, а скоріше це функціональність, на яку задіяно. Зробити клон рахунку BankAccount - це те, що ви дуже добре можете зробити, але робити клони себе не є особливістю банківського рахунку. Ваш приклад клітини не є повчальним, оскільки відтворення - це саме те, що розвивалося для клітин. Cell.Clone був би гарним методом екземпляра, але це не відповідає дійсності більшості інших речей.
Джефрі Л Вітлідж

23

Я рекомендую використовувати конструктор копій над методом клонування насамперед тому, що метод клонування не дозволить вам робити поля, readonlyякі могли бути, якби ви використовували конструктор замість цього.

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

Якщо вам потрібна більше одного типу копії (наприклад: deep / неглибока), ви можете вказати її за допомогою параметра в конструкторі копій, хоча, на моєму досвіді, я вважаю, що зазвичай потрібна суміш глибокої та дрібної копіювання.

Наприклад:

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 для вирішення питань поліморфного клонування; значне застосування клонування.
саміс

18

Є чудовий аргумент, що вам слід реалізувати clone () за допомогою захищеного конструктора копій

Краще забезпечити захищений (непублічний) конструктор копій і викликати це методом клонування. Це дає нам можливість делегувати завдання створення об'єкта до екземпляра самого класу, забезпечуючи таким чином розширюваність, а також безпечно створюючи об'єкти за допомогою захищеного конструктора копій.

Тож це не питання "проти". Можливо, вам знадобляться як конструктори копій, так і інтерфейс клонів, щоб зробити це правильно.

(Хоча рекомендований загальнодоступний інтерфейс - це інтерфейс Clone (), а не на основі конструктора.)

Не зациклюйтеся на явному глибокому чи мілкому аргументі в інших відповідях. У реальному світі це майже завжди є щось середнє - і в будь-якому випадку не повинно бути проблемою абонента.

Договір Clone () просто "не зміниться, коли я зміню перший". Яка частина графіка вам доведеться скопіювати або як уникнути нескінченних рекурсій, щоб це сталося, не повинно стосуватися абонента.


"не повинно викликати занепокоєння абонента". Я не міг би погодитися більше, але ось я намагаюся з’ясувати, чи спис <T> aList = новий Список <T> (aFullListOfT) зробить глибоку копію (чого я хочу) або дрібну копію (яка б розірвалася мій код) і чи потрібно реалізувати інший спосіб, щоб виконати роботу!
Грім

3
Список <T> є надто загальним (ха-ха) для клонування, щоб навіть мати сенс. У вашому випадку це, безумовно, лише копія списку, а НЕ об’єкти, на які вказує список. Маніпулювання новим списком не вплине на перший список, але об'єкти однакові, і якщо вони не є непорушними, вони в першому наборі зміняться, якщо ви зміните їх у другому наборі. Якщо у вашій бібліотеці була операція list.Clone (), слід очікувати, що результат буде повним клоном, оскільки в "не зміниться, коли я щось зроблю з першим". що стосується і об'єктів, що містяться.
DanO

1
Список <T> не дізнається нічого більше про те, як правильно клонувати його вміст, ніж ви. Якщо основний об'єкт незмінний, ви добре піти. В іншому випадку, якщо в базовому об'єкті є метод Clone (), вам доведеться його використовувати. Список <T> aList = новий Список <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
+1 для гібридного підходу. Обидва підходи мають перевагу та недолік, але це, мабуть, має загальну користь.
Кайл Баран

12

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


5
@Grant, як конструктор ретранслирує наміри? IOW, якщо об’єкт взяв себе у конструкторі, копія глибока чи неглибока? В іншому випадку я повністю згоден з пропозицією DeepCopy () (або іншим чином).
Марк

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

2
@Jon - Реакторинг ніколи не закінчується!
Грант Крофтон

@Marc, @Kent - так, справедливий момент, конструктор, мабуть, теж не є хорошою ідеєю.
Грант Крофтон,

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

Якщо об’єкт, який ви намагаєтесь скопіювати, є 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
}

6
Використання серіалізації як засобу здійснення глибокого клонування не має значення для питання про те, чи слід глибокий клон виходити на поверхню як ctor чи метод.
Кент Богаарт

1
Я думаю, що це ще одна вагома альтернатива. Я не думав, що він обмежується цими двома методами глибокого копіювання.
Шон Боу

5
@Kent Boogaart - Враховуючи, що ОП починається з рядка "В 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.