Construtor de cópia versus Clone ()


119

Em C #, qual é a maneira preferida de adicionar funcionalidade de cópia (profunda) a uma classe? Deve-se implementar o construtor de cópia, ou melhor, derivar ICloneablee implementar o Clone()método?

Observação : escrevi "profundo" entre colchetes porque achei irrelevante. Aparentemente, outros discordam, então perguntei se um construtor / operador / função de cópia precisa deixar claro qual variante de cópia ele implementa .

Respostas:


91

Você não deve derivar de ICloneable.

O motivo é que, quando a Microsoft projetou a estrutura .net, eles nunca especificaram se o Clone()método em ICloneabledeve ser um clone profundo ou superficial, portanto, a interface é semanticamente quebrada, pois seus chamadores não saberão se a chamada clonará profundamente ou superficialmente o objeto.

Em vez disso, você deve definir as suas próprias IDeepCloneable(e IShallowCloneable) interfaces com DeepClone()(e ShallowClone()) métodos.

Você pode definir duas interfaces, uma com um parâmetro genérico para oferecer suporte à clonagem fortemente tipada e outra sem, para manter a capacidade de clonagem fracamente tipada para quando você estiver trabalhando com coleções de diferentes tipos de objetos clonáveis:

public interface IDeepCloneable
{
    object DeepClone();
}
public interface IDeepCloneable<T> : IDeepCloneable
{
    T DeepClone();
}

Que você implementaria assim:

public class SampleClass : IDeepCloneable<SampleClass>
{
    public SampleClass DeepClone()
    {
        // Deep clone your object
        return ...;
    }
    object IDeepCloneable.DeepClone()   
    {
        return this.DeepClone();
    }
}

Geralmente, prefiro usar as interfaces descritas em vez de um construtor de cópia, pois mantém a intenção muito clara. Um construtor de cópia provavelmente seria considerado um clone profundo, mas certamente não é uma intenção tão clara quanto usar uma interface IDeepClonable.

Isso é discutido no .net Framework Design Guidelines e no blog de Brad Abrams

(Suponho que se você estiver escrevendo um aplicativo (em oposição a um framework / biblioteca), então você pode ter certeza que ninguém fora de sua equipe irá chamar seu código, não importa muito e você pode atribuir um significado semântico de "deepclone" para a interface .net ICloneable, mas você deve se certificar de que isso seja bem documentado e bem compreendido por sua equipe. Pessoalmente, seguiria as diretrizes da estrutura.)


2
Se você está indo para uma interface, que tal ter DeepClone (de T) () e DeepClone (de T) (dummy como T), ambos retornando T? A última sintaxe permitiria que T fosse inferida com base no argumento.
supercat

@supercat: Você está dizendo para ter um parâmetro fictício para que o tipo possa ser inferido? É uma opção, suponho. Não tenho certeza se gosto de um parâmetro fictício apenas para obter o tipo inferido automaticamente. Talvez eu esteja te entendendo mal. (Talvez poste algum código em uma nova resposta para que eu possa ver o que você quer dizer).
Simon P Stevens

@supercat: O parâmetro dummy existiria precisamente para permitir a inferência de tipo. Existem situações em que algum código pode querer clonar algo sem ter acesso imediato ao que o tipo é (por exemplo, porque é um campo, propriedade ou retorno de função de outra classe) e um parâmetro fictício permitiria um meio de inferir adequadamente o tipo. Pensando bem, provavelmente não é realmente útil, já que o objetivo de uma interface seria criar algo como uma coleção profundamente clonável, caso em que o tipo deve ser o tipo genérico da coleção.
supercat

2
Questão! Em que situação você gostaria de ter a versão não genérica? Para mim, faz sentido apenas IDeepCloneable<T>existir, porque ... você sabe o que T se fizer sua própria implementação, ou sejaSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@Kyle diz que você tinha um método que pegava objetos clonáveis MyFunc(IDeepClonable data), então ele poderia funcionar em todos os clonáveis, não apenas em um tipo específico. Ou se você tivesse uma coleção de clonáveis. IEnumerable<IDeepClonable> lotsOfCloneablesentão você poderia clonar muitos objetos ao mesmo tempo. Se você não precisa desse tipo de coisa, deixe o não genérico de fora.
Simon P Stevens

33

Em C #, qual é a maneira preferida de adicionar funcionalidade de cópia (profunda) a uma classe? Deve-se implementar o construtor de cópia, ou melhor, derivar de ICloneable e implementar o método Clone ()?

O problema com ICloneableé, como outros mencionaram, que não especifica se é uma cópia profunda ou superficial, o que a torna praticamente inutilizável e, na prática, raramente usada. Também retorna object, o que é chato, pois requer muito casting. (E embora você tenha mencionado especificamente as classes na pergunta, a implementação ICloneableem um structrequer boxe.)

Um construtor de cópia também sofre de um dos problemas com o ICloneable. Não é óbvio se um construtor de cópia está fazendo uma cópia profunda ou superficial.

Account clonedAccount = new Account(currentAccount); // Deep or shallow?

Seria melhor criar um método DeepClone (). Dessa forma, a intenção é perfeitamente clara.

Isso levanta a questão de saber se deve ser um método estático ou de instância.

Account clonedAccount = currentAccount.DeepClone();  // instance method

ou

Account clonedAccount = Account.DeepClone(currentAccount); // static method

Às vezes, prefiro um pouco a versão estática, só porque a clonagem parece algo que está sendo feito em um objeto, em vez de algo que o objeto está fazendo. Em ambos os casos, haverá problemas com os quais lidar ao clonar objetos que fazem parte de uma hierarquia de herança, e como esses problemas são resolvidos pode, em última instância, conduzir o design.

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
Embora eu não saiba se a opção DeepClone () é a melhor, gosto muito da sua resposta, pois sublinha a situação confusa que existe sobre um recurso básico de linguagem de programação na minha opinião. Eu acho que cabe ao usuário escolher qual opção ele mais gosta.
Dimitri C.

11
Não vou discutir os pontos aqui, mas na minha opinião, o chamador não deve se preocupar tanto com profundo ou raso ao chamar Clone (). Eles devem saber que estão recebendo um clone sem estado compartilhado inválido. Por exemplo, é perfeitamente possível que em um clone profundo, eu não queira clonar profundamente cada elemento. Tudo o que o chamador de Clone deve se preocupar é se eles estão obtendo uma nova cópia que não contém nenhuma referência inválida e sem suporte ao original. Chamar o método 'DeepClone "parece transmitir muitos detalhes de implementação para o chamador.
zumalifeguard

1
O que há de errado em uma instância de objeto saber como se clonar em vez de ser copiada por um método estático? Isso ocorre no mundo real o tempo todo com células biológicas. As células em seu próprio corpo estão ocupadas se clonando agora, enquanto você lê isto. IMO, a opção de método estático é mais complicada, tende a ocultar a funcionalidade e se desvia do uso da implementação "menos surpreendente" para o benefício de terceiros.
Ken Beckett

8
@KenBeckett - Acho que clonar é algo feito em um objeto é porque um objeto deve "fazer uma coisa e fazê-la bem". Normalmente, fazer cópias de si mesmo não é a competência central de uma classe, mas sim a funcionalidade que é acrescentada. Fazer um clone de uma conta bancária é algo que você pode muito bem querer fazer, mas fazer clones de si mesmo não é uma característica de uma conta bancária. Seu exemplo de célula não é muito instrutivo, porque a reprodução é precisamente o que as células evoluíram para fazer. Cell.Clone seria um bom método de instância, mas isso não é verdade para a maioria das outras coisas.
Jeffrey L Whitledge,

23

Eu recomendo usar um construtor de cópia em vez de um método clone principalmente porque um método clone impedirá que você crie campos readonlyque poderiam ter sido se você tivesse usado um construtor em vez disso.

Se precisar de clonagem polimórfica, você pode adicionar um método abstractou virtual Clone()à sua classe base que você implementa com uma chamada para o construtor de cópia.

Se você precisar de mais de um tipo de cópia (por exemplo: profunda / superficial), poderá especificá-la com um parâmetro no construtor de cópia, embora, em minha experiência, eu ache que geralmente uma mistura de cópia profunda e superficial é o que preciso.

Ex:

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 Para endereçamento de clonagem polimórfica; uma aplicação significativa da clonagem.
samis

18

Há um grande argumento de que você deve implementar clone () usando um construtor de cópia protegida

É melhor fornecer um construtor de cópia protegida (não pública) e invocá-lo a partir do método clone. Isso nos dá a capacidade de delegar a tarefa de criar um objeto a uma instância da própria classe, fornecendo assim extensibilidade e também criando com segurança os objetos usando o construtor de cópia protegida.

Portanto, esta não é uma questão "versus". Você pode precisar de um (s) construtor (es) de cópia e uma interface clone para fazer isso direito.

(Embora a interface pública recomendada seja a interface Clone () em vez de baseada no Construtor.)

Não se deixe levar pelo argumento explícito profundo ou superficial das outras respostas. No mundo real, quase sempre é algo intermediário - e de qualquer forma, não deve ser a preocupação de quem liga.

O contrato Clone () é simplesmente "não mudará quando eu mudar o primeiro". O quanto do gráfico você precisa copiar ou como evitar a recursão infinita para fazer isso acontecer não deve preocupar o chamador.


"não deve ser a preocupação de quem liga". Eu não poderia concordar mais, mas aqui estou eu, tentando descobrir se List <T> aList = new List <T> (aFullListOfT) fará uma cópia profunda (que é o que eu quero) ou uma cópia superficial (que quebraria meu código) e se eu tenho que implementar outra maneira de fazer o trabalho!
ThunderGr

3
Uma lista <T> é muito genérica (ha ha) para que o clone faça sentido. No seu caso, certamente é apenas uma cópia da lista e NÃO os objetos apontados pela lista. Manipular a nova lista não afetará a primeira lista, mas os objetos são os mesmos e, a menos que sejam imutáveis, os do primeiro conjunto serão alterados se você alterar os do segundo conjunto. Se houver uma operação list.Clone () em sua biblioteca, você deve esperar que o resultado seja um clone completo, como em "não mudará quando eu fizer algo com o primeiro." isso se aplica aos objetos contidos também.
DanO de

1
Um List <T> não saberá nada mais sobre clonar corretamente seu conteúdo do que você. Se o objeto subjacente for imutável, você está pronto para prosseguir. Caso contrário, se o objeto subjacente tiver um método Clone (), você terá que usá-lo. List <T> aList = new List <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
1 para a abordagem híbrida. Ambas as abordagens têm uma vantagem e uma desvantagem, mas isso parece ter um benefício mais geral.
Kyle Baran

12

Implementar ICloneable não é recomendado devido ao fato de que não é especificado se é uma cópia profunda ou superficial, então eu escolheria o construtor, ou apenas implementaria algo você mesmo. Talvez chame de DeepCopy () para tornar realmente óbvio!


5
@Grant, como o construtor retransmite a intenção? IOW, se um objeto assumiu a si mesmo no construtor, a cópia é profunda ou superficial? Caso contrário, concordo totalmente com a sugestão DeepCopy () (ou outra).
Marc

7
Eu diria que um construtor é quase tão confuso quanto a interface ICloneable - você teria que ler os documentos / código da API para saber se ele faz uma clonagem profunda ou não. Acabei de definir uma IDeepCloneable<T>interface com um DeepClone()método.
Kent Boogaart

2
@Jon - A reator nunca acabou!
Grant Crofton

@Marc, @Kent - é verdade, o construtor provavelmente também não é uma boa ideia.
Grant Crofton

3
Alguém viu um uso em que iCloneable foi usado em um objeto de tipo desconhecido? O ponto principal das interfaces é que elas podem ser usadas em objetos de tipo desconhecido; caso contrário, pode-se simplesmente tornar Clone um método padrão que retorna o tipo em questão.
supercat

12

Você terá problemas com construtores de cópia e classes abstratas. Imagine que você queira fazer o seguinte:

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

Logo depois de fazer o acima, você começa a desejar ter usado uma interface ou se conformado com uma implementação DeepCopy () / ICloneable.Clone ().


2
Bom argumento para uma abordagem baseada em interface.
DanO

4

O problema com ICloneable é tanto intenção quanto consistência. Nunca fica claro se é uma cópia profunda ou superficial. Por causa disso, provavelmente nunca é usado apenas de uma maneira ou de outra.

Não acho um construtor de cópia pública para ser mais claro sobre esse assunto.

Dito isso, gostaria de apresentar um sistema de método que funciona para você e retransmite a intenção (a'la um pouco autodocumentável)


3

Se o objeto que você está tentando copiar for serializável, você pode cloná-lo, serializando-o e desserializando-o. Então você não precisa escrever um construtor de cópia para cada classe.

Não tenho acesso ao código agora, mas é algo assim

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
O uso de serialização como meio de implementar a clonagem profunda é irrelevante para a questão de saber se o clone profundo deve ser apresentado como um ctor ou método.
Kent Boogaart

1
Acho que é outra alternativa válida. Não achei que ele estivesse limitado a esses dois métodos de cópia profunda.
Shaun Bowe

5
@Kent Boogaart - Dado que o OP começa com a linha "Em C #, qual é a forma preferida de adicionar funcionalidade de cópia (profunda) a uma classe", acho que é justo que Shaun ofereça alternativas diferentes. Especialmente em um cenário legado onde você tem um grande número de classes para as quais deseja implementar a funcionalidade de clonagem, este truque pode ser útil; não tão leve quanto implementar seu próprio clone diretamente, mas mesmo assim útil. Se as pessoas nunca ofereceram alternativas do tipo "você já pensou em ..." para minhas perguntas, eu não teria aprendido tanto quanto ao longo dos anos.
Rob Levine

2

É dependente da semântica de cópia da classe em questão, que você deve definir como o desenvolvedor. O método escolhido geralmente é baseado nos casos de uso pretendidos da classe. Talvez faça sentido implementar os dois métodos. Mas ambos compartilham desvantagens semelhantes - não está exatamente claro qual método de cópia eles implementam. Isso deve ser claramente declarado na documentação de sua classe.

Para mim ter:

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

ao invés de:

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

parecia uma declaração de intenções mais clara.


0

Acho que deve haver um padrão para objetos clonáveis, embora não tenha certeza de qual deve ser exatamente o padrão. Com relação à clonagem, parece que existem três tipos de classes:

  1. Aqueles que explicitamente suportam clonagem profunda
  2. Aqueles em que a clonagem por membro funcionará como clonagem profunda, mas que não têm nem precisam de suporte explícito.
  3. Aqueles que não podem ser clonados de forma útil e em que a clonagem por membros produzirá resultados ruins.

Até onde eu posso dizer, a única maneira (pelo menos em .net 2.0) de obter um novo objeto da mesma classe de um objeto existente é usar MemberwiseClone. Um bom padrão seria ter uma função "nova" / "Sombras" Clone que sempre retorna o tipo atual, cuja definição é sempre chamar MemberwiseClone e então chamar uma sub-rotina virtual protegida CleanupClone (originalObject). A rotina CleanupCode deve chamar base.Cleanupcode para lidar com as necessidades de clonagem do tipo base e, em seguida, adicionar sua própria limpeza. Se a rotina de clonagem tiver que usar o objeto original, ele terá que ser typecast, mas caso contrário, a única typecasting será na chamada MemberwiseClone.

Infelizmente, o nível mais baixo de classe que era do tipo (1) acima, em vez do tipo (2), teria que ser codificado para assumir que seus tipos inferiores não precisariam de nenhum suporte explícito para clonagem. Eu realmente não vejo nenhuma maneira de contornar isso.

Mesmo assim, acho que ter um padrão definido seria melhor do que nada.

A propósito, se alguém souber que seu tipo base suporta iCloneable, mas não souber o nome da função que ele usa, há alguma maneira de fazer referência à função iCloneable.Clone de seu tipo base?


0

Se você ler todas as respostas e discussões interessantes, ainda poderá se perguntar como exatamente copia as propriedades - todas elas explicitamente, ou existe uma maneira mais elegante de fazer isso? Se essa for a sua pergunta restante, dê uma olhada (no StackOverflow):

Como posso clonar “profundamente” as propriedades de classes de terceiros usando um método de extensão genérico?

Ele descreve como implementar um método de extensão CreateCopy()que cria uma cópia "profunda" do objeto, incluindo todas as propriedades (sem ter que copiar propriedade por propriedade manualmente).

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.