Почему нет ICloneable <T>?


223

Есть ли конкретная причина, по которой дженерик ICloneable<T>не существует?

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


6
Нет! При всем уважении к «причинам», я согласен с вами, они должны были это осуществить!
Shimmy Weitzhandler

Для Microsoft было бы неплохо определить (проблема с интерфейсами brew-your-own заключается в том, что интерфейсы в двух разных сборках будут несовместимы, даже если они семантически идентичны). Если бы я проектировал интерфейс, он имел бы три члена, Clone, Self и CloneIfMutable, каждый из которых возвращал бы T (последний член либо возвращал Clone, либо Self, в зависимости от ситуации). Член Self позволил бы принять ICloneable (из Foo) в качестве параметра, а затем использовать его как Foo, без необходимости приведения типа.
суперкат

Это позволило бы создать надлежащую иерархию классов клонирования, в которой унаследованные классы предоставляют защищенный метод «клонирования» и имеют закрытые производные, которые предоставляют общедоступный. Например, можно использовать Pile, CloneablePile: Pile, EnhancedPile: Pile и CloneableEnhancedPile: EnhancedPile, ни один из которых не будет разбит при клонировании (даже если не все предоставляют открытый метод клонирования), и AdditionalEnhancedPile: EnhancedPile (который будет разбит если клонирован, но не предоставляет никакого метода клонирования). Подпрограмма, которая принимает ICloneable (из Pile), может принять CloneablePile или CloneableEnhancedPile ...
суперкат

... хотя CloneableEnhancedPile не наследуется от CloneablePile. Обратите внимание, что если EnhancedPile унаследован от CloneablePile, AdditionalEnhancedPile должен будет предоставить публичный метод клонирования и может быть передан в код, который будет ожидать его клонирования, нарушая принцип замещения Лискова. Поскольку CloneableEnhancedPile будет реализовывать ICloneable (Of EnhancedPile) и, как следствие, ICloneable (Of Pile), он может быть передан в процедуру, ожидающую клонируемого производного Pile.
суперкат

Ответы:


111

Сейчас ICloneable считается плохим API, поскольку он не определяет, является ли результат глубокой или мелкой копией. Я думаю, что именно поэтому они не улучшают этот интерфейс.

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


8
Я не согласен, плохой или не плохой, иногда он очень полезен для клонирования SHALLOW, и в этих случаях действительно нужен Clone <T>, чтобы сохранить ненужный бокс и распаковку.
Shimmy Weitzhandler

2
@AndreyShchekin: Что неясного в глубоком и мелком клонировании? Если List<T>бы у меня был метод клонирования, я ожидал бы, что он выдаст тот, List<T>чьи элементы имеют те же идентификаторы, что и в исходном списке, но я ожидаю, что любые внутренние структуры данных будут дублироваться по мере необходимости, чтобы гарантировать, что ничего, что сделано с одним списком, не повлияет в тождестве элементов , хранящиеся в других. Где двусмысленность? Более серьезная проблема с клонированием связана с вариацией «алмазной проблемы»: если она CloneableFooнаследуется от [не подлежит публичному клонированию] Foo, то должна CloneableDerivedFoo
возникать

1
@supercat: Я не могу сказать, что полностью понимаю ваши ожидания, как, например, то, что вы будете рассматривать identityв самом списке (в случае списка списков)? Однако, игнорируя это, ваши ожидания - не единственная возможная идея, которую люди могут иметь при вызове или реализации Clone. Что если авторы библиотек, реализующие какой-то другой список, не будут соответствовать вашим ожиданиям? API должен быть тривиально однозначным, а не однозначно однозначным.
Андрей Щекин

2
@supercat Итак, вы говорите о мелкой копии. Однако в глубокой копии нет ничего принципиально неправильного, например, если вы хотите сделать обратимую транзакцию для объектов в памяти. Ваши ожидания основаны на вашем случае использования, другие люди могут иметь другие ожидания. Если они поместят ваши объекты в свои объекты (или наоборот), результаты будут неожиданными.
Андрей Щекин

5
@supercat Вы не принимаете во внимание, что это часть .NET Framework, а не часть вашего приложения. так что если вы создаете класс, который не выполняет глубокое клонирование, то кто-то другой создает класс, который выполняет глубокое клонирование и вызывает Cloneвсе его части, он не будет работать предсказуемо - в зависимости от того, была ли эта часть реализована вами или этим человеком кто любит глубокое клонирование Ваша точка зрения о шаблонах верна, но наличие IMHO в API недостаточно ясно - оно либо должно вызываться ShallowCopyдля подчеркивания, либо не указывается вообще.
Андрей Щекин

141

В дополнение к ответу Андрея (с которым я согласен, +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>? Вы быстро начинаете реализовывать множество идентичных интерфейсов ... Сравните с актерами ... и неужели это так плохо?


10
+1 Я всегда думал, что проблема ковариации была настоящей причиной
Тамас Чинеге

Есть проблема с этим: явная реализация интерфейса должна быть закрытой , что могло бы вызвать проблемы, если Foo в какой-то момент нужно привести к ICloneable ... Я что-то здесь упускаю?
Джоэл в Gö

3
Моя ошибка: оказывается, что, несмотря на то, что метод определен как private, он будет вызван, если Foo будет приведен к IClonable (CLR через C # 2nd Ed, p.319). Похоже, странное дизайнерское решение, но это так. Таким образом, Foo.Clone () дает первый метод, а ((ICloneable) Foo) .Clone () дает второй метод.
Джоэл в Gö

На самом деле, явные реализации интерфейса, строго говоря, являются частью общедоступного API. В том, что они могут быть публично вызваны через кастинг.
Марк Гравелл

8
Предложенное решение на первый взгляд кажется великолепным ... пока вы не поймете, что в иерархии классов вы хотите, чтобы public Foo Clone () был виртуальным и переопределялся в производных классах, чтобы они могли возвращать свой собственный тип (и клонировать свои собственные поля курс). К сожалению, вы не можете изменить тип возвращаемого значения в переопределении (упомянутая проблема ковариации). Итак, вы снова возвращаетесь к исходной проблеме, пройдя полный круг - явная реализация на самом деле мало что дает вам (кроме возврата базового типа вашей иерархии вместо «объекта»), и вы возвращаетесь к приведению большей части своих Clone () результаты вызова. Тьфу!
Кен Беккет

18

Мне нужно спросить, что бы вы сделали с интерфейсом, кроме как реализовать его? Интерфейсы, как правило, полезны, только когда вы приводите его (т.е. поддерживает ли этот класс 'IBar') или имеют параметры или установщики, которые его принимают (т.е. я беру 'IBar'). С ICloneable - мы прошли через всю платформу и не смогли найти ни одного использования где-либо еще, кроме его реализации. Нам также не удалось найти какое-либо использование в «реальном мире», которое также делает что-то кроме реализации (в ~ 60 000 приложений, к которым у нас есть доступ).

Теперь, если вы просто хотите применить шаблон, который вы хотите, чтобы ваши «клонируемые» объекты реализовали, это вполне подходящее использование - и вперед. Вы также можете решить, что именно означает для вас «клонирование» (т. Е. Глубокое или поверхностное). Однако в этом случае нам (BCL) не нужно его определять. Мы определяем абстракции в BCL только тогда, когда необходимо обмениваться экземплярами, типизированными в качестве этой абстракции, между несвязанными библиотеками.

Дэвид Кин (команда BCL)


1
Неуниверсальный ICloneable не очень полезен, но ICloneable<out T>может быть весьма полезным, если он унаследован от ISelf<out T>одного метода Selfтипа T. Нередко не нужно «что-то клонируемое», но вполне может понадобиться то, Tчто можно клонировать. Если реализуется клонируемый объект ISelf<itsOwnType>, подпрограмма, для которой требуется клонируемый объект , Tможет принимать параметр типа ICloneable<T>, даже если не все клонируемые производные Tимеют общего предка.
Суперкат

Спасибо. Это похоже на ситуацию приведения, которую я упомянул выше (т.е. поддерживает ли этот класс 'IBar'). К сожалению, мы только придумали очень ограниченные и изолированные сценарии, в которых вы фактически использовали бы тот факт, что T является клонируемым. У вас есть ситуации?
Дэвид Кин

Одна вещь, которая иногда неудобна в .net - это управление изменяемыми коллекциями, когда содержимое коллекции предполагается рассматривать как значение (т.е. я хочу узнать позже набор элементов, которые сейчас находятся в коллекции). Для этого ICloneable<T>может быть полезно нечто подобное , хотя более полезной может быть более широкая структура для поддержки параллельных изменяемых и неизменяемых классов. Другими словами, код, который должен видеть, что Fooсодержит какой-то тип, но не собирается его мутировать или ожидать, что он никогда не изменится, мог бы использовать IReadableFoo, в то время как ...
суперкат

... код, который хочет хранить содержимое, Fooможет использовать ImmutableFooкод while, который для манипулирования им может использовать a MutableFoo. Код, заданный любым типом, IReadableFooдолжен иметь возможность получить изменяемую или неизменяемую версию. Такая структура была бы хороша, но, к сожалению, я не могу найти какой-либо хороший способ настроить вещи в общем виде. Если бы существовал непротиворечивый способ сделать обертку только для чтения для класса, такую ​​вещь можно было бы использовать в сочетании с ICloneable<T>созданием неизменной копии класса, который содержит T'.
Суперкат

@supercat Если вы хотите клонировать a List<T>, так что клонированный List<T>является новой коллекцией, содержащей указатели на все те же объекты в исходной коллекции, есть два простых способа сделать это без ICloneable<T>. Первый - это Enumerable.ToList()метод расширения List<foo> clone = original.ToList();. Второй - это List<T>конструктор, который принимает IEnumerable<T>: List<foo> clone = new List<foo>(original);Я подозреваю, что метод расширения, вероятно, просто вызывает конструктор, но оба они будут делать то, что вы запрашиваете. ;)
CptRobby

12

Я думаю, что вопрос «почему» не нужен. Существует множество интерфейсов / классов / и т. Д., Которые очень полезны, но не являются частью базовой библиотеки .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(); }
}

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

@supercat, почему бы тогда не сделать ответ?
nawfal

«Существует множество интерфейсов / классов / и т. Д., Которые очень полезны, но не являются частью базовой библиотеки .NET Frameworku». Можете ли вы привести примеры?
Krythic

1
@Krythic ie: расширенные структуры данных, такие как b-дерево, красно-черное дерево, круговой буфер. В '09 не было кортежей, общих слабых ссылок, одновременных коллекций ...
TcKs

10

Довольно просто написать интерфейс, если он вам нужен:

public interface ICloneable<T> : ICloneable
        where T : ICloneable<T>
{
    new T Clone();
}

Я включил фрагмент, так как из-за чести авторских прав ссылка была разорвана ...
Shimmy Weitzhandler

2
И как именно этот интерфейс должен работать? Чтобы результат операции клонирования был преобразован в тип T, клонируемый объект должен быть производным от T. Нет способа установить такое ограничение типа. На самом деле, единственное, что я вижу в результате возврата iCloneable, это тип iCloneable.
суперкат

@supercat: да, это именно то, что возвращает Clone (): T, который реализует ICloneable <T>. MbUnit использует этот интерфейс в течение многих лет , так что да, он работает. Что касается реализации, взгляните на источник MbUnit.
Маурисио Шеффер

2
@Mauricio Scheffer: Я вижу, что происходит. Ни один тип на самом деле не реализует iCloneable (из T). Вместо этого каждый тип реализует iCloneable (сам по себе). Думаю, это сработает, хотя все, что он делает - это перенос типа. Жаль, что нет способа объявить поле как имеющее ограничение наследования и интерфейсное ограничение; Было бы полезно, чтобы метод клонирования возвращал объект базового типа полуклонируемого (клонирование предоставляется только как защищенный метод), но с ограничением клонируемого типа. Это позволило бы объекту принимать клонируемые производные полуклонируемых производных основного типа.
суперкат

@Mauricio Scheffer: Кстати, я бы сказал, что ICloneable (из T) также поддерживает свойство Self только для чтения (возвращаемого типа T). Это позволило бы методу принять ICloneable (из Foo) и использовать его (через свойство Self) в качестве Foo.
суперкат

9

Прочитав недавно статью « Почему копирование объекта - ужасная вещь? Думаю, этот вопрос требует дополнительного уточнения. Другие ответы здесь дают хорошие советы, но все же ответ не полный - почему нет ICloneable<T>?

  1. использование

    Итак, у вас есть класс, который его реализует. Если раньше у вас был метод, который требуется 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>. Выбрав второй, вы нарушите сам интерфейс, создав методы, которые должны делать то же самое, возвращая разные объекты.

  2. точка

    Вы хотите ICloneable<T>комфорта, но комфорт - это не то, для чего предназначены интерфейсы, их смысл (в общем случае ООП) - унифицировать поведение объектов (хотя в C # он ограничен унификацией внешнего поведения, например методов и свойств, а не их работа).

    Если первая причина еще не убедила вас, вы можете возразить, что ICloneable<T>также может работать ограничительно, чтобы ограничить тип, возвращаемый методом clone. Однако противный программист может реализовать, 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вообще.


Приложение № 1: Единственное возможное использование универсального ICloneableзначения - для типов значений, где оно может обойти бокс метода Clone, и это подразумевает, что у вас есть значение без коробки. И поскольку структуры могут быть клонированы (поверхностно) автоматически, нет необходимости реализовывать их (если вы не укажете, что это означает глубокое копирование).
IllidanS4 хочет вернуть Монику

2

Хотя вопрос очень старый (5 лет с момента написания этого ответа :) и уже был дан ответ, но я обнаружил, что эта статья отвечает на вопрос довольно хорошо, проверьте его здесь

РЕДАКТИРОВАТЬ:

Вот цитата из статьи, которая отвечает на вопрос (обязательно прочитайте всю статью, она включает в себя другие интересные вещи):

В Интернете есть много ссылок, указывающих на сообщение в блоге Брэда Абрамса в 2003 году - в то время работавшее в Microsoft - в котором обсуждаются некоторые мысли о ICloneable. Запись в блоге можно найти по этому адресу: Реализация ICloneable . Несмотря на вводящее в заблуждение название, эта запись в блоге призывает не внедрять ICloneable, в основном из-за мелкой / глубокой путаницы. Статья заканчивается прямым предложением: если вам нужен механизм клонирования, определите свою собственную методику клонирования или копирования и убедитесь, что вы четко документируете, является ли она глубокой или мелкой копией. Соответствующий шаблон:public <type> Copy();


1

Большая проблема заключается в том, что они не могут ограничить 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

8
Это не так плохо, как 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или который требует a FancyContainer(но не волнует, является ли он клонируемым).
суперкат

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

+1 - этот ответ - единственный из тех, что я видел здесь, который действительно отвечает на вопрос.
IllidanS4 хочет вернуть Монику

0

Это очень хороший вопрос ... Вы можете сделать свой собственный, хотя:

interface ICloneable<T> : ICloneable
{
  new T Clone ( );
}

Андрей говорит, что это плохой API, но я ничего не слышал об устаревании этого интерфейса. И это сломало бы тонны интерфейсов ... Метод Clone должен выполнять поверхностное копирование. Если объект также обеспечивает глубокое копирование, можно использовать перегруженный клон (bool deep).

РЕДАКТИРОВАТЬ: Шаблон, который я использую для «клонирования» объекта, передает прототип в конструктор.

class C
{
  public C ( C prototype )
  {
    ...
  }
}

Это удаляет любые потенциальные избыточные ситуации реализации кода. Кстати, говоря об ограничениях ICloneable, не правда ли, сам объект решает, должен ли быть выполнен неглубокий клон или глубокий клон, или даже частично неглубокий / частично глубокий клон? Должны ли мы действительно заботиться, пока объект работает, как задумано? В некоторых случаях хорошая реализация Clone вполне может включать как поверхностное, так и глубокое клонирование.


«Плохо» не означает «не рекомендуется». Это просто означает «тебе лучше не использовать его». Это не осуждается в том смысле, что «мы удалим интерфейс ICloneable в будущем». Они просто рекомендуют вам не использовать его и не будут обновлять его с помощью дженериков или других новых функций.
jalf

Деннис: см. Ответы Марка. Проблемы ковариантности / наследования делают этот интерфейс практически непригодным для любого сценария, который по крайней мере незначительно сложен.
Тамас Чинеге

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