¿Por qué no ICloneable <T>?

223

¿Hay alguna razón particular por la que ICloneable<T>no existe un genérico ?

Sería mucho más cómodo si no tuviera que lanzarlo cada vez que clono algo.

Bluenuance
fuente
66
¡No! Con el debido respeto a las "razones", estoy de acuerdo con usted, ¡deberían haberlo implementado!
Shimmy Weitzhandler
Hubiera sido bueno para Microsoft haber definido (el problema con las interfaces brew-your-own es que las interfaces en dos conjuntos diferentes serán incompatibles, incluso si son semánticamente idénticas). Si estuviera diseñando la interfaz, tendría tres miembros, Clone, Self y CloneIfMutable, todos los cuales devolverían una T (el último miembro devolvería Clone o Self, según corresponda). El miembro Self haría posible aceptar un ICloneable (de Foo) como parámetro y luego usarlo como un Foo, sin necesidad de una conversión tipográfica.
supercat
Eso permitiría una jerarquía de clases de clonación adecuada, donde las clases heredables exponen un método de "clonación" protegido y tienen derivados sellados que exponen uno público. Por ejemplo, uno podría tener Pile, CloneablePile: Pile, EnhancedPile: Pile y CloneableEnhancedPile: EnhancedPile, ninguno de los cuales se rompería si se clonara (aunque no todos expongan un método de clonación público), y LaterEnhancedPile: EnhancedPile (que se rompería si está clonado, pero no expone ningún método de clonación). Una rutina que acepta un ICloneable (de Pile) podría aceptar un CloneablePile o un CloneableEnhancedPile ...
supercat
... a pesar de que CloneableEnhancedPile no hereda de CloneablePile. Tenga en cuenta que si EnhancedPile se hereda de CloneablePile, LaterEnhancedPile tendría que exponer un método de clonación pública y podría pasarse a un código que esperaría clonarlo, violando el Principio de sustituibilidad de Liskov. Dado que CloneableEnhancedPile implementaría ICloneable (Of EnhancedPile) y, por implicación, ICloneable (Of Pile), podría pasarse a una rutina que espera un derivado clonable de Pile.
supercat

Respuestas:

111

ICloneable se considera una API incorrecta ahora, ya que no especifica si el resultado es una copia profunda o superficial. Creo que es por eso que no mejoran esta interfaz.

Probablemente pueda hacer un método de extensión de clonación escrito, pero creo que requeriría un nombre diferente ya que los métodos de extensión tienen menos prioridad que los originales.

Andrey Shchekin
fuente
8
No estoy de acuerdo, sea malo o no malo, es muy útil en algún momento para la clonación SHALLOW, y en esos casos realmente se necesita el Clon <T>, para guardar un boxeo y unboxing innecesarios.
Shimmy Weitzhandler
2
@AndreyShchekin: ¿Qué no está claro acerca de la clonación profunda frente a la superficial? Si List<T>tuviera un método de clonación, esperaría que produjera uno List<T>cuyos elementos tengan las mismas identidades que los de la lista original, pero esperaría que cualquier estructura de datos internos se duplicara según sea necesario para garantizar que nada de lo que se haga en una lista afecte Las identidades de los elementos almacenados en el otro. ¿Dónde está la ambigüedad? Un problema mayor con la clonación viene con una variación del "problema del diamante": si CloneableFoohereda de [no clonable públicamente] Foo, debería CloneableDerivedFooderivar de ...
supercat
1
@supercat: No puedo decir que entiendo completamente sus expectativas, ya que, ¿qué considerará identityde la lista en sí, por ejemplo (en el caso de la lista de listas)? Sin embargo, ignorando eso, su expectativa no es la única idea posible que las personas puedan tener al llamar o implementar Clone. ¿Qué sucede si los autores de la biblioteca que implementan alguna otra lista no siguen sus expectativas? La API debe ser trivialmente inequívoca, no podría decirse inequívoca.
Andrey Shchekin
2
@supercat Entonces, estás hablando de una copia superficial. Sin embargo, no hay nada fundamentalmente malo en una copia profunda, por ejemplo, si desea realizar una transacción reversible en objetos en memoria. Su expectativa se basa en su caso de uso, otras personas pueden tener expectativas diferentes. Si colocan sus objetos en sus objetos (o viceversa), los resultados serán inesperados.
Andrey Shchekin
55
@supercat no tiene en cuenta que es parte de .NET framework, no parte de su aplicación. así que si haces una clase que no hace clonación profunda, entonces alguien más hace una clase que sí hace clonación profunda y llama Clonea todas sus partes, no funcionará de manera predecible, dependiendo de si esa parte fue implementada por ti o esa persona a quien le gusta la clonación profunda. su punto sobre los patrones es válido, pero tener IMHO en API no es lo suficientemente claro: debe llamarse ShallowCopypara enfatizar el punto o no proporcionarse en absoluto.
Andrey Shchekin
141

Además de la respuesta de Andrey (con la que estoy de acuerdo, +1): cuando ICloneable haya terminado, también puede elegir una implementación explícita para que el público Clone()devuelva un objeto escrito:

public Foo Clone() { /* your code */ }
object ICloneable.Clone() {return Clone();}

Por supuesto, hay un segundo problema con una ICloneable<T>herencia genérica .

Si tengo:

public class Foo {}
public class Bar : Foo {}

Y lo implementé ICloneable<T>, ¿entonces lo implemento ICloneable<Foo>? ICloneable<Bar>? Empiezas rápidamente a implementar muchas interfaces idénticas ... Compara con un elenco ... ¿y es realmente tan malo?

Marc Gravell
fuente
10
+1 Siempre pensé que el problema de covarianza era la verdadera razón
Tamas Czinege
Hay un problema con esto: la implementación explícita de la interfaz debe ser privada , lo que causaría problemas en caso de que Foo en algún momento necesite ser enviado a ICloneable ... ¿Me estoy perdiendo algo aquí?
Joel en Go
3
Mi error: resulta que, a pesar de ser definido como privado, el método se llamará si Foo se lanza a IClonable (CLR a través de C # 2nd Ed, p.319). Parece una decisión de diseño extraña, pero ahí está. Entonces Foo.Clone () da el primer método, y ((ICloneable) Foo) .Clone () da el segundo método.
Joel en Go
En realidad, las implementaciones explícitas de interfaz son, estrictamente hablando, parte de la API pública. En eso se pueden llamar públicamente a través del casting.
Marc Gravell
8
La solución propuesta parece genial al principio ... hasta que te das cuenta de que en una jerarquía de clases, quieres que 'public Foo Clone ()' sea virtual y se anule en clases derivadas para que puedan devolver su propio tipo (y clonar sus propios campos de curso). Lamentablemente, no puede cambiar el tipo de retorno en una anulación (el problema de covarianza mencionado). Entonces, vuelves al círculo completo al problema original nuevamente: la implementación explícita realmente no te compra mucho (aparte de devolver el tipo base de tu jerarquía en lugar de 'objeto'), y vuelves a emitir la mayoría de tus Clonar () resultados de llamadas. ¡Qué asco!
Ken Beckett
18

Tengo que preguntar, ¿qué haría exactamente con la interfaz además de implementarla? Por lo general, las interfaces solo son útiles cuando se emite (es decir, esta clase admite 'IBar'), o tiene parámetros o establecedores que lo toman (es decir, tomo un 'IBar'). Con ICloneable, revisamos todo el Framework y no pudimos encontrar un solo uso en ningún lugar que fuera algo más que una implementación del mismo. Tampoco hemos podido encontrar ningún uso en el 'mundo real' que también haga algo más que implementarlo (en las ~ 60,000 aplicaciones a las que tenemos acceso).

Ahora, si solo desea aplicar un patrón que desea que implementen sus objetos 'clonables', es un uso completamente bueno, y continúe. También puede decidir exactamente qué significa "clonación" para usted (es decir, profundo o poco profundo). Sin embargo, en ese caso, no es necesario que nosotros (el BCL) lo definamos. Solo definimos abstracciones en el BCL cuando existe la necesidad de intercambiar instancias escritas como esa abstracción entre bibliotecas no relacionadas.

David Kean (equipo BCL)

David Kean
fuente
1
ICloneable no genérico no es muy útil, pero ICloneable<out T>podría ser bastante útil si se hereda de ISelf<out T>, con un único método Selfde tipo T. Uno no necesita a menudo "algo que se pueda clonar", pero es muy posible que necesite uno Tque se pueda clonar. Si se implementa un objeto clonable ISelf<itsOwnType>, una rutina que necesita un Tclonable puede aceptar un parámetro de tipo ICloneable<T>, incluso si no todos los derivados clonables Tcomparten un ancestro común.
supercat
Gracias. Eso es similar a la situación de lanzamiento que mencioné anteriormente (es decir, '¿esta clase admite' IBar '?). Lamentablemente, solo hemos encontrado escenarios muy limitados y aislados en los que realmente harías uso del hecho de que T es clonable. ¿Tienes situaciones en mente?
David Kean
Una cosa que a veces es incómoda en .net es administrar colecciones mutables cuando se supone que el contenido de una colección debe verse como un valor (es decir, querré saber más adelante el conjunto de elementos que están en la colección ahora). Algo así ICloneable<T>podría ser útil para eso, aunque un marco más amplio para mantener clases mutables e inmutables paralelas podría ser más útil. En otras palabras, el código que necesita ver qué tipo de contenido Foocontiene pero que no va a mutarlo ni a esperar que nunca cambie podría usar un IReadableFoo, mientras que ...
supercat
... el código que quiere contener el contenido de Foopodría usar un ImmutableFoocódigo while que quiere manipularlo podría usar un MutableFoo. El código dado cualquier tipo de IReadableFoodebería poder obtener una versión mutable o inmutable. Tal marco sería bueno, pero desafortunadamente no puedo encontrar ninguna manera agradable de configurar las cosas de manera genérica. Si hubiera una manera consistente de hacer un contenedor de solo lectura para una clase, tal cosa podría usarse en combinación con ICloneable<T>para hacer una copia inmutable de una clase que contiene T'.
supercat
@supercat Si desea clonar a List<T>, de modo que el clonado List<T>es una nueva colección que contiene punteros a todos los mismos objetos de la colección original, hay dos formas sencillas de hacerlo sin ellos ICloneable<T>. El primero es el Enumerable.ToList()método de extensión: List<foo> clone = original.ToList();el segundo es el List<T>constructor que toma un IEnumerable<T>: List<foo> clone = new List<foo>(original);sospecho que el método de extensión probablemente solo está llamando al constructor, pero ambos harán lo que está solicitando. ;)
CptRobby
12

Creo que la pregunta "por qué" es innecesaria. Hay muchas interfaces / clases / etc ... lo cual es muy útil, pero no es parte de la biblioteca base .NET Frameworku.

Pero, principalmente, puedes hacerlo tú mismo.

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(); }
}
TcKs
fuente
Cualquier método de clonación viable para una clase heredable debe usar Object.MemberwiseClone como punto de partida (o usar la reflexión) ya que de lo contrario no hay garantía de que el clon sea del mismo tipo que el objeto original. Tengo un patrón bastante bonito si te interesa.
supercat
@supercat, ¿por qué no responderlo entonces?
nawfal
"Hay muchas interfaces / clases / etc ... lo cual es muy útil, pero no es parte de la biblioteca base de .NET Frameworku". ¿Puedes darnos ejemplos?
Krythic
1
@Krythic es decir: estructuras de datos avanzadas como b-tree, red-black tree, circle buffer. En el '09 no había tuplas, referencias genéricas débiles, colecciones concurrentes ...
TcKs
10

Es muy fácil escribir la interfaz usted mismo si la necesita:

public interface ICloneable<T> : ICloneable
        where T : ICloneable<T>
{
    new T Clone();
}
Mauricio Scheffer
fuente
Incluí un fragmento ya que debido al honor de los derechos de copia, el enlace se rompió ...
Shimmy Weitzhandler
2
¿Y cómo se supone que funciona esa interfaz? Para que el resultado de una operación de clonación se pueda convertir al tipo T, el objeto que se está clonando tiene que derivar de T. No hay forma de establecer una restricción de tipo de este tipo. Hablando de manera realista, lo único que puedo ver el resultado de la devolución de iCloneable es un tipo iCloneable.
supercat
@supercat: sí, eso es exactamente lo que devuelve Clone (): una T que implementa ICloneable <T>. MbUnit ha estado usando esta interfaz durante años , así que sí, funciona. En cuanto a la implementación, eche un vistazo a la fuente de MbUnit.
Mauricio Scheffer
2
@Mauricio Scheffer: veo lo que está pasando. Ningún tipo implementa realmente iCloneable (de T). En cambio, cada tipo implementa iCloneable (de sí mismo). Supongo que eso funcionaría, aunque todo lo que hace es mover un typecast. Es una pena que no haya forma de declarar un campo como que tiene una restricción de herencia y una interfaz; sería útil que un método de clonación devuelva un objeto de un tipo base semi-clonable (clonación expuesta solo como método protegido), pero con una restricción de tipo clonable. Eso permitiría a un objeto aceptar derivados clonables de derivados semi-clonables de un tipo base.
supercat
@Mauricio Scheffer: Por cierto, sugeriría que ICloneable (de T) también admita una propiedad Self de solo lectura (de tipo de retorno T). Esto permitiría un método para aceptar un ICloneable (de Foo) y usarlo (a través de la propiedad Self) como Foo.
supercat
9

Habiendo leído recientemente el artículo ¿Por qué copiar un objeto es algo terrible? , Creo que esta pregunta necesita una confirmación adicional. Otras respuestas aquí proporcionan buenos consejos, pero aún así la respuesta no está completa, ¿por qué no ICloneable<T>?

  1. Uso

    Entonces, tienes una clase que lo implementa. Mientras que anteriormente tenía un método que quería ICloneable, ahora tiene que ser genérico para aceptar ICloneable<T>. Necesitarías editarlo.

    Entonces, podría haber obtenido un método que verifica si un objeto is ICloneable. ¿Ahora que? No puede hacerlo is ICloneable<>y, como no conoce el tipo de objeto en el tipo de compilación, no puede hacer que el método sea genérico. Primer problema real.

    Por lo tanto, debe tener ambos ICloneable<T>y ICloneable, el primero implementando el segundo. Por lo tanto, un implementador necesitaría implementar ambos métodos, object Clone()y T Clone(). No, gracias, ya tenemos suficiente diversión con IEnumerable.

    Como ya se señaló, también existe la complejidad de la herencia. Si bien puede parecer que la covarianza resuelve este problema, un tipo derivado necesita implementarse ICloneable<T>de su propio tipo, pero ya existe un método con la misma firma (= parámetros, básicamente), el Clone()de la clase base. Hacer explícita su nueva interfaz de método de clonación no tiene sentido, perderá la ventaja que buscaba al crear ICloneable<T>. Entonces agregue la newpalabra clave. ¡Pero no olvide que también necesitaría anular la clase base ' Clone()(la implementación debe ser uniforme para todas las clases derivadas, es decir, para devolver el mismo objeto de cada método de clonación, por lo que el método de clonación base debe ser virtual)! Pero, desafortunadamente, no puedes ambos overrideynewmétodos con la misma firma. Al elegir la primera palabra clave, perdería el objetivo que deseaba tener al agregar ICloneable<T>. Al elegir el segundo, rompería la interfaz en sí misma, haciendo que los métodos que deberían hacer lo mismo devuelvan diferentes objetos.

  2. Punto

    Desea ICloneable<T>comodidad, pero la comodidad no es para lo que están diseñadas las interfaces, su significado es (en general, OOP) para unificar el comportamiento de los objetos (aunque en C #, se limita a unificar el comportamiento externo, por ejemplo, los métodos y propiedades, no su funcionamiento)

    Si la primera razón aún no lo ha convencido, podría objetar que ICloneable<T>también podría funcionar de manera restrictiva, para limitar el tipo devuelto por el método de clonación. Sin embargo, el programador desagradable puede implementar ICloneable<T>donde T no es el tipo que lo está implementando. Entonces, para lograr su restricción, puede agregar una buena restricción al parámetro genérico:
    public interface ICloneable<T> : ICloneable where T : ICloneable<T>
    ciertamente más restrictivo que el que no tiene where, aún no puede restringir que T sea ​​el tipo que está implementando la interfaz (puede derivar ICloneable<T>de un tipo diferente que lo implementa).

    Verá, incluso este propósito no se pudo lograr (el original ICloneabletambién falla en esto, ninguna interfaz puede realmente limitar el comportamiento de la clase implementadora).

Como puede ver, esto demuestra que hacer que la interfaz genérica sea difícil de implementar por completo y realmente innecesaria e inútil.

Pero volviendo a la pregunta, lo que realmente buscas es tener consuelo al clonar un objeto. Hay dos maneras de hacerlo:

Métodos adicionales

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

Esta solución proporciona comodidad y comportamiento previsto a los usuarios, pero también es demasiado larga para implementarla. Si no quisiéramos que el método "cómodo" devolviera el tipo actual, es mucho más fácil tenerlo justo public virtual object Clone().

Entonces, veamos la solución "definitiva": ¿qué es lo que en C # tiene realmente la intención de brindarnos comodidad?

¡Métodos de extensión!

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;
}

Se llama Copiar para no chocar con los métodos de clonación actuales (el compilador prefiere los métodos declarados del tipo sobre los de extensión). La classrestricción está ahí para la velocidad (no requiere verificación nula, etc.).

Espero que esto aclare la razón por la que no hacer ICloneable<T>. Sin embargo, se recomienda no implementar ICloneableen absoluto.

IllidanS4 quiere que Mónica vuelva
fuente
Apéndice # 1: El único uso posible de un genérico ICloneablees para los tipos de valor, donde podría eludir el boxeo del método Clone, e implica que tiene el valor sin caja. Y como las estructuras se pueden clonar (superficialmente) automáticamente, no hay necesidad de implementarlo (a menos que lo especifique, significa copia profunda).
IllidanS4 quiere que Mónica regrese el
2

Aunque la pregunta es muy antigua (5 años después de escribir estas respuestas :) y ya fue respondida, pero encontré que este artículo responde la pregunta bastante bien, compruébelo aquí

EDITAR:

Aquí está la cita del artículo que responde a la pregunta (asegúrese de leer el artículo completo, incluye otras cosas interesantes):

Hay muchas referencias en Internet que apuntan a una publicación de blog de 2003 de Brad Abrams, en ese momento empleado en Microsoft, en la que se discuten algunas ideas sobre ICloneable. La entrada del blog se puede encontrar en esta dirección: Implementación de ICloneable . A pesar del título engañoso, esta entrada del blog llama a no implementar ICloneable, principalmente debido a la confusión superficial / profunda. El artículo termina en una sugerencia directa: si necesita un mecanismo de clonación, defina su propia metodología de clonación o copia y asegúrese de documentar claramente si se trata de una copia profunda o superficial. Un patrón apropiado es:public <type> Copy();

Sameh Deabes
fuente
1

Un gran problema es que no podían restringir que T sea la misma clase. Por ejemplo, qué le impediría hacer esto:

interface IClonable<T>
{
    T Clone();
}

class Dog : IClonable<JackRabbit>
{
    //not what you would expect, but possible
    JackRabbit Clone()
    {
        return new JackRabbit();
    }

}

Necesitan una restricción de parámetros como:

interfact IClonable<T> where T : implementing_type
sheamus
fuente
8
Eso no parece tan malo como class A : ICloneable { public object Clone() { return 1; } /* I can return whatever I want */ }
Nikola Novak
Realmente no veo cuál es el problema. Ninguna interfaz puede obligar a las implementaciones a comportarse razonablemente. Incluso si ICloneable<T>pudiera restringir la Tcoincidencia con su propio tipo, eso no obligaría a una implementación de Clone()devolver nada remotamente parecido al objeto sobre el que fue clonado. Además, sugeriría que si uno está usando la covarianza de la interfaz, puede ser mejor que las clases que implementan ICloneableestén selladas, que la interfaz ICloneable<out T>incluya una Selfpropiedad que se espera que regrese, y ...
supercat
... hacer que los consumidores emitan o limiten a ICloneable<BaseType>o ICloneable<ICloneable<BaseType>>. El BaseTypeen cuestión debería tener un protectedmétodo para la clonación, que sería llamado por el tipo que implementa ICloneable. Este diseño permitiría la posibilidad de que uno desee tener a Container, a CloneableContainer, a FancyContainery a CloneableFancyContainer, siendo este último utilizable en un código que requiere una derivada clonable de Containero que requiere un FancyContainer(pero no le importa si es clonable).
supercat
La razón por la que preferiría tal diseño es que la cuestión de si un tipo puede clonarse de manera sensata es algo ortogonal a otros aspectos del mismo. Por ejemplo, uno podría tener un FancyListtipo que podría clonarse con sensatez, pero una derivada podría persistir automáticamente en un archivo de disco (especificado en el constructor). El tipo derivado no se pudo clonar, porque su estado se uniría al de un singleton mutable (el archivo), pero eso no debería impedir el uso del tipo derivado en lugares que necesitan la mayoría de las características de un FancyListpero no necesitarían para clonarlo
supercat
+1: esta respuesta es la única de las que he visto aquí que realmente responde la pregunta.
IllidanS4 quiere que Mónica regrese el
0

Es una muy buena pregunta ... Sin embargo, podrías hacer la tuya propia:

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

Andrey dice que se considera una mala API, pero no he escuchado nada acerca de que esta interfaz quede obsoleta. Y eso rompería toneladas de interfaces ... El método Clone debería realizar una copia superficial. Si el objeto también proporciona una copia profunda, se puede usar un clon sobrecargado (bool deep).

EDITAR: Patrón que uso para "clonar" un objeto, está pasando un prototipo en el constructor.

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

Esto elimina cualquier posible situación de implementación de código redundante. Por cierto, hablando de las limitaciones de ICloneable, ¿no depende realmente del objeto mismo decidir si se debe realizar un clon superficial o profundo, o incluso un clon parcialmente superficial / parcialmente profundo? ¿Realmente debería importarnos, siempre y cuando el objeto funcione según lo previsto? En algunas ocasiones, una buena implementación de clonación podría incluir clonación tanto superficial como profunda.

baretta
fuente
"malo" no significa obsoleto. Simplemente significa "es mejor no usarlo". No está en desuso en el sentido de que "eliminaremos la interfaz ICloneable en el futuro". Simplemente lo alientan a no usarlo y no lo actualizarán con genéricos u otras características nuevas.
jalf
Dennis: Ver las respuestas de Marc. Los problemas de covarianza / herencia hacen que esta interfaz sea prácticamente inutilizable para cualquier escenario que sea al menos marginalmente complicado.
Tamas Czinege
Sí, puedo ver las limitaciones de ICloneable, seguro ... Sin embargo, rara vez necesito usar la clonación.
baretta