Rappel▲
Dans le Framework .NET il y a deux espaces mémoire 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 deux 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 deux 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 deux types :
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 :
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 :
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 :
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;
Il n'y a aucune « dépendance » entre les deux objets personneVal1 et personneValue2, ce sont deux 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 deux 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>.
I. Copier un objet▲
Voici la classe Personne, qui nous que nous utiliserons pour illustrer nos exemples :
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} né 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 :
// 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 suffire, mais si on modifie notre personne p :
// 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 est 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 :
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 :
#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 :
public
object
Clone
(
)
{
return
this
.
MemberwiseClone
(
);
}
Et de l'utiliser ainsi :
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 deux 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.
II. 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'objets composés de type référence, puis le comportement d'objets de type valeur.
II-A. Objet composé de types références▲
On va créer trois instances de la classe Personne, puis les ajouter dans une liste de Personne :
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 :
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) :
//...
personnes.
Add
(
new
Personne
(
string
.
Empty,
"Flea"
,
new
DateTime
(
1962
,
10
,
16
)));
Console.
WriteLine
(
"Liste modifiée :"
);
AfficherPersonnes
(
personnes);
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) :
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 :
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 :
List<
Personne>
copie_personnes =
personnes;
par :
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 à insister 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 :
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 :
En revanche, si on modifie un ou des items de la collection :
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 dû aux références, si on modifie un objet stocké dans la collection , il sera modifié partout… Or 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.
II-B. La sérialisation▲
La sérialisation est le processus qui permet de convertir et stocker un objet en une séquence d'octets. La désérialisation est le processus inverse qui convertit une séquence d'octets en un objet. Cette séquence d'octets peut être stockée n'importe où (en mémoire ou 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é :
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 bytes. 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 :
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
(
);
//...
À 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 :
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 :
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 !
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 aient l'attribut [Serializable].
On peut ajouter ce comportement de sauvegarde d'état en implémentant l'interface ICloneable à notre objet à sérialiser :
[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 :
//...
PersonneCollection personnes_bck =
(
PersonneCollection)personnes.
Clone
(
);
//...
II-C. 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éées dans la pile ainsi :
On remplace la classe par une structure pour la définition de Personne :
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 :
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 :
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 :
Maintenant si on veut récupérer la liste initiale ? Peut-on écrire ceci :
List<
Personne>
personnes_bck =
personnes;
eh bien non, car si les éléments de la liste 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 :
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 :
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 :
Il faut noter deux 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 :
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 :
//...
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
III. 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.