Por que nenhum ICloneable <T>?


222

Existe uma razão específica para a inexistência de um genérico ICloneable<T>?

Seria muito mais confortável, se eu não precisasse transmiti-lo toda vez que clonasse algo.


6
Não! com todo o respeito pelas 'razões', concordo com você, eles deveriam ter implementado!
Shimmy Weitzhandler

Teria sido uma coisa agradável para a Microsoft ter definido (o problema com as próprias interfaces é que as interfaces em dois assemblies diferentes serão incompatíveis, mesmo que sejam semanticamente idênticas). Se eu estivesse projetando a interface, ela teria três membros, Clone, Self e CloneIfMutable, todos retornando um T (o último membro retornaria Clone ou Self, conforme apropriado). O membro Self tornaria possível aceitar um ICloneable (de Foo) como parâmetro e depois usá-lo como Foo, sem a necessidade de uma conversão de tipo.
supercat

Isso permitiria uma hierarquia de classes de clonagem adequada, onde classes herdáveis ​​expõem um método "clone" protegido e selavam derivativos que expõem um método público. Por exemplo, pode-se ter Pile, CloneablePile: Pile, EnhancedPile: Pile e CloneableEnhancedPile: EnhancedPile, nenhum dos quais seria quebrado se clonado (mesmo que nem todos expusessem um método de clonagem pública) e FurtherEnhancedPile: EnhancedPile (que seria quebrado se clonado, mas não expõe nenhum método de clonagem). Uma rotina que aceita um ICloneable (de Pilha) poderia aceitar um CloneablePile ou um CloneableEnhancedPile ...
supercat

... mesmo que CloneableEnhancedPile não herda de CloneablePile. Observe que, se o EnhancedPile herdado de CloneablePile, o FurtherEnhancedPile precisaria expor um método público de clonagem e poderia ser passado para o código que seria esperado para cloná-lo, violando o Princípio de substituibilidade de Liskov. Como o CloneableEnhancedPile implementaria o ICloneable (Of EnhancedPile) e, por implicação, o ICloneable (Of Pile), ele poderia ser passado para uma rotina esperando um derivado clonável do Pile.
supercat 12/01

Respostas:


111

O ICloneable é considerado uma API incorreta agora, pois não especifica se o resultado é uma cópia profunda ou superficial. Eu acho que é por isso que eles não melhoram essa interface.

Você provavelmente pode fazer um método de extensão de clonagem digitado, mas acho que exigiria um nome diferente, já que os métodos de extensão têm menos prioridade que os originais.


8
Eu discordo, ruim ou não ruim, é muito útil em algum momento para a clonagem SHALLOW, e nesses casos é realmente necessário o Clone <T>, para salvar um boxe e unboxing desnecessários.
Shimmy Weitzhandler

2
@AndreyShchekin: O que não está claro sobre a clonagem profunda versus superficial? Se List<T>tivesse um método clone, esperaria que ele produzisse um List<T>cujos itens tenham as mesmas identidades que os da lista original, mas esperaria que quaisquer estruturas de dados internas fossem duplicadas conforme necessário para garantir que nada feito em uma lista afete as identidades dos itens armazenados no outro. Onde está a ambiguidade? Um problema maior com a clonagem vem com uma variação do "problema diamante": se CloneableFooherda de [não cloneable publicamente] Foo, deve CloneableDerivedFooderivar de ...
supercat

1
@ supercat: Não posso dizer que entendo totalmente sua expectativa, como o que você considerará uma identitylista em si, por exemplo (no caso de lista de listas)? No entanto, ignorando isso, sua expectativa não é a única ideia possível que as pessoas possam ter ao ligar ou implementar Clone. E se os autores da biblioteca que implementam alguma outra lista não atenderem às suas expectativas? A API deve ser trivialmente inequívoca, não indiscutivelmente inequívoca.
Andrey Shchekin

2
@ supercat Então você está falando de uma cópia superficial. No entanto, não há nada de fundamentalmente errado em uma cópia profunda, por exemplo, se você deseja fazer uma transação reversível em objetos na memória. Sua expectativa é baseada no seu caso de uso, outras pessoas podem ter expectativas diferentes. Se eles colocarem seus objetos nos objetos deles (ou vice-versa), os resultados serão inesperados.
Andrey Shchekin

5
@ supercat você não leva em consideração que faz parte do .NET framework, não faz parte do seu aplicativo. portanto, se você criar uma classe que não faça a clonagem profunda, alguém fará uma classe que faça a clonagem profunda e chamará Clonetodas as suas partes, não funcionará previsivelmente - dependendo se essa parte foi implementada por você ou por essa pessoa quem gosta de clonagem profunda. seu ponto de vista sobre os padrões é válido, mas ter o IMHO na API não é claro o suficiente - ele deve ser chamado ShallowCopypara enfatizar o ponto ou não ser fornecido.
Andrey Shchekin

141

Além de resposta de Andrey (que eu concordo com, +1) - quando ICloneable é feito, você pode também escolher implementação explícita para fazer o público Clone()retornar um objeto digitado:

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

Claro que há um segundo problema com uma ICloneable<T>herança genérica .

Se eu tiver:

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

E eu implementei ICloneable<T>, então eu implemento ICloneable<Foo>? ICloneable<Bar>? Você começa rapidamente a implementar muitas interfaces idênticas ... Compare com um elenco ... e é realmente tão ruim assim?


10
+1 Eu sempre pensei que o problema covariância era a verdadeira razão
Tamas Czinege

Há um problema com isso: a implementação explícita da interface deve ser privada , o que causaria problemas caso o Foo em algum momento precisasse ser convertido para o ICloneable ... Estou perdendo alguma coisa aqui?
Joel em Gö

3
Meu erro: acontece que, apesar de definido como privado, o método será chamado caso Foo seja convertido para IClonable (CLR via C # 2nd Ed, p.319). Parece uma decisão estranha de design, mas existe. Então Foo.Clone () fornece o primeiro método e ((ICloneable) Foo) .Clone () fornece o segundo método.
Joel em Gö

Na verdade, implementações explícitas de interface são, estritamente falando, parte da API pública. Na medida em que eles são chamados publicamente via casting.
Marc Gravell

8
A solução proposta parece ótima no começo ... até que você perceba que em uma hierarquia de classes, você deseja que 'public Foo Clone ()' seja virtual e substituído em classes derivadas para que eles possam retornar seu próprio tipo (e clonar seus próprios campos de curso). Infelizmente, você não pode alterar o tipo de retorno em uma substituição (o problema de covariância mencionado). Então, você volta ao problema original novamente - a implementação explícita realmente não lhe compra muito (além de retornar o tipo base de sua hierarquia em vez de 'objeto'), e você volta a lançar a maior parte do seu Clone () resultados da chamada. Que nojo!
Ken Beckett

18

Eu preciso perguntar, o que exatamente você faria com a interface além de implementá-la? As interfaces geralmente são úteis apenas quando você faz conversão para ela (por exemplo, essa classe suporta 'IBar') ou possui parâmetros ou configuradores que a utilizam (por exemplo, eu uso um 'IBar'). Com o ICloneable - percorremos todo o Framework e falhamos em encontrar um único uso em qualquer lugar que não fosse uma implementação dele. Também não conseguimos encontrar nenhum uso no 'mundo real' que faça algo além de implementá-lo (nos ~ 60.000 aplicativos aos quais temos acesso).

Agora, se você gostaria de impor um padrão que deseja que seus objetos 'clonáveis' implementem, esse é um uso muito bom - e vá em frente. Você também pode decidir exatamente o que "clonagem" significa para você (por exemplo, profundo ou superficial). No entanto, nesse caso, não há necessidade de nós (o BCL) defini-lo. Só definimos abstrações na BCL quando há necessidade de trocar instâncias digitadas como essa abstração entre bibliotecas não relacionadas.

David Kean (Equipe BCL)


1
O ICloneable não genérico não é muito útil, mas ICloneable<out T>pode ser bastante útil se herdado de ISelf<out T>, com um único método Selfde tipo T. Muitas vezes, não é necessário "algo que seja clonável", mas pode-se muito bem precisar de um Tque seja clonável. Se um objeto clonável é implementado ISelf<itsOwnType>, uma rotina que precisa de um Tclonável pode aceitar um parâmetro do tipo ICloneable<T>, mesmo que nem todos os derivados clonáveis ​​de Tcompartilhem um ancestral comum.
Super12 /

Obrigado. É semelhante à situação de elenco que mencionei acima (ou seja, 'essa classe suporta' IBar '). Infelizmente, apresentamos apenas cenários muito limitados e isolados, nos quais você realmente utilizaria o fato de que T é clonável. Você tem situações em mente?
David Kean

Uma coisa que às vezes é estranha no .net é o gerenciamento de coleções mutáveis ​​quando o conteúdo de uma coleção deve ser visto como um valor (ou seja, desejarei saber mais tarde o conjunto de itens que estão na coleção agora). Algo como ICloneable<T>poderia ser útil para isso, embora uma estrutura mais ampla para manter classes paralelas mutáveis ​​e imutáveis ​​possa ser mais útil. Em outras palavras, o código que precisa ver o que algum tipo de Foocontém, mas não é nem vai sofrer mutação que nem esperar que ele não vai mudar nunca poderia usar um IReadableFoo, enquanto ...
supercat

... o código que deseja conter o conteúdo de Foopoderia usar um ImmutableFoocódigo while que, para querer manipulá-lo, poderia usar a MutableFoo. O código fornecido por qualquer tipo IReadableFoodeve ser capaz de obter uma versão mutável ou imutável. Essa estrutura seria legal, mas infelizmente não consigo encontrar nenhuma maneira legal de configurar as coisas de maneira genérica. Se houvesse uma maneira consistente de criar um wrapper somente leitura para uma classe, tal coisa poderia ser usada em combinação com ICloneable<T>para fazer uma cópia imutável de uma classe que contém T'.
Super12

@supercat Se você deseja clonar a List<T>, de modo que a clonada List<T>seja uma nova coleção que contém ponteiros para todos os mesmos objetos da coleção original, há duas maneiras fáceis de fazer isso sem ICloneable<T>. O primeiro é o Enumerable.ToList()método de extensão: List<foo> clone = original.ToList();o segundo é o List<T>construtor que recebe um IEnumerable<T>: List<foo> clone = new List<foo>(original);Eu suspeito que o método de extensão provavelmente esteja apenas chamando o construtor, mas ambos farão o que você está solicitando. ;)
CptRobby

12

Eu acho que a pergunta "por que" é desnecessária. Há muitas interfaces / classes / etc ... que são muito úteis, mas não fazem parte da biblioteca base do .NET Frameworku.

Mas, principalmente, você pode fazer isso sozinho.

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

Qualquer método de clone viável para uma classe herdável deve usar Object.MemberwiseClone como ponto de partida (ou então usar reflexão), pois, caso contrário, não há garantia de que o clone seja do mesmo tipo que o objeto original. Eu tenho um padrão muito bom se você estiver interessado.
Supercat 22/10

@ supercat porque não fazer uma resposta então?
Nawfal 17/04

"Há muitas interfaces / classes / etc ... que são muito úteis, mas não fazem parte da biblioteca base do .NET Frameworku." Você pode nos dar exemplos?
Krythic

1
@ Krythic ie: estruturas de dados avançadas como b-tree, árvore vermelho-preta, buffer de círculo. Em '09 não havia tuplas, referência fraca genérica, coleções simultâneas ...
TcKs

10

É muito fácil escrever a interface você mesmo, se precisar:

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

Eu incluí um trecho de uma vez por causa da honra dos de direitos de cópia do vínculo foi quebrado ...
Shimmy Weitzhandler

2
E como exatamente essa interface deve funcionar? Para que o resultado de uma operação de clone seja convertível para o tipo T, o objeto que está sendo clonado deve derivar de T. Não há como configurar essa restrição de tipo. Realisticamente falando, a única coisa que consigo ver o resultado do retorno do iCloneable é um tipo iCloneable.
Super16 /

@ supercat: sim, é exatamente isso que Clone () retorna: um T que implementa ICloneable <T>. O MbUnit usa essa interface há anos , então sim, funciona. Quanto à implementação, dê uma olhada na fonte do MbUnit.
Mauricio Scheffer

2
@Mauricio Scheffer: Entendo o que está acontecendo. Nenhum tipo realmente implementa iCloneable (de T). Em vez disso, cada tipo implementa o iCloneable (por si só). Acho que funcionaria, apesar de tudo o que faz é mover um typecast. Pena que não há como declarar um campo como tendo uma restrição de herança e uma interface; seria útil que um método de clonagem retornasse um objeto do tipo base semi-clonável (clonagem exposta apenas como método protegido), mas com uma restrição de tipo clonável. Isso permitiria que um objeto aceitasse derivados clonáveis ​​de derivados semi-clonáveis ​​de um tipo base.
supercat

@Mauricio Scheffer: BTW, eu sugeriria que o ICloneable (de T) também suporta uma propriedade Self somente leitura (do tipo de retorno T). Isso permitiria que um método aceitasse um ICloneable (de Foo) e o usasse (através da propriedade Self) como Foo.
supercat

9

Tendo lido recentemente o artigo Por que copiar um objeto é uma coisa terrível a se fazer? , Acho que essa pergunta precisa de clafiricação adicional. Outras respostas aqui fornecem bons conselhos, mas a resposta ainda não está completa - por que não ICloneable<T>?

  1. Uso

    Então, você tem uma classe que a implementa. Enquanto anteriormente você tinha um método que queria ICloneable, agora ele precisa ser genérico para aceitar ICloneable<T>. Você precisaria editá-lo.

    Então, você poderia ter um método que verifica se há um objeto is ICloneable. E agora? Você não pode fazer is ICloneable<>e, como não conhece o tipo de objeto no tipo de compilação, não pode tornar o método genérico. Primeiro problema real.

    Então você precisa ter ambos ICloneable<T>e ICloneable, o primeiro implementando o último. Assim, um implementador precisaria implementar os dois métodos - object Clone()e T Clone(). Não, obrigado, já nos divertimos o suficiente IEnumerable.

    Como já apontado, há também a complexidade da herança. Embora a covariância pareça resolver esse problema, um tipo derivado precisa implementar ICloneable<T>seu próprio tipo, mas já existe um método com a mesma assinatura (= parâmetros, basicamente) - a Clone()da classe base. Tornar explícita sua nova interface de método de clone é inútil, você perderá a vantagem que procurava ao criar ICloneable<T>. Então adicione a newpalavra - chave. Mas não esqueça que você também precisaria substituir a classe base ' Clone()(a implementação deve permanecer uniforme para todas as classes derivadas, ou seja, para retornar o mesmo objeto de cada método clone, portanto, o método clone base deve ser virtual)! Mas, infelizmente, você não pode tanto overrideenewmétodos com a mesma assinatura. Ao escolher a primeira palavra-chave, você perderia a meta que queria ter ao adicionar ICloneable<T>. Ao escolher o segundo, você quebraria a própria interface, criando métodos que deveriam fazer o mesmo retornar objetos diferentes.

  2. Ponto

    Você deseja ICloneable<T>conforto, mas o conforto não é para o que as interfaces são projetadas, seu significado é (em geral OOP) para unificar o comportamento dos objetos (embora em C # seja limitado à unificação do comportamento externo, por exemplo, métodos e propriedades, não seu funcionamento).

    Se o primeiro motivo ainda não o convenceu, você pode objetar que ICloneable<T>também funcione de forma restritiva, para limitar o tipo retornado pelo método clone. No entanto, o programador desagradável pode implementar ICloneable<T>onde T não é o tipo que está implementando. Portanto, para alcançar sua restrição, você pode adicionar uma boa restrição ao parâmetro genérico:
    public interface ICloneable<T> : ICloneable where T : ICloneable<T>
    certamente mais restritivo que o outro where, ainda não é possível restringir que T é o tipo que está implementando a interface (você pode derivar ICloneable<T>de outro tipo que o implementa).

    Veja bem, mesmo esse propósito não pode ser alcançado (o original ICloneable também falha nisso, nenhuma interface pode realmente limitar o comportamento da classe de implementação).

Como você pode ver, isso prova que a interface genérica é difícil de implementar completamente e também é realmente desnecessária e inútil.

Mas voltando à questão, o que você realmente procura é ter conforto ao clonar um objeto. Existem duas maneiras de fazer isso:

Métodos adicionais

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

Essa solução fornece conforto e comportamento pretendido aos usuários, mas também é muito longa para ser implementada. Se não queremos que o método "confortável" retorne o tipo atual, é muito mais fácil ter apenaspublic virtual object Clone() .

Então, vamos ver a solução "definitiva" - o que em C # realmente pretende nos dar conforto?

Métodos de extensão!

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

É chamado Copiar para não colidir com os métodos Clone atuais (o compilador prefere os métodos declarados do tipo em vez dos de extensão). oclass restrição existe para a velocidade (não requer verificação nula etc.).

Espero que isso esclareça a razão pela qual não fazer ICloneable<T>. No entanto, é recomendável não implementar ICloneable.


Apêndice # 1: O único uso possível de um genérico ICloneableé para tipos de valor, onde ele pode contornar o boxe do método Clone e implica que você tem o valor fora da caixa. E como as estruturas podem ser clonadas (superficialmente) automaticamente, não há necessidade de implementá-las (a menos que você especifique que isso significa cópia em profundidade).
IllidanS4 quer Monica de volta

2

Embora a pergunta seja muito antiga (cinco anos depois de escrever essas respostas :) e já tenha sido respondida, mas achei que este artigo responde muito bem à pergunta, verifique aqui

EDITAR:

Aqui está a citação do artigo que responde à pergunta (leia o artigo completo, inclui outras coisas interessantes):

Existem muitas referências na Internet que apontam para uma publicação de Brad Abrams em 2003 - na época empregada na Microsoft - na qual são discutidas algumas reflexões sobre o ICloneable. A entrada do blog pode ser encontrada neste endereço: Implementando o ICloneable . Apesar do título enganoso, esta entrada do blog pede para não implementar o ICloneable, principalmente por causa de confusão superficial / profunda. O artigo termina com uma sugestão direta: se você precisar de um mecanismo de clonagem, defina sua própria metodologia Clone ou Copiar e garanta que você documente claramente se é uma cópia profunda ou superficial. Um padrão apropriado é:public <type> Copy();


1

Um grande problema é que eles não podiam restringir T à mesma classe. Por exemplo, o que impediria você de fazer isso:

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

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

}

Eles precisam de uma restrição de parâmetro como:

interfact IClonable<T> where T : implementing_type

8
Isso não parece tão ruim quanto class A : ICloneable { public object Clone() { return 1; } /* I can return whatever I want */ }
Nikola Novak

Realmente não vejo qual é o problema. Nenhuma interface pode forçar as implementações a se comportarem razoavelmente. Mesmo que ICloneable<T>pudesse restringir Ta correspondência com seu próprio tipo, isso não forçaria uma implementação Clone()a retornar algo remotamente semelhante ao objeto no qual foi clonado. Além disso, gostaria de sugerir que, se alguém está usando interface de covariância, pode ser melhor ter classes que implementam ICloneableser selado, ter a interface ICloneable<out T>incluem uma Selfpropriedade que é esperado para si voltar, e ...
supercat

... os consumidores lançam ou restringem a ICloneable<BaseType>ou ICloneable<ICloneable<BaseType>>. O BaseTypeem questão deve ter um protectedmétodo de clonagem, que seria chamado pelo tipo que implementa ICloneable. Esse design permitiria a possibilidade de alguém ter a Container, a CloneableContainer, a FancyContainere a CloneableFancyContainer, sendo este último utilizável em código que requer uma derivada clonável Containerou que requer uma FancyContainer(mas não se importa se é clonável).
Supercat 27/08/2012

A razão pela qual eu favoreceria esse design é que a questão de saber se um tipo pode ser clonado de maneira sensata é um tanto ortogonal a outros aspectos dele. Por exemplo, pode-se ter um FancyListtipo que pode ser clonado de maneira sensata, mas um derivado pode persistir automaticamente em um arquivo de disco (especificado no construtor). O tipo derivado não pôde ser clonado, porque seu estado seria anexado ao de um singleton mutável (o arquivo), mas isso não deve impedir o uso do tipo derivado em locais que precisam da maioria dos recursos de um, FancyListmas não precisam. cloná-lo.
Supercat

+1 - esta resposta é a única dentre as que eu vi aqui que realmente responde à pergunta.
IllidanS4 quer Monica de volta

0

É uma pergunta muito boa ... Você pode fazer o seu, no entanto:

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

Andrey diz que é considerada uma API ruim, mas não ouvi nada sobre essa interface ficar obsoleta. E isso quebraria toneladas de interfaces ... O método Clone deve executar uma cópia superficial. Se o objeto também fornecer cópia em profundidade, um Clone sobrecarregado (profundidade em bool) pode ser usado.

Edição: Padrão que eu uso para "clonar" um objeto, está passando um protótipo no construtor.

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

Isso remove qualquer possível situação de implementação de código redundante. BTW, falando sobre as limitações do ICloneable, não cabe realmente ao objeto decidir se um clone raso ou profundo, ou mesmo um clone parcialmente raso / parcialmente profundo, deve ser executado? Deveríamos realmente nos importar, desde que o objeto funcione como pretendido? Em algumas ocasiões, uma boa implementação de Clone pode muito bem incluir clonagem superficial e profunda.


"ruim" não significa obsoleto. Significa simplesmente "é melhor não usá-lo". Não é preterido no sentido de "removeremos a interface do ICloneable no futuro". Eles apenas incentivam você a não usá-lo e não o atualizam com genéricos ou outros novos recursos.
jalf

Dennis: Veja as respostas de Marc. Os problemas de covariância / herança tornam essa interface praticamente inutilizável para qualquer cenário que seja pelo menos marginalmente complicado.
Tamas Czinege 11/02/09

Sim, eu posso ver as limitações do ICloneable, com certeza ... Porém, raramente preciso usar a clonagem.
baretta
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.