Developpez.com

Club des développeurs et IT pro
Plus de 4 millions de visiteurs uniques par mois

Copier un objet en .NET

Voici un petit article qui explique comment copier un objet. Nous allons voir dans un premier temps voir ce que l'on entend par "copier un objet", puis nous verrons quelles solutions apportent le Framework .NET pour résoudre ce problème. Avant de poursuivre nous allons faire un petit rappel sur ce que sont les types références et les types valeurs en .NET.

N'hésitez pas à commenter cet article ! Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

Rappel

Dans le Framework .NET il y a 2 espaces mémoires distincts pour stocker les objets ; la pile (ou "stack" en anglais) et le tas (ou "heap" en anglais). Pour la suite, il faut retenir ces 2 points :

  • C'est sur la pile que sont stockés les types valeurs du .NET Framework (c'est-à-dire les structures, les enum et globalement les types qui dérivent de System.Value).
  • C'est sur le tas que sont stockés les types références du .NET Framework (c'est-à-dire les types qui ne dérivent pas de System.Value). Et sur la pile n'est stocké qu'un pointeur vers l'objet sur le tas.

Exemple : Prenons 2 types : PersonneRef et PersonneValue. PersonneRef est un type référence et PersonneValue un type valeur. Observons ce qui se passe en mémoire pour expliquer ces différences de comportements entre ces 2 types :

 
Sélectionnez

PersonneRef personneRef1 = new PersonneRef("Pastorius", "Jaco", new DateTime(1951, 1, 12));
PersonneValue personneVal1 = new PersonneValue("Michael Peter", "Balzary", new DateTime(1952, 10, 16));
PersonneRef personneRef2 = personneRef1;
			

Peut être résumé comme ceci :

Image non disponible

En créant une copie de PersonneRef1 nous ne copions que le pointeur vers l'objet stocké sur le tas et non l'objet lui-même. Ce qui fait qu'en modifiant l'objet via une des deux références (soit personneRef1, soit personneRef2) les deux variables seront affectées :

 
Sélectionnez

personneRef2.Prenom = "John Francis Anthony";
Console.WriteLine(personneRef1); // KO Affiche John Francis Anthony Pastorius...
Console.WriteLine(personneRef2); // OK Affiche John Francis Anthony Pastorius...
			

En revanche avec un type valeur, l'objet est entièrement copié dans la pile :

 
Sélectionnez

PersonneRef personneRef1 = new PersonneRef("Pastorius", "Jaco", new DateTime(1951, 1, 12));
PersonneValue personneVal1 = new PersonneValue("Michael Peter", "Balzary", new DateTime(1952, 10, 16));
PersonneRef personneRef2 = personneRef1;
PersonneValue personneVal2 = personneVal1;
Image non disponible

Il n'y a aucune "dépendance" entre les 2 objets personneVal1 et personneValue2, ce sont 2 objets distincts. Ce qui fait que si on modifie soit personneVal1 soit personneVal2, l'autre objet ne sera pas affecté.

Il est à noter toutefois, que certains types références se comportement comme des types valeurs, c'est-à-dire qu'il n'y a plus aucune référence à l'objet copié. System.String ainsi que System.Nullable<T> sont 2 exemples de ce type de classe qu'on appelle immuables (ou "immutable" en anglais).

Pour illustrer nos propos :

  • Nous allons créer une classe Personne.
  • Nous allons créer une collection de Personne à l'aide d'une List<Personne>

1. Copier un objet

Voici la classe Personne, qui nous que nous utiliserons pour illustrer nos exemples :

 
Sélectionnez

class Personne
{
    private string _nom;
    private string _prenom;
    private DateTime _dateNaissance;

    public DateTime DateNaissance
    {
        get { return _dateNaissance; }
        set { _dateNaissance = value; }
    }

    public string Prenom
    {
        get { return _prenom; }
        set { _prenom = value; }
    }

    public string Nom
    {
        get { return _nom; }
        set { _nom = value; }
    }

    public Personne(string nom, string prenom, DateTime dateNaissance)
    {
        _nom = nom;
        _prenom = prenom;
        _dateNaissance = dateNaissance;
    }

    public override string ToString()
    {
        string[] s = String.Format("{0} {1}  le {2}", 
                                Prenom, 
                                Nom, 
                                DateNaissance.ToShortDateString()).Split(new char[]{' '}, 
                                StringSplitOptions.RemoveEmptyEntries);
        return String.Join(" ",s);
    }
}
			

Supposons que nous instancions 1 'Personne' et que nous souhaitions garder une copie de cette instance en mémoire :

 
Sélectionnez

// On instancie notre 'Personne'
Personne p = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));

// On souhaite garder une copie de p
Personne p_bck = p;

// On vérifie si les données sont bien "copiées" dans la variable p_bck
Console.WriteLine(p);
Console.WriteLine(p_bck); // OK, identique à p
Console.WriteLine();

A priori, cela pourrait suffir, mais si on modifie notre personne p :

 
Sélectionnez

// On modifie p
p.Prenom = "Jaco";

// On revérifie.
Console.WriteLine(p);
Console.WriteLine(p_bck); // KO, l'objet p_bck a changé de valeur également.

Notre "copie" a également changé de valeur, ce n'est pas du tout ce qu'on souhaitait.
C'est dû au fait que Personne soit un type référence. Si Personne avait été un type valeur (une structure) le problème ne se serait pas posé, on aurait eu le résultat escompté :
Pour copier véritablement l'objet (et son état) il faut créer une autre instance :

 
Sélectionnez

Personne p = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));
Personne p_bck = new Personne(p.Nom, p.Prenom, p.DateNaissance);

// On vérifie si les données sont bien "copiées" dans la variable p_bck
Console.WriteLine(p);
Console.WriteLine(p_bck); // OK, identique à p
Console.WriteLine();

//On modifie p
p.Prenom = "Jaco";

// On revérifie.
Console.WriteLine(p);
Console.WriteLine(p_bck); // OK, l'objet p_bck reste une copie 
			

Il est préférable d'implémenter ce code au sein de la classe, en implémentant l'interface ICloneable :

 
Sélectionnez

#region ICloneable Members
public object Clone()
{
    PersonneCollection personnes_bck = new PersonneCollection();
    personnes_bck.AddRange(PersonneClone());
    return personnes_bck;
} 
#endregion

On peut également utiliser la méthode MemberwiseClone() de la classe Object :

 
Sélectionnez

public object Clone()
{
	return this.MemberwiseClone();
}

Et de l'utiliser ainsi :

 
Sélectionnez

Personne p = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));
Personne p_bck = (Personne)p.Clone();

// On vérifie si les données sont bien "copiées" dans la variable p_bck
Console.WriteLine(p);
Console.WriteLine(p_bck); // OK, identique à  p
Console.WriteLine();

//On modifie p
p.Prenom = "Jaco";

// On revérifie.
Console.WriteLine(p);
Console.WriteLine(p_bck); // OK, l'objet p_bck reste une copie 
			

MemberwiseClone n'est pas la solution à tous nos problèmes. En effet, il ya 2 types de copies :

  • La copie dite superficielle (ou Shallow copy en anglais).
  • La copie dite profonde (ou Deep Copy en anglais).

La copie superficielle "copie" les membres de l'instance en fonction de son type (type valeur ou type référence) c'est-à-dire que si un membre de l'instance est un type valeur, le comportement de la copie superficielle va effectivement copier la valeur dans une nouvelle variable. En revanche, si le membre de l'instance est de type référence, alors il sera de copier uniquement la référence et non la valeur du membre elle-même (voir rappels).

La copie profonde ne va pas se contenter de copier les références mais va créer des copies des valeurs de ces références.

Nous avons vu comment copier un objet simple de façon permanente. Qu'en est-il des objets composés ? Nous verrons que dans beaucoup de cas, la copie superficielle ne suffira pas.

2. Copier un objet "composé"

Un objet composé est un objet composé d'un ou plusieurs autres objets. Les collections sont un bon exemple d'objet composé.

Nous allons voir le comportement d'objet composés de type référence, puis le comportement d'objets de type valeur.

2.1. Objet composé de types références

On va créer 3 instance de la classe Personne, puis les ajouter dans une liste de Personne :

 
Sélectionnez

Personne jaco = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));
Personne marcus = new Personne("Miller", "Marcus", new DateTime(1959, 6, 14));
Personne victor = new Personne("Wooten", "Victor Lemonte", new DateTime(1964, 11, 8));

List<Personne> personnes = new List<Personne>();
personnes.Add(jaco);
personnes.Add(marcus);
personnes.Add(victor);

Console.WriteLine("Liste initiale :");
AfficherPersonnes(personnes);

affiche :

Image non disponible

Nous allons maintenant modifier la liste. La modification consistera en l'addition d'une Personne dans la liste (Notez que chaque fois que l'on modifie un objet, on change son état) :

 
Sélectionnez

//...
personnes.Add(new Personne(string.Empty, "Flea", new DateTime(1962, 10, 16)));
Console.WriteLine("Liste modifiée :");
AfficherPersonnes(personnes);
Image non disponible

Que faire si on veut retrouver notre liste initiale ? Si on crée une autre liste à partir de celle-là, nous n'obtiendrons pas le résultat escompté car nous copierons uniquement la référence de la liste (et non la liste elle-même) :

 
Sélectionnez

Personne jaco = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));
Personne marcus = new Personne("Miller", "Marcus", new DateTime(1959, 6, 14));
Personne victor = new Personne("Wooten", "Victor Lemonte", new DateTime(1964, 11, 8));

List<Personne> personnes = new List<Personne>();
personnes.Add(jaco);
personnes.Add(marcus);
personnes.Add(victor);

// On fait une "copie" de la liste initiale
List<Personne> copie_personnes = personnes;

// On affiche la liste initiale
Console.WriteLine("Liste initiale :");
AfficherPersonnes(personnes);

// On modifie la liste initiale en ajoutant un élément	
personnes.Add(new Personne(string.Empty, "Flea", new DateTime(1962, 10, 16)));

// On affiche la liste modifiée
Console.WriteLine("Liste modifiée :");
AfficherPersonnes(personnes); // OK, la liste est bien modifiée

// On affiche la copie de la liste initiale
Console.WriteLine("Copie de la liste initiale :");
AfficherPersonnes(copie_personnes); // KO, la liste affichée n'est pas la copie de la liste initiale.
				

affiche :

Image non disponible

Le moyen de contourner ceci est de copier tous les items de la collection (et non copier la référence à la collection) via la méthode CopyTo. Pour garder la liste initiale il faut donc remplacer ceci :

 
Sélectionnez

List<Personne> copie_personnes = personnes;
				

par :

 
Sélectionnez

Personne[] personnesArray = new Personne[personnes.Count];
personnes.CopyTo(personnesArray);
List<Personne> copie_personnes = new List<Personne>(personnesArray);
				

Note : le code ci-dessus est écrit de façon a insisté sur le comportement de la copie via la méthode CopyTo de la classe Array. Il se trouve qu'il existe une sur surcharge du constructeur List<T> qui fait exactement la même chose. le code suivant fera donc la même chose et est plus lisible :

 
Sélectionnez

List<Personne> copie_personnes = new List<Personne>(personnes); // Copie les éléments de la liste 'personnes'
				

Ainsi on se retrouve bien avec une vraie copie de la liste :

Image non disponible

En revanche, si on modifie un ou des items de la collection :

 
Sélectionnez

List<Personne> personnes = new List<Personne>();
personnes.Add(jaco);
personnes.Add(marcus);
personnes.Add(victor);

List<Personne> copie_personnes = new List<Personne>(personnes);
jaco.Prenom = "Jaco"; // On modifie un élément de la liste.
//...
				

Toujours du aux références, si on modifie un objet stocké dans la collection , il sera modifié partout... Hors nous souhaitons avoir une copie exacte et fiable de notre liste initiale. Il existe plusieurs méthodes pour cela dont celle-ci : la sérialisation.

2.2. La sérialisation

La sérialisation est le processus qui permet de convertir et stocker un objet en une sequence d'octets. La désérialisation est le processus inverse qui convertit une séquence d'octets en un objets. Cette séquence d'octets peut être stockée n'importe où (en mémoire où dans un fichier).

Le gros inconvénient de cette méthode est qu'il faut que la classe métier soit "sérialisable", c'est-à-dire que si elle ne l'est pas et que vous n'avez pas la main sur cette classe, il faudra passer par une autre méthode.

Si vous avez accès au code de la classe en question (ici la classe Personne), il suffit d'ajouter l'attribut [Serializable] et le tour est joué :

 
Sélectionnez

namespace CopyCollectionObjects
{
    [Serializable]
    class Personne
    {
        private string _nom;
//...
				

On va utiliser un MemoryStream dans lequel on va stocker notre collection sérialisée, puis on va stocker notre collection sérialisée dans un tableau de byte. Quand on aura besoin de retrouver notre collection initiale, il suffira de désérialiser le tableau d'octets dans un MemoryStream et de le caster dans la collection.

On ajoute un tableau d'octets :

 
Sélectionnez

static void Main(string[] args)
{
    Personne jaco = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));
    Personne marcus = new Personne("Miller", "Marcus", new DateTime(1959, 6, 14));
    Personne victor = new Personne("Wooten", "Victor Lemonte", new DateTime(1964, 11, 8));
    byte[] personnes_bck = null;
    BinaryFormatter formatter = new BinaryFormatter();
//...
				

A noter que la classe BinaryFormatter se trouve dans l'espace de nom System.Runtime.Serialization.Formatters.Binary

C'est le BinaryFormatter qui se charge de tout pour la sérialisation et la désérialisation.

Ensuite, on sérialise la List<Personne> en mémoire comme ceci :

 
Sélectionnez

using (MemoryStream stream = new MemoryStream())
{
    formatter.Serialize(stream, personnes);
    personnes_bck = stream.ToArray(); // On stocke la collection sérialisée dans un tableau d'octets
}
				

Et voilà. Maintenant si on veut récupérer la liste initiale il faut :

 
Sélectionnez

List<Personne> liste_personnes_initiale = null;
using (MemoryStream stream = new MemoryStream(personnes_bck))
{
    liste_personnes_initiale = (List<Personne>)formatter.Deserialize(stream);
}
				

Attention à ne pas oublier de renseigner le tableau d'octets à l'instanciation du memorystream !

Image non disponible

L'avantage de cette méthode est qu'elle est relativement simple à mettre en œuvre (étant donné que c'est le framework qui se charge de tout au niveau de la sérialisation et de la désérialisation). L'autre avantage énorme c'est que le binaryformatter accepte en paramètre un objet, on peut donc sérialiser n'importe quel type. On peut y stocker un graphe d'objets et non un objet seulement : Si un objet est composé d'un autre objet (par exemple une classe société a une propriété List<Employe> Employes). Il faut cependant que les objets du graphe en question ait l'attribut [Serializable].

On peut ajouter ce comportement de sauvegarde d'état en implémentant l'interface ICloneable à notre objet à sérialiser :

 
Sélectionnez

[Serializable]
class Personne : ICloneable
{
    #region ICloneable Members
    public object Clone()
    {
        BinaryFormatter formatter = new BinaryFormatter();
        object personnes_bck = null;
        using (MemoryStream stream = new MemoryStream())
        {
            formatter.Serialize(stream, this);
            stream.Position = 0;
            personnes_bck = formatter.Deserialize(stream);
        }
    } 
    #endregion
}
				

On peut ainsi l'utiliser plus facilement :

 
Sélectionnez

//...
PersonneCollection personnes_bck = (PersonneCollection)personnes.Clone();
//...
				

2.3. Copier un objet composé de types valeurs

Pour illustrer cette partie, on va simplement modifier la classe Personne en une structure Personne (ce qui revient à remplacer le mot-clé class par struct), on aura donc bien un objet (une liste) composé d'objets de type valeur (Personne). De ce fait, on n'aura plus le même type de comportement. En effet les structures étant par définition des types valeurs, les données sont directement créer dans la pile ainsi :

On remplace la classe par une structure pour la définition de Personne :

 
Sélectionnez

namespace CopyCollectionObjects
{
    [Serializable]
    struct Personne
    {
     //...
			    

Si on refait les mêmes tests qu'au départ, que se passe-t-il ? Commençons par créer notre liste initiale :

 
Sélectionnez

Personne jaco = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));
Personne marcus = new Personne("Miller", "Marcus", new DateTime(1959, 6, 14));
Personne victor = new Personne("Wooten", "Victor Lemonte", new DateTime(1964, 11, 8));

List<Personne> personnes = new List<Personne>();
personnes.Add(jaco);
personnes.Add(marcus);
personnes.Add(victor);

Console.WriteLine("Liste initiale :");
AfficherPersonnes(personnes);
			    

affiche :

Image non disponible

Pour l'instant le code est strictement identique qu'avec une classe un type référence et le résultat aussi. Maintenant, modifions notre liste :

Image non disponible

Maintenant si on veut récupérer la liste initiale ? Peut-on écrire ceci :

 
Sélectionnez
List<Personne> personnes_bck = personnes;

Et bien non, car si les éléments de la listes sont de type valeur, la liste elle, est bien un type référence. Le résultat est donc identique. On ne copie pas la liste mais uniquement sa référence.
en revanche en faisant :

 
Sélectionnez

List<Personne> personnes_bck = new List<Personne>(personnes);
				

On récupère bien la liste initiale. Essayons maintenant de modifier un élément de la liste :

 
Sélectionnez

Personne jaco = new Personne("Pastorius", "John Francis Anthony", new DateTime(1951, 12, 1));
Personne marcus = new Personne("Miller", "Marcus", new DateTime(1959, 6, 14));
Personne victor = new Personne("Wooten", "Victor Lemonte", new DateTime(1964, 11, 8));

List<Personne> personnes = new List<Personne>();
personnes.Add(jaco);
personnes.Add(marcus);
personnes.Add(victor);

List<Personne> personnes_bck = new List<Personne>(personnes);

List<Personne> personnes_bck = new List<Personne>(personnes);

Console.WriteLine("Liste initiale :");
AfficherPersonnes(personnes);

personnes.Add(new Personne(string.Empty, "Flea", new DateTime(1962, 10, 16)));

Console.WriteLine("Liste modifiée :");
AfficherPersonnes(personnes);

Console.WriteLine("Copie de la liste initiale :");
AfficherPersonnes(personnes_bck);
				

affiche :

Image non disponible

Il faut noter 2 choses : d'une part la "copie de la liste initiale" est strictement identique à la "liste initiale". C'est le comportement que nous attendions. En revanche dans la "liste modifiée" le prenom n'a pas été modifié bien que j'ai ajouté cette instruction :

 
Sélectionnez
jaco.Prenom = "jaco";

En effet ce n'est pas l'objet jaco qui est dans la liste mais une copie de l'objet. Donc si on veut modifier l'élément dans la liste, il faudra faire ça :

 
Sélectionnez

//...
int index = personnes.IndexOf(jaco); // On récupère l'index de l'objet.
Personne jacoTmp = personnes[index]; // On crée une copie de l'objet que l'on souhaite modifier. 
jacoTmp.Prenom = "Jaco";             // On modifie la copie de l'objet.
personnes.RemoveAt(index);           // On supprime l'objet que l'on souhaite modifier de la collection.
personnes.Insert(index, jacoTmp);    // Puis on insère la copie de l'objet dans la collection.
//...
				

On doit créer une copie de l'objet dans la collection, modifier cette copie, supprimer l'objet que l'on souhaitait dans la collection, puis insérer la copie de l'objet dans la collection.
Affiche

Image non disponible

3. Conclusion

Copier un objet est beaucoup moins trivial qu'il n'y parait. Si vous avez accès au code source de la classe, pensez à la sérialisation. En effet cette puissante fonctionnalité est peu coûteuse à implémenter en termes de temps. Elle permettra d'implémenter une méthode capable de copier le graphe de votre objet (Deep copy) et ce, simplement. Si vous n'avez pas accès au code source de votre classe, alors il vous faudra implémenter vous-même la méthode pour copier le graphe de votre objet via une classe helper par exemple.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2009 Nouri Florian. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.