Copier le constructeur contre Clone ()


120

En C #, quelle est la méthode préférée pour ajouter une fonctionnalité de copie (profonde) à une classe? Doit-on implémenter le constructeur de copie, ou plutôt dériver ICloneableet implémenter la Clone()méthode?

Remarque : j'ai écrit "deep" entre crochets parce que je pensais que ce n'était pas pertinent. Apparemment, d'autres ne sont pas d'accord, alors j'ai demandé si un constructeur / opérateur / fonction de copie devait préciser quelle variante de copie il implémentait .

Réponses:


91

Vous ne devriez pas dériver de ICloneable.

La raison en est que lorsque Microsoft a conçu le framework .net, ils n'ont jamais spécifié si la Clone()méthode sur ICloneabledoit être un clone profond ou superficiel, l'interface est donc sémantiquement interrompue car vos appelants ne sauront pas si l'appel clonera l'objet en profondeur ou en profondeur.

Au lieu de cela, vous devez définir vos propres IDeepCloneable(et IShallowCloneable) interfaces avec les méthodes DeepClone()(et ShallowClone()).

Vous pouvez définir deux interfaces, une avec un paramètre générique pour prendre en charge le clonage fortement typé et une sans pour conserver la capacité de clonage faiblement typé lorsque vous travaillez avec des collections de différents types d'objets clonables:

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

Ce que vous implémenteriez ensuite comme ceci:

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

En général, je préfère utiliser les interfaces décrites plutôt qu'un constructeur de copie, cela garde l'intention très claire. Un constructeur de copie serait probablement supposé être un clone profond, mais ce n'est certainement pas une intention aussi claire que l'utilisation d'une interface IDeepClonable.

Ceci est discuté dans les directives de conception du .net et sur le blog de Brad Abrams

(Je suppose que si vous écrivez une application (par opposition à un framework / bibliothèque) afin que vous puissiez être sûr que personne en dehors de votre équipe n'appellera votre code, cela n'a pas tellement d'importance et vous pouvez attribuer une signification sémantique de "deepclone" à l'interface .net ICloneable, mais vous devez vous assurer que cela est bien documenté et bien compris au sein de votre équipe. Personnellement, je m'en tiendrai aux directives du cadre.)


2
Si vous optez pour une interface, que diriez-vous d'avoir DeepClone (de T) () et DeepClone (de T) (factice comme T), qui retournent tous deux T? Cette dernière syntaxe permettrait à T d'être inféré sur la base de l'argument.
supercat

@supercat: Dites-vous avoir un paramètre factice pour que le type puisse être déduit? C'est une option, je suppose. Je ne suis pas sûr que j'aime avoir un paramètre factice juste pour obtenir le type déduit automatiquement. Peut-être que je vous comprends mal. (Peut-être publier du code dans une nouvelle réponse pour que je puisse voir ce que vous voulez dire).
Simon P Stevens

@supercat: le paramètre factice existerait précisément pour permettre l'inférence de type. Il y a des situations où un code peut vouloir cloner quelque chose sans avoir un accès immédiat à ce qu'est le type (par exemple parce que c'est un champ, une propriété ou une fonction renvoyée par une autre classe) et un paramètre factice permettrait de déduire correctement le type. En y réfléchissant, ce n'est probablement pas vraiment utile car le but d'une interface serait de créer quelque chose comme une collection clonable en profondeur, auquel cas le type devrait être le type générique des collections.
supercat

2
Question! Dans quelle situation souhaiteriez-vous la version non générique? Pour moi, cela n'a IDeepCloneable<T>de sens que pour exister, parce que ... vous savez ce que T si vous faites votre propre implémentation, c'estSomeClass : IDeepCloneable<SomeClass> { ... }
Kyle Baran

2
@Kyle dit que vous aviez une méthode qui prenait des objets clonables MyFunc(IDeepClonable data), alors cela pourrait fonctionner sur tous les clonables, pas seulement sur un type spécifique. Ou si vous aviez une collection de clonables. IEnumerable<IDeepClonable> lotsOfCloneablesalors vous pouvez cloner de nombreux objets en même temps. Si vous n'avez pas besoin de ce genre de chose, laissez de côté le non générique.
Simon P Stevens

33

En C #, quelle est la méthode préférée pour ajouter une fonctionnalité de copie (profonde) à une classe? Faut-il implémenter le constructeur de copie, ou plutôt dériver de ICloneable et implémenter la méthode Clone ()?

Le problème avec ICloneableest, comme d'autres l'ont mentionné, qu'il ne spécifie pas s'il s'agit d'une copie profonde ou superficielle, ce qui la rend pratiquement inutilisable et, en pratique, rarement utilisée. Il revient également object, ce qui est pénible, car il nécessite beaucoup de casting. (Et bien que vous ayez spécifiquement mentionné les classes dans la question, la mise ICloneableen œuvre sur un structnécessite de la boxe.)

Un constructeur de copie souffre également de l'un des problèmes liés à ICloneable. Il n'est pas évident qu'un constructeur de copie effectue une copie profonde ou superficielle.

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

Il serait préférable de créer une méthode DeepClone (). De cette façon, l'intention est parfaitement claire.

Cela soulève la question de savoir s'il doit s'agir d'une méthode statique ou d'instance.

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

ou

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

Je préfère parfois légèrement la version statique, simplement parce que le clonage semble être quelque chose qui est fait à un objet plutôt que quelque chose que l'objet fait. Dans les deux cas, il y aura des problèmes à résoudre lors du clonage d'objets qui font partie d'une hiérarchie d'héritage, et la façon dont ces problèmes sont résolus peut finalement conduire la conception.

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
Bien que je ne sache pas si l'option DeepClone () est la meilleure, j'aime beaucoup votre réponse, car elle souligne la situation déroutante qui existe à mon avis à propos d'une fonctionnalité de langage de programmation de base. Je suppose que c'est à l'utilisateur de choisir l'option qu'il préfère.
Dimitri C.

11
Je ne vais pas discuter des points ici, mais à mon avis, l'appelant ne devrait pas tellement se soucier de profond ou peu profond quand il appelle Clone (). Ils doivent savoir qu'ils obtiennent un clone sans état partagé invalide. Par exemple, il est parfaitement possible que dans un clone profond, je ne veuille pas cloner en profondeur chaque élément. Tout ce dont l'appelant de Clone doit se soucier, c'est qu'il reçoit une nouvelle copie qui ne contient aucune référence invalide et non prise en charge à l'original. L'appel de la méthode «DeepClone» semble transmettre trop de détails d'implémentation à l'appelant.
zumalifeguard

1
Quel est le problème avec une instance d'objet sachant comment se cloner au lieu d'être copiée par une méthode statique? Cela se produit tout le temps dans le monde réel avec des cellules biologiques. Les cellules de votre propre corps sont en train de se cloner en ce moment pendant que vous lisez ceci. IMO, l'option de méthode statique est plus lourde, a tendance à cacher la fonctionnalité et s'écarte de l'utilisation de l'implémentation "la moins surprenante" au profit des autres.
Ken Beckett

8
@KenBeckett - La raison pour laquelle je pense que le clonage est quelque chose qui est fait à un objet est parce qu'un objet doit "faire une chose et bien la faire". Normalement, faire des copies de lui-même n'est pas la compétence de base d'une classe, mais plutôt une fonctionnalité qui est ajoutée. Faire un clone d'un BankAccount est quelque chose que vous pourriez très bien vouloir faire, mais faire des clones de lui-même n'est pas une caractéristique d'un compte bancaire. Votre exemple de cellule n'est pas très instructif, car la reproduction est précisément ce pour quoi les cellules ont évolué. Cell.Clone serait une bonne méthode d'instance, mais ce n'est pas vrai pour la plupart des autres choses.
Jeffrey L Whitledge

23

Je recommande d'utiliser un constructeur de copie sur une méthode de clonage principalement parce qu'une méthode de clonage vous empêchera de créer des champs readonlyqui auraient pu l'être si vous aviez utilisé un constructeur à la place.

Si vous avez besoin d'un clonage polymorphe, vous pouvez ensuite ajouter une méthode abstractou virtual Clone()à votre classe de base que vous implémentez avec un appel au constructeur de copie.

Si vous avez besoin de plus d'un type de copie (ex: deep / shallow), vous pouvez le spécifier avec un paramètre dans le constructeur de copie, bien que d'après mon expérience, je trouve que généralement un mélange de copie profonde et superficielle est ce dont j'ai besoin.

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 Pour traiter le clonage polymorphe; une application importante du clonage.
samis

18

Il existe un excellent argument selon lequel vous devriez implémenter clone () en utilisant un constructeur de copie protégée

Il est préférable de fournir un constructeur de copie protégée (non publique) et de l'appeler à partir de la méthode de clonage. Cela nous donne la possibilité de déléguer la tâche de création d'un objet à une instance d'une classe elle-même, fournissant ainsi l'extensibilité et aussi, créant en toute sécurité les objets à l'aide du constructeur de copie protégée.

Ce n'est donc pas une question «contre». Vous devrez peut-être à la fois des constructeurs de copie et une interface de clonage pour le faire correctement.

(Bien que l'interface publique recommandée soit l'interface Clone () plutôt que basée sur un constructeur.)

Ne vous laissez pas prendre par l'argument explicite profond ou superficiel des autres réponses. Dans le monde réel, c'est presque toujours quelque chose entre les deux - et de toute façon, ne devrait pas être la préoccupation de l'appelant.

Le contrat Clone () est simplement "ne changera pas quand je changerai le premier". La quantité de graphique que vous devez copier ou la manière d'éviter une récursivité infinie pour que cela se produise ne devrait pas concerner l'appelant.


"ne devrait pas être la préoccupation de l'appelant". Je ne pourrais pas être plus d'accord mais, me voici, en train d'essayer de comprendre si List <T> aList = new List <T> (aFullListOfT) fera une copie profonde (ce que je veux) ou une copie superficielle (qui casserait mon code) et si je dois implémenter une autre façon de faire le travail!
ThunderGr

3
Une liste <T> est trop générique (ha ha) pour que le clonage ait même un sens. Dans votre cas, ce n'est certainement qu'une copie de la liste et PAS les objets pointés par la liste. La manipulation de la nouvelle liste n'affectera pas la première liste, mais les objets sont les mêmes et à moins qu'ils ne soient immuables, ceux du premier ensemble changeront si vous modifiez ceux du second ensemble. S'il y avait une opération list.Clone () dans votre bibliothèque, vous devriez vous attendre à ce que le résultat soit un clone complet comme dans "ne changera pas quand je ferai quelque chose au premier." cela s'applique également aux objets contenus.
DanO

1
Un List <T> ne saura rien de plus sur le clonage correct de son contenu que vous. Si l'objet sous-jacent est immuable, vous êtes prêt à partir. Sinon, si l'objet sous-jacent a une méthode Clone (), vous devrez l'utiliser. List <T> aList = new List <T> (aFullListOfT.Select (t = t.Clone ())
DanO

1
+1 pour l'approche hybride. Les deux approches ont un avantage et un inconvénient, mais cela semble avoir l'avantage le plus global.
Kyle Baran

12

La mise en œuvre d'ICloneable n'est pas recommandée raison du fait qu'il n'est pas spécifié s'il s'agit d'une copie profonde ou superficielle, alors je choisirais le constructeur, ou implémenterais simplement quelque chose vous-même. Appelez-le peut-être DeepCopy () pour le rendre vraiment évident!


5
@Grant, comment le constructeur transmet-il l'intention? IOW, si un objet se prend dans le constructeur, la copie est-elle profonde ou superficielle? Sinon, je suis entièrement d'accord avec la suggestion DeepCopy () (ou autre).
Marc

7
Je dirais qu'un constructeur est presque aussi flou que l'interface IClonable - vous devrez lire la documentation / le code de l'API pour savoir qu'il fait un clonage profond ou non. Je viens de définir une IDeepCloneable<T>interface avec une DeepClone()méthode.
Kent Boogaart

2
@Jon - La réaction n'est jamais terminée!
Grant Crofton

@Marc, @Kent - ouais bon point, le constructeur n'est probablement pas non plus une bonne idée.
Grant Crofton

3
Quelqu'un a-t-il vu une utilisation où iCloneable a été utilisé sur un objet de type inconnu? L'intérêt des interfaces est qu'elles peuvent être utilisées sur des objets de type inconnu; sinon on peut tout aussi bien faire de Clone une méthode standard qui retourne le type en question.
supercat

12

Vous rencontrerez des problèmes avec les constructeurs de copie et les classes abstraites. Imaginez que vous vouliez faire ce qui suit:

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

Juste après avoir fait ce qui précède, vous commencez à souhaiter soit utiliser une interface, soit opter pour une implémentation DeepCopy () / ICloneable.Clone ().


2
Bon argument pour une approche basée sur l'interface.
DanO le

4

Le problème avec ICloneable est à la fois l'intention et la cohérence. Il n'est jamais clair s'il s'agit d'une copie profonde ou superficielle. Pour cette raison, il n'est probablement jamais utilisé d'une seule manière ou d'une autre.

Je ne trouve pas de constructeur de copie publique plus clair à ce sujet.

Cela dit, je présenterais un système de méthodes qui fonctionne pour vous et qui relaie l'intention (a'la un peu auto-documenté)


3

Si l'objet que vous essayez de copier est sérialisable, vous pouvez le cloner en le sérialisant et en le désérialisant. Ensuite, vous n'avez pas besoin d'écrire un constructeur de copie pour chaque classe.

Je n'ai pas accès au code pour le moment mais c'est quelque chose comme ça

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
L'utilisation de la sérialisation comme moyen d'implémenter le clonage profond est sans rapport avec la question de savoir si le clone profond doit être présenté comme un critère ou une méthode.
Kent Boogaart

1
Je pense que c'est une autre alternative valable. Je ne pensais pas qu'il était limité à ces deux méthodes de copie profonde.
Shaun Bowe

5
@Kent Boogaart - Étant donné que l'OP commence par la ligne "En C #, quelle est la meilleure façon d'ajouter des fonctionnalités de copie (profonde) à une classe", je pense qu'il est assez juste que Shaun propose différentes alternatives. Surtout dans un scénario hérité où vous avez un grand nombre de classes pour lesquelles vous souhaitez implémenter la fonctionnalité de clonage, cette astuce peut être utile; pas aussi léger que l'implémentation directe de votre propre clone, mais néanmoins utile. Si les gens n'avaient jamais proposé d'alternatives «avez-vous pensé à…» à mes questions, je n'aurais pas appris autant que moi au fil des ans.
Rob Levine

2

Cela dépend de la sémantique de copie de la classe en question, que vous devez définir vous-même en tant que développeur. La méthode choisie est généralement basée sur les cas d'utilisation prévus de la classe. Il sera peut-être judicieux d'implémenter les deux méthodes. Mais les deux partagent un inconvénient similaire - la méthode de copie qu'ils implémentent n'est pas exactement claire. Cela doit être clairement indiqué dans la documentation de votre classe.

Pour moi ayant:

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

au lieu 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();

ressemblait à une déclaration d'intention plus claire.


0

Je pense qu'il devrait y avoir un modèle standard pour les objets clonables, même si je ne suis pas sûr de ce que le modèle devrait être exactement. En ce qui concerne le clonage, il semblerait qu'il existe trois types de classes:

  1. Ceux qui soutiennent explicitement le clonage profond
  2. Ceux pour lesquels le clonage par membre fonctionnera comme un clonage profond, mais qui n'ont ni besoin de soutien explicite.
  3. Ceux qui ne peuvent pas être utilement clonés en profondeur, et où le clonage par membre donnera de mauvais résultats.

Pour autant que je sache, le seul moyen (au moins dans .net 2.0) d'obtenir un nouvel objet de la même classe qu'un objet existant est d'utiliser MemberwiseClone. Un bon modèle semble être d'avoir une fonction "new" / "Shadows" Clone qui retourne toujours le type actuel, dont la définition est toujours d'appeler MemberwiseClone puis d'appeler un sous-programme virtuel protégé CleanupClone (originalObject). La routine CleanupCode doit appeler base.Cleanupcode pour gérer les besoins de clonage du type de base, puis ajouter son propre nettoyage. Si la routine de clonage doit utiliser l'objet d'origine, il devrait être transtypé, mais sinon le seul typage serait sur l'appel MemberwiseClone.

Malheureusement, le niveau le plus bas de classe qui était de type (1) ci-dessus plutôt que de type (2) devrait être codé pour supposer que ses types inférieurs n'auraient pas besoin de support explicite pour le clonage. Je ne vois vraiment aucun moyen de contourner cela.

Pourtant, je pense qu'avoir un modèle défini serait mieux que rien.

Incidemment, si l'on sait que son type de base prend en charge iCloneable, mais ne connaît pas le nom de la fonction qu'il utilise, y a-t-il un moyen de référencer la fonction iCloneable.Clone de son type de base?


0

Si vous lisez toutes les réponses et discussions intéressantes, vous pourriez toujours vous demander comment exactement vous copiez les propriétés - toutes explicitement, ou y a-t-il une manière plus élégante de le faire? Si c'est votre dernière question, jetez un coup d'œil à ceci (sur StackOverflow):

Comment puis-je cloner «profondément» les propriétés de classes tierces à l'aide d'une méthode d'extension générique?

Il décrit comment implémenter une méthode d'extension CreateCopy()qui crée une copie "profonde" de l'objet comprenant toutes les propriétés (sans avoir à copier manuellement propriété par propriété).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.