Чи є певна причина, чому родового ICloneable<T>
не існує?
Було б набагато зручніше, якби мені не потрібно було це робити щоразу, коли я щось клоную.
Чи є певна причина, чому родового ICloneable<T>
не існує?
Було б набагато зручніше, якби мені не потрібно було це робити щоразу, коли я щось клоную.
Відповіді:
Зараз ICloneable вважається поганим API, оскільки він не визначає, чи є результат глибокою чи неглибокою копією. Я думаю, саме тому вони не покращують цей інтерфейс.
Можливо, ви можете зробити набраний метод розширення клонування, але я думаю, що це потребує іншої назви, оскільки методи розширення мають менший пріоритет, ніж оригінальні.
List<T>
б був метод клонування, я б очікував, що він List<T>
отримає ті елементи, чиї елементи мають ті ж тотожності, що і ті, що були в початковому списку, але я би сподівався, що будь-які внутрішні структури даних будуть дублюватись за необхідності, щоб гарантувати, що нічого, зроблене для одного списку, не вплине в тотожність елементів , що зберігаються в інших. Де двозначність? Більша проблема з клонуванням виникає з варіацією "алмазної проблеми": якщо CloneableFoo
успадковується від [не публічно закритого] Foo
, має CloneableDerivedFoo
випливати з ...
identity
, наприклад, сам список, (у випадку, якщо список списків)? Однак, ігноруючи це, ваші очікування - не єдина можлива ідея, яку люди можуть мати під час дзвінка чи реалізації Clone
. Що робити, якщо автори бібліотек, які впроваджують якийсь інший список, не відповідають вашим очікуванням? API повинен бути тривіально однозначним, не може бути однозначним.
Clone
всі його частини, він не працюватиме передбачувано - залежно від того, чи була ця частина реалізована вами чи тією особою хто любить глибоке клонування. Ваша думка щодо шаблонів справедлива, але наявність IMHO в API недостатньо зрозуміла - її або слід викликати, ShallowCopy
щоб підкреслити цю точку, або взагалі не надано.
На додаток до відповіді Андрея (з якою я згоден, +1) - коли ICloneable
це буде зроблено, ви також можете вибрати явну реалізацію, щоб громадськість Clone()
повернула введений об’єкт:
public Foo Clone() { /* your code */ }
object ICloneable.Clone() {return Clone();}
Звичайно, є другий випуск із загальним ICloneable<T>
- успадкування.
Якщо я маю:
public class Foo {}
public class Bar : Foo {}
І я реалізував ICloneable<T>
, то чи реалізую ICloneable<Foo>
? ICloneable<Bar>
? Ви швидко починаєте впроваджувати безліч однакових інтерфейсів ... Порівняйте з акторською командою ... і чи справді це так погано?
Мені потрібно запитати, що саме ви зробили б з іншим інтерфейсом його реалізації? Інтерфейси, як правило, корисні лише тоді, коли ви передаєте до нього (тобто, чи підтримує цей клас 'IBar'), або у ньому є параметри або сеттери, які приймають його (тобто я приймаю 'IBar'). За допомогою ICloneable - ми пройшли всю Раму і не змогли знайти ніде одного використання ніде, що було б іншим, ніж її реалізація. Нам також не вдалося знайти використання у реальному світі, що також робить щось інше, ніж реалізовувати це (у ~ 60 000 додатках, до яких ми маємо доступ).
Тепер, якщо ви просто хочете застосувати шаблон, який ви хочете реалізовувати для своїх «клонуючих» об’єктів, це цілком чудове використання - і продовжуйте. Ви також можете вирішити, що саме означає "клонування" для вас (тобто глибоке або неглибоке). Однак у цьому випадку нам (BCL) не потрібно визначати це. Ми визначаємо абстракції в BCL лише тоді, коли виникає потреба в обміні екземплярами, введеними як ця абстракція між непов'язаними бібліотеками.
Девід Кін (команда BCL)
ICloneable<out T>
може бути дуже корисним, якщо він успадковується від ISelf<out T>
одного методу Self
типу T
. Людині часто не потрібно «щось, що може бути клоноване», але, можливо, дуже потрібне саме T
те, що може бути клоноване. Якщо об'єкт, що клонується, реалізується ISelf<itsOwnType>
, програма, яка потребує T
клонування, може приймати параметр типу ICloneable<T>
, навіть якщо не всі похідні, що підлягають клонуванню, T
мають спільного предка.
ICloneable<T>
може бути корисним для цього, хоча більш корисна може бути більш широка рамка для підтримки паралельних змінних та незмінних класів. Іншими словами, код, який повинен бачити, що Foo
містить якийсь тип, але не буде його вимкнути, і не очікує, що він ніколи не зміниться, може використовувати IReadableFoo
, в той час як ...
Foo
може використовувати ImmutableFoo
код час, який для маніпулювання ним може використовувати MutableFoo
. Код, що надається будь-якому типу, IReadableFoo
повинен мати можливість змінювати чи змінювати версію. Така рамка була б непоганою, але, на жаль, я не можу знайти жодного приємного способу налаштувати речі на загальний лад. Якби існував послідовний спосіб зробити обгортку лише для читання для класу, таку річ можна було б використовувати в поєднанні з тим, ICloneable<T>
щоб зробити незмінну копію класу, який містить T
".
List<T>
, таким чином, що клонований List<T>
- це нова колекція, що містить покажчики на всі ті самі об’єкти в оригінальній колекції, є два простих способи зробити це без ICloneable<T>
. Перший - це Enumerable.ToList()
метод розширення: List<foo> clone = original.ToList();
другий - це List<T>
конструктор, який приймає IEnumerable<T>
: List<foo> clone = new List<foo>(original);
я підозрюю, що метод розширення, ймовірно, просто викликає конструктор, але обидва вони будуть робити те, що ви вимагаєте. ;)
Я думаю, що питання "чому" зайве. Існує багато інтерфейсів / класів / тощо ..., що дуже корисно, але не є частиною базової бібліотеки .NET Frameworku.
Але, в основному, ви можете це зробити самостійно.
public interface ICloneable<T> : ICloneable {
new T Clone();
}
public abstract class CloneableBase<T> : ICloneable<T> where T : CloneableBase<T> {
public abstract T Clone();
object ICloneable.Clone() { return this.Clone(); }
}
public abstract class CloneableExBase<T> : CloneableBase<T> where T : CloneableExBase<T> {
protected abstract T CreateClone();
protected abstract void FillClone( T clone );
public override T Clone() {
T clone = this.CreateClone();
if ( object.ReferenceEquals( clone, null ) ) { throw new NullReferenceException( "Clone was not created." ); }
return clone
}
}
public abstract class PersonBase<T> : CloneableExBase<T> where T : PersonBase<T> {
public string Name { get; set; }
protected override void FillClone( T clone ) {
clone.Name = this.Name;
}
}
public sealed class Person : PersonBase<Person> {
protected override Person CreateClone() { return new Person(); }
}
public abstract class EmployeeBase<T> : PersonBase<T> where T : EmployeeBase<T> {
public string Department { get; set; }
protected override void FillClone( T clone ) {
base.FillClone( clone );
clone.Department = this.Department;
}
}
public sealed class Employee : EmployeeBase<Employee> {
protected override Employee CreateClone() { return new Employee(); }
}
Написати інтерфейс досить просто, якщо він вам потрібен:
public interface ICloneable<T> : ICloneable
where T : ICloneable<T>
{
new T Clone();
}
Нещодавно прочитавши статтю Чому копіювати об’єкт - це страшно? , Я думаю, що це питання потребує додаткового уточнення. Інші відповіді тут дають хороші поради, але все ж відповідь не є повною - чому ні ICloneable<T>
?
Використання
Отже, у вас є клас, який його реалізує. Якщо раніше у вас був метод, який хотіли ICloneable
, тепер це потрібно прийняти загальним чином ICloneable<T>
. Вам потрібно буде відредагувати його.
Тоді ви могли отримати метод, який перевіряє, чи є об'єкт is ICloneable
. Що тепер? Ви не можете зробити, is ICloneable<>
і як ви не знаєте тип об'єкта при компіляційному типі, ви не можете зробити метод загальним. Перша реальна проблема.
Отже, вам потрібно мати ICloneable<T>
і те ICloneable
, і перше, що реалізує останнє. Таким чином, реалізатору потрібно буде реалізувати обидва методи - object Clone()
і T Clone()
. Ні, дякую, нам уже досить весело IEnumerable
.
Як уже вказувалося, існує також складність успадкування. Хоча коваріація може здатися вирішити цю проблему, похідному типу потрібно реалізувати ICloneable<T>
власний тип, але вже існує метод з тим же підписом (= параметри, в основному) - Clone()
базового класу. Зробити явний новий інтерфейс методу клонування безглуздим, ви втратите перевагу, яку ви шукали при створенні ICloneable<T>
. Тому додайте new
ключове слово. Але не забувайте, що вам також знадобиться переосмислити базовий клас ' Clone()
(реалізація повинна залишатися рівномірною для всіх похідних класів, тобто повертати один і той же об'єкт від кожного методу клонування, тому метод базового клонування повинен бути virtual
)! Але, на жаль, ви не можете обоєoverride
іnew
методи з однаковим підписом. Вибираючи перше ключове слово, ви втратите мету, яку хотіли мати при додаванні ICloneable<T>
. Вибираючи другий, ви зламаєте сам інтерфейс, роблячи методи, які повинні робити те саме, що повертають різні об'єкти.
Точка
Ви хочете ICloneable<T>
для комфорту, але комфорт - це не те, для чого призначені інтерфейси, їх значення - це (загалом OOP) для уніфікації поведінки об'єктів (хоча в C # вона обмежується уніфікацією зовнішньої поведінки, наприклад, методів та властивостей, а не їхня робота).
Якщо перша причина вас ще не переконала, ви можете заперечити, що ICloneable<T>
також може працювати обмежувально, щоб обмежити тип, повернений із методу клонування. Однак неприємний програміст може реалізувати ICloneable<T>
там, де T - не той тип, який його реалізує. Отже, щоб досягти обмеження, ви можете додати приємне обмеження до загального параметра:
public interface ICloneable<T> : ICloneable where T : ICloneable<T>
Безумовно, більш обмежуючим, ніж той, без якого where
, ви все одно не можете обмежувати, що T - тип, що реалізує інтерфейс (ви можете отримати ICloneable<T>
інший тип що реалізує це).
Розумієте, навіть цієї мети не вдалося досягти (оригінал ICloneable
також не вдається, жоден інтерфейс не може по-справжньому обмежувати поведінку класу реалізації).
Як бачимо, це доводить, що загальний інтерфейс є важким для повного впровадження, а також справді непотрібним і марним.
Але повернемось до питання, чого ви дійсно прагнете - це мати комфорт під час клонування предмета. Є два способи зробити це:
public class Base : ICloneable
{
public Base Clone()
{
return this.CloneImpl() as Base;
}
object ICloneable.Clone()
{
return this.CloneImpl();
}
protected virtual object CloneImpl()
{
return new Base();
}
}
public class Derived : Base
{
public new Derived Clone()
{
return this.CloneImpl() as Derived;
}
protected override object CloneImpl()
{
return new Derived();
}
}
Це рішення забезпечує як комфорт, так і цілеспрямовану поведінку користувачів, але це також занадто довго для реалізації. Якщо ми не хотіли, щоб «комфортний» метод повертав поточний тип, це набагато простіше просто public virtual object Clone()
.
Тож давайте подивимось на "остаточне" рішення - що C # насправді має намір забезпечити нам комфорт?
public class Base : ICloneable
{
public virtual object Clone()
{
return new Base();
}
}
public class Derived : Base
{
public override object Clone()
{
return new Derived();
}
}
public static T Copy<T>(this T obj) where T : class, ICloneable
{
return obj.Clone() as T;
}
Він називається Copy, щоб не стикатися з поточними методами Clone (компілятор надає перевагу власним заявленим методам типу над розширеннями). class
Обмеження існує для швидкості (не вимагає нульовий перевірки і т.д.).
Я сподіваюся, що це пояснює причину, чому її не зробити ICloneable<T>
. Однак рекомендується взагалі не виконувати ICloneable
.
ICloneable
для типів значень, де воно може обійти бокс методом Clone, і це означає, що у вас є значення без коробки. Оскільки структури можуть бути клоновані (неглибоко) автоматично, немає необхідності в їх реалізації (якщо тільки ви не зробите це конкретним, що означає глибоку копію).
Хоча запитання дуже давнє (5 років з моменту написання цієї відповіді :) і на нього вже відповіли, але я вважаю, що ця стаття відповідає на питання досить добре, перевірте її тут
Редагувати:
Ось цитата зі статті, яка відповідає на запитання (обов’язково прочитайте повну статтю, вона включає інші цікаві речі):
В Інтернеті є багато посилань, які вказують на повідомлення про блог Бреда Абрамса в 2003 році, який працював в Microsoft, і в якому обговорюються деякі думки про ICloneable. Запис у блозі можна знайти за цією адресою: Реалізація ICloneable . Незважаючи на оманливий заголовок, цей запис у блозі закликає не застосовувати ICloneable, головним чином через дрібну / глибоку плутанину. Стаття закінчується прямою пропозицією: Якщо вам потрібен механізм клонування, визначте власний метод Clone або Copy і переконайтесь, що ви чітко документуєте, чи це копія глибокої чи дрібної. Відповідний зразок:
public <type> Copy();
Велика проблема полягає в тому, що вони не могли обмежити T тим самим класом. Для прикладу, що б заважало вам це робити:
interface IClonable<T>
{
T Clone();
}
class Dog : IClonable<JackRabbit>
{
//not what you would expect, but possible
JackRabbit Clone()
{
return new JackRabbit();
}
}
Їм потрібно обмеження параметрів, наприклад:
interfact IClonable<T> where T : implementing_type
class A : ICloneable { public object Clone() { return 1; } /* I can return whatever I want */ }
ICloneable<T>
може обмежитися T
відповідності своєму типу, це не змусить реалізацію Clone()
повернути щось віддалено, що нагадує об'єкт, на який його клонували. Далі я б припустив, що якщо використовується коваріація інтерфейсу, можливо, найкраще мати класи, які реалізують, ICloneable
бути запечатаними, щоб інтерфейс ICloneable<out T>
включав Self
властивість, яка, як очікується, повернеться, і ...
ICloneable<BaseType>
або ICloneable<ICloneable<BaseType>>
. BaseType
У питанні повинен мати protected
метод клонування, який буде називатися по типу , який реалізує ICloneable
. Цей дизайн дозволив би передбачити можливість того, що ви можете мати a Container
, a CloneableContainer
, a FancyContainer
і a CloneableFancyContainer
, останній може бути використаний в коді, який вимагає похідної, що може бути клонований, Container
або який вимагає FancyContainer
(але не важливо, чи він може бути клонований).
FancyList
тип, який міг би бути клонований розумно, але похідна може автоматично зберігати свій стан у файлі диска (вказаному в конструкторі). Похідний тип не можна було клонувати, оскільки його стан буде приєднано до стану мутаційного сингтона (файлу), але це не повинно перешкоджати використанню похідного типу в місцях, які потребують більшості функцій, FancyList
але не потрібні клонувати його.
Це дуже гарне запитання ... Ти можеш зробити свій власний, хоча:
interface ICloneable<T> : ICloneable
{
new T Clone ( );
}
Андрій каже, що це вважається поганим API, але я нічого не чув про те, щоб цей інтерфейс застарів. І це зламає тонни інтерфейсів ... Метод Clone повинен виконувати дрібну копію. Якщо об’єкт також забезпечує глибоку копію, може бути використаний перевантажений клон (bool deep).
EDIT: Шаблон, який я використовую для "клонування" об'єкта, передає прототип в конструктор.
class C
{
public C ( C prototype )
{
...
}
}
Це видаляє будь-які можливі надлишкові ситуації впровадження коду. BTW, говорячи про обмеження ICloneable, чи не насправді сам об'єкт вирішує, чи слід виконувати дрібний чи глибокий клон чи навіть частково мілководний / частково глибокий клон? Чи нас справді турбувати, поки об’єкт працює за призначенням? У деяких випадках хороша реалізація клонів може дуже добре включати як дрібне, так і глибоке клонування.