129 votes

Constructeur de copie versus Clone()

En C#, quelle est la meilleure façon d'ajouter la fonctionnalité de copie (profonde) à une classe ? Doit-on implémenter le constructeur de copie, ou plutôt dériver de ICloneable et mettre en œuvre le Clone() méthode ?

Remarque : J'ai écrit "profond" entre parenthèses parce que je pensais que ce n'était pas pertinent. Apparemment, d'autres ne sont pas d'accord, donc j'ai demandé si un constructeur/opérateur/fonction de copie doit indiquer clairement la variante de copie qu'il implémente .

101voto

Simon P Stevens Points 17536

Vous ne devez pas dériver de ICloneable .

La raison en est que lorsque Microsoft a conçu le cadre .net, il n'a jamais spécifié si l'interface de l'utilisateur devait être modifiée. Clone() méthode sur ICloneable devrait être un clone profond ou superficiel, donc l'interface est sémantiquement cassée car vos appelants ne sauront pas si l'appel clonera l'objet de manière profonde ou superficielle.

Au lieu de cela, vous devriez définir votre propre IDeepCloneable (et IShallowCloneable ) des interfaces avec DeepClone() (et ShallowClone() ).

Vous pouvez définir deux interfaces, l'une avec un paramètre générique pour prendre en charge le clonage fortement typé et l'autre 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();
}

Que vous mettriez ensuite en œuvre 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 permet de garder 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.

Ce point est abordé dans le document .net Directives de conception du cadre et sur Brad Abrams blog

(Je suppose que si vous écrivez une application (par opposition à un framework/une bibliothèque) et que vous pouvez être sûr que personne en dehors de votre équipe n'appellera votre code, cela n'a pas tant 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 tiendrais aux directives du framework).

2 votes

Si vous optez pour une interface, pourquoi ne pas avoir DeepClone(of T)() et DeepClone(of T)(dummy as T), qui renvoient tous deux T ? La dernière syntaxe permettrait de déduire T en fonction de l'argument.

0 votes

@supercat : Vous voulez dire que vous avez un paramètre fictif 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 fictif juste pour que le type soit déduit automatiquement. Peut-être que je vous comprends mal. (Peut-être que vous pouvez poster du code dans une nouvelle réponse pour que je puisse voir ce que vous voulez dire).

0 votes

@supercat : Le paramètre fictif 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 au type (par exemple parce que c'est un champ, une propriété ou un retour de fonction d'une autre classe) et un paramètre fictif permettrait de déduire correctement le type. En y réfléchissant, cela n'est probablement pas vraiment utile puisque 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 de la collection.

33voto

Jeffrey L Whitledge Points 27574

En C#, quelle est la meilleure façon d'ajouter la fonctionnalité de copie (profonde) à une classe ? Doit-on implémenter le constructeur de copie, ou plutôt dériver de ICloneable et implémenter la méthode Clone() ?

Le problème avec ICloneable est, comme d'autres l'ont mentionné, qu'il ne précise pas s'il s'agit d'une copie profonde ou superficielle, ce qui le rend pratiquement inutilisable et, en pratique, rarement utilisé. Il renvoie également object ce qui est pénible, car cela nécessite beaucoup de casting. (Et bien que vous ayez spécifiquement mentionné les classes dans la question, l'implémentation de ICloneable sur un struct nécessite de la boxe).

Un constucteur de copie souffre également de l'un des problèmes de ICloneable. Il n'est pas évident de savoir si 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'une méthode d'instance.

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

o

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 un cas comme dans l'autre, le clonage d'objets faisant partie d'une hiérarchie d'héritage pose des problèmes, et la façon dont ces problèmes sont résolus peut finalement déterminer 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 votes

Bien que je ne sache pas si l'option DeepClone() est la meilleure, j'aime beaucoup votre réponse, car elle souligne la situation confuse qui existe au sujet d'une fonctionnalité de base du langage de programmation, à mon avis. Je suppose que c'est à l'utilisateur de choisir l'option qu'il préfère.

11 votes

Je ne vais pas débattre de ces points ici, mais à mon avis, l'appelant ne devrait pas se soucier autant de la profondeur ou de la superficialité lorsqu'il appelle Clone(). Il devrait savoir qu'il obtient un clone sans état partagé invalide. Par exemple, il est parfaitement possible que dans un clone profond, je ne veuille pas cloner profondément chaque élément. Tout ce dont l'appelant de Clone devrait se soucier, c'est qu'il obtient une nouvelle copie qui n'a pas de références invalides et non prises en charge à l'original. Appeler la méthode "DeepClone" semble transmettre trop de détails d'implémentation à l'appelant.

1 votes

Qu'y a-t-il de mal à ce qu'une instance d'objet sache se cloner au lieu d'être copiée par une méthode statique ? Cela se produit dans le monde réel, tout le temps, avec les cellules biologiques. Les cellules de votre propre corps sont en train de se cloner au moment où vous lisez ces lignes. Selon moi, l'option de la méthode statique est plus encombrante, tend à cacher la fonctionnalité et s'écarte de l'implémentation "la moins surprenante" au profit des autres.

20voto

DanO Points 1173

Il y a un grand argument pour dire que vous devriez implémenter clone() en utilisant un constructeur de copie protégé

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

Il ne s'agit donc pas d'une question "versus". Vous pouvez avoir besoin à la fois d'un ou de plusieurs constructeurs de copie et d'une interface de clonage pour bien faire les choses.

(Bien que l'interface publique recommandée soit l'interface Clone() plutôt que Constructor-based).

Ne vous laissez pas entraîner par les arguments explicites, profonds ou superficiels, des autres réponses. Dans le monde réel, c'est presque toujours quelque chose entre les deux - et dans tous les cas, cela ne devrait pas préoccuper l'appelant.

Le contrat Clone() est simplement "ne change pas quand je change le premier". La quantité du graphe que vous devez copier, ou la façon dont vous évitez la récursion infinie pour y parvenir ne devrait pas concerner l'appelant.

0 votes

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

3 votes

Une liste<T> est trop générique (ha ha) pour que le clone ait un sens. Dans votre cas, il ne s'agit certainement que d'une copie de la liste et NON des 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 si je fais quelque chose à la première", qui s'applique également aux objets contenus.

1 votes

Une List<T> ne saura pas plus que vous comment cloner correctement son contenu. Si l'objet sous-jacent est immuable, vous pouvez y aller. Sinon, si l'objet sous-jacent possède une méthode Clone(), vous devrez l'utiliser. List<T> aList = nouvelle List<T>(aFullListOfT.Select(t=t.Clone())

12voto

Grant Crofton Points 3176

La mise en œuvre de ICloneable n'est pas recommandée. en raison du fait qu'il n'est pas spécifié s'il s'agit d'une copie profonde ou superficielle, donc j'opterais pour le constructeur, ou implémentez simplement quelque chose vous-même. Peut-être l'appeler DeepCopy() pour que ce soit vraiment évident !

5 votes

@Grant, comment le constructeur relaie-t-il l'intention ? Autrement dit, si un objet s'est pris lui-même dans le constructeur, la copie est-elle profonde ou superficielle ? Sinon, je suis complètement d'accord avec la suggestion DeepCopy() (ou autre).

7 votes

Je dirais qu'un constructeur est presque aussi peu clair que l'interface ICloneable - il faut lire la documentation/code de l'API pour savoir s'il fait un clone profond ou non. Je définis simplement un IDeepCloneable<T> avec une interface DeepClone() méthode.

2 votes

@Jon - Le réacteur n'est jamais terminé !

12voto

Matt Jacobsen Points 2151

Vous rencontrerez des problèmes avec les constructeurs de copie et les classes abstraites. Imaginez que vous voulez 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 à regretter de ne pas avoir utilisé une interface ou de vous être contenté d'une implémentation de DeepCopy()/ICloneable.Clone().

2 votes

Bon argument en faveur d'une approche basée sur l'interface.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X