Exporter (0) Imprimer
Développer tout

Sérialisation d'objets dans .NET

Piet Obermeyer
Microsoft Corporation

Résumé : Pourquoi utiliser la sérialisation ? Premièrement, pour conserver l'état d'un objet sur un support de stockage de façon à pouvoir en recréer une copie exacte ultérieurement ; deuxièmement, pour transmettre l'objet par valeur d'un domaine d'application à un autre. Par exemple, la sérialisation est utilisée afin de sauvegarder l'état de la session dans ASP.NET et copier des objets vers le presse-papiers dans les formulaires Windows. Elle est également utilisée par Remoting pour passer des objets par valeur d'un domaine d'application à un autre. Cet article constitue une introduction à la sérialisation utilisée dans Microsoft .NET.

Sommaire

Introduction
Stockage persistant
Ordonnancement par valeur
Sérialisation de base
Sérialisation sélective
Sérialisation personnalisée
Les phases du processus de sérialisation
Gestion des versions
Principes directeurs de la sérialisation

Introduction

On peut définir la sérialisation comme le processus qui consiste à stocker l'état d'une instance d'objet sur un support de stockage. Durant ce processus, les champs publics et privés de l'objet et le nom de la classe, y compris l'assemblage contenant la classe, sont convertis en un flux d'octets, celui-ci étant ensuite transcrit dans un flux de données. Par la suite, lorsque l'objet est désérialisé, un clone de l'original est créé.

Si vous mettez en œuvre un processus de sérialisation dans un environnement orienté objet, vous devez faire un certain nombre de compromis entre la facilité d'utilisation et la flexibilité. Le processus peut dans une large mesure être automatisé, à condition que vous puissiez exercer un contrôle suffisant. Par exemple, il arrive qu'une sérialisation binaire simple soit insuffisante ou qu'il soit nécessaire, pour une raison quelconque, de spécifier les champs d'une classe à sérialiser. Les sections suivantes examinent le mécanisme robuste de sérialisation proposé avec .NET Framework et mettent en évidence certaines fonctionnalités importantes qui permettent de personnaliser le processus en fonction de vos besoins.

Stockage persistant

Il est souvent nécessaire de stocker sur disque la valeur des champs d'un objet puis de récupérer cette information ultérieurement. Bien que cette opération soit facile à réaliser sans passer par la sérialisation, elle s'avère souvent fastidieuse et source d'erreur et elle se complique lorsqu'il s'agit de suivre une hiérarchie d'objets. Imaginez que vous ayez à développer une importante application métier contenant des milliers d'objets, avec la contrainte d'écrire pour chacun un code permettant de sauvegarder et de restaurer les champs et les propriétés sur disque. Avec la sérialisation, vous y parviendrez avec un minimum d'effort.

Le CLR (Common Language Runtime) gère la manière dont les objets sont organisés en mémoire et .NET Framework offre un mécanisme automatisé de sérialisation qui s'appuie sur la réflexion. Lorsqu'un objet est sérialisé, le nom de la classe, l'assemblage et tous les membres de données de l'instance de classe sont enregistrés. Les objets contiennent souvent des références à d'autres instances dans des variables de membre. Lorsque la classe est sérialisée, le moteur de sérialisation garde la trace de tous les objets référencés déjà sérialisés afin de garantir qu'un même objet n'est pas sérialisé plusieurs fois. L'architecture de sérialisation proposée avec .NET Framework traite correctement et automatiquement les graphiques d'objet et les références circulaires. La seule contrainte liée aux graphiques d'objet est que tous les objets référencés par celui qui est sérialisé doivent également être marqués avec l'attribut Serializable (voir la section Sérialisation de base). À défaut, une exception est générée en cas de tentative de sérialiser l'objet non marqué.

Lorsque la classe sérialisée est désérialisée, elle est recréée et les valeurs de tous les membres de données sont automatiquement restaurées.

Ordonnancement par valeur

Les objets ne sont valides que dans le domaine d'application où ils sont créés. Toute tentative de passer l'objet en tant que paramètre ou de le renvoyer en tant que résultat échoue, à moins que l'objet ne soit dérivé de MarshalByRefObject ou ne soit marqué comme étant Serializable. Si l'objet est marqué avec l'attribut Serializable, il sera automatiquement sérialisé, transféré d'un domaine d'application à l'autre, puis désérialisé de façon à produire, dans le second domaine d'application, une copie exacte de l'original. Ce processus est en général appelé "ordonnancement par valeur" (marshal by value).

Lorsqu'un objet est dérivé de MarshalByRefObject, une référence d'objet et non l'objet lui-même est transférée d'un domaine d'application à l'autre. Vous pouvez également marquer un objet dérivé de MarshalByRefObject comme étant Serializable. Lorsque cet objet est utilisé avec Remoting, le formateur chargé de la sérialisation, qui a été préconfiguré avec un sélecteur de substitution (SurrogateSelector), prend le contrôle du processus de sérialisation et remplace tous les objets dérivés de MarshalByRefObject par un proxy. Sans le sélecteur SurrogateSelector, l'architecture de sérialisation obéit aux règles standard de sérialisation (voir la section Les phases du processus de sérialisation).

Sérialisation de base

Le plus simple pour qu'une classe soit sérialisable est de la marquer avec l'attribut Serializable, comme ci-après :

[Serializable]
public class MyObject {
  public int n1 = 0;
  public int n2 = 0;
  public String str = null;
}

L'extrait de code ci-dessous montre comment une instance de cette classe peut être sérialisée dans un fichier :

MyObject obj = new MyObject();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "Some String";
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Create, 
FileAccess.Write, FileShare.None);
formatter.Serialize(stream, obj);
stream.Close();

Dans cet exemple, un formateur binaire est utilisé pour effectuer la sérialisation. Il vous suffit de créer une instance du flux et le formateur à utiliser, puis d'appeler la méthode Serialize sur le formateur. Le flux et l'instance d'objet à sérialiser sont fournis comme paramètres pour cet appel. Bien que cela n'apparaisse pas explicitement dans cet exemple, toutes les variables de membre d'une classe seront sérialisées, même celles marquées comme étant privées (private). Sur ce point, la sérialisation binaire diffère du sérialiseur XML qui ne sérialise que des champs publics.

La restauration de l'objet dans son état antérieur est tout aussi facile. En premier lieu, il convient de créer un formateur et un flux pour la lecture, puis de donner la consigne au formateur de désérialiser l'objet. L'extrait de code ci-dessous illustre l'opération.

IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Open, 
FileAccess.Read, FileShare.Read);
MyObject obj = (MyObject) formatter.Deserialize(fromStream);
stream.Close();

// Voici la preuve
Console.WriteLine("n1: {0}", obj.n1);
Console.WriteLine("n2: {0}", obj.n2);
Console.WriteLine("str: {0}", obj.str);

Le formateur BinaryFormatter utilisé ici est très performant et génère un flux d'octets très compact. Tous les objets sérialisés avec ce formateur peuvent être désérialisés avec celui-ci, ce qui en fait l'outil idéal pour sérialiser des objets qui seront désérialisés sur la plate-forme .NET. Il est important de noter l'absence d'appel à des constructeurs durant la désérialisation d'un objet. Il s'agit là d'une contrainte imposée à dessein au processus de désérialisation pour des raisons de performances. Cependant, elle vient à l'encontre de certaines règles habituelles établies entre le générateur d'objets et le module d'exécution et les développeurs doivent être pleinement conscients des conséquences de la déclaration d'un objet sérialisable.

Si la portabilité est une condition à prendre en compte, utilisez plutôt le formateur SoapFormatter. Pour ce faire, remplacez simplement le formateur cité dans le code ci-dessus par SoapFormatter, puis appelez les méthodes Serialize et Deserialize. Avec l'exemple de code ci-dessus, ce formateur génère le résultat suivant.

<SOAP-ENV:Envelope
  xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:SOAP- ENC=http://schemas.xmlsoap.org/soap/encoding/
  xmlns:SOAP- ENV=http://schemas.xmlsoap.org/soap/envelope/
  SOAP-ENV:encodingStyle=
  "http://schemas.microsoft.com/soap/encoding/clr/1.0
  http://schemas.xmlsoap.org/soap/encoding/"
  xmlns:a1="http://schemas.microsoft.com/clr/assem/ToFile">

  <SOAP-ENV:Body>
    <a1:MyObject id="ref-1">
      <n1>1</n1>
      <n2>24</n2>
      <str id="ref-3">Some String</str>
    </a1:MyObject>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Il est important de noter que l'attribut Serializable ne peut pas être hérité. Si nous dérivons une nouvelle classe de MyObject, celle-ci doit être également marquée avec l'attribut, sinon elle ne peut pas être sérialisée. Par exemple, si vous tentez de sérialiser une instance de la classe ci-dessous, vous obtenez une SerializationException vous informant que le type MyStuff n'est pas marqué comme étant sérialisable.

public class MyStuff : MyObject 
{
  public int n3;
}

L'utilisation de l'attribut de sérialisation s'avère pratique, mais elle comporte certaines contraintes, comme nous venons de le voir. Pour savoir quand marquer une classe pour la sérialisation, reportez-vous à la section Principes directeurs de la sérialisation plus loin dans ce document, puisqu'il est impossible d'ajouter la sérialisation à une classe une fois celle-ci compilée.

Sérialisation sélective

Souvent, une classe comprend des champs qui ne doivent pas être sérialisés. Prenons l'exemple d'une classe qui contient un ID de thread dans une variable de membre. Lorsque la classe est désérialisée, le thread stocké avec cet ID lorsque la classe était sérialisée risque de ne plus s'exécuter, de sorte que la sérialisation de cette valeur n'a aucun sens. Vous pouvez éviter que des variables de membre ne soient sérialisées en les marquant avec l'attribut NonSerialized, comme suit :

[Serializable]
public class MyObject 
{
  public int n1;
  [NonSerialized] public int n2;
  public String str;
}

Sérialisation personnalisée

Vous pouvez personnaliser le processus de sérialisation en mettant en œuvre l'interface ISerializable sur un objet. Cette technique est particulièrement utile lorsque la valeur d'une variable de membre est incorrecte après la désérialisation, mais que vous devez malgré tout fournir une valeur à cette variable afin de recréer l'état complet de l'objet. Le recours à l'interface ISerializable suppose la mise en œuvre de la méthode GetObjectData et d'un constructeur spécial qui sera utilisé au moment de la désérialisation de l'objet. L'exemple de code ci-dessous montre comment mettre en œuvre l'interface ISerializable sur la classe MyObject (citée plus haut).

[Serializable]
public class MyObject : ISerializable 
{
  public int n1;
  public int n2;
  public String str;

  public MyObject()
  {
  }

  protected MyObject(SerializationInfo info, StreamingContext context)
  {
    n1 = info.GetInt32("i");
    n2 = info.GetInt32("j");
    str = info.GetString("k");
  }

  public virtual void GetObjectData(SerializationInfo info, 
StreamingContext context)
  {
    info.AddValue("i", n1);
    info.AddValue("j", n2);
    info.AddValue("k", str);
  }
}

Lorsque la méthode GetObjectData est appelée durant la sérialisation, vous devez remplir l'objet SerializationInfo fourni avec l'appel à la méthode. Pour ce faire, ajoutez les variables à sérialiser sous la forme de paires nom/valeur. N'importe quel texte peut servir de nom. Vous avez la possibilité de choisir les variables de membre qui doivent être ajoutées dans SerializationInfo, à condition qu'un nombre suffisant de données soit sérialisé pour permettre la restauration de l'objet au moment de la désérialisation. Les classes dérivées doivent appeler la méthode GetObjectData sur l'objet de base si celui-ci implémente l'interface ISerializable.

N'oubliez pas que vous devez implémenter à la fois la méthode GetObjectData et le constructeur spécial lorsque ISerializable est ajoutée dans une classe. Si la méthode GetObjectData est absente, le compilateur vous avertit, mais comme il est impossible d'imposer l'implémentation d'un constructeur, vous ne serez pas informé de l'absence ce dernier et une exception sera générée en cas de tentative de désérialiser une classe sans constructeur. Le modèle actuel a été préféré à la méthode SetObjectData pour contourner les problèmes éventuels de sécurité et de gestion des versions. Par exemple, une méthode SetObjectData doit être publique si elle est définie comme élément d'une interface, de sorte que les utilisateurs doivent écrire du code évitant que la méthode SetObjectData ne soit appelée plusieurs fois. Il est facile d'imaginer les problèmes générés par une application malveillante qui appellerait la méthode SetObjectData sur un objet en train d'exécuter une opération.

Durant la désérialisation, SerializationInfo est transmis à la classe à l'aide du constructeur fourni à cet effet. Les contraintes de visibilité associées au constructeur sont ignorées lorsque l'objet est désérialisé, et vous pouvez donc marquer la classe comme étant publique, protégée, interne ou privée. Il est bon de protéger le constructeur, à moins que la classe ne soit scellée (sealed), auquel cas le constructeur doit être marqué comme étant privé. Pour restaurer l'état de l'objet, il suffit de récupérer les valeurs des variables à partir de SerializationInfo en utilisant les noms adoptés durant la sérialisation. Si la classe de base met en œuvre l'interface ISerializable, le constructeur de base doit être appelé pour que l'objet de base puisse restaurer ses variables.

Lorsque vous dérivez une nouvelle classe à partir d'une classe qui met en œuvre l'interface ISerializable, la classe dérivée doit implémenter à la fois le constructeur et la méthode GetObjectData s'il existe des variables à sérialiser. L'extrait de code ci-dessous montre le processus, avec la classe MyObject évoquée plus haut.

[Serializable]
public class ObjectTwo : MyObject
{
  public int num;

  public ObjectTwo() : base()
  {
  }

  protected ObjectTwo(SerializationInfo si, StreamingContext context) : 
base(si,context)
  {
    num = si.GetInt32("num");
  }

  public override void GetObjectData(SerializationInfo si, 
StreamingContext context)
  {
    base.GetObjectData(si,context);
    si.AddValue("num", num);
  }
}

N'oubliez pas d'appeler la classe de base dans le constructeur de désérialisation ; à défaut, le constructeur sur la classe de base ne sera jamais appelé et l'objet ne sera pas entièrement construit après la désérialisation.

Les objets sont reconstruits de l'intérieur vers l'extérieur et le fait d'appeler des méthodes durant la désérialisation peut avoir des conséquences néfastes car les méthodes appelées risquent de renvoyer à des références d'objets pas encore désérialisés au moment de l'appel. Si la classe en cours de désérialisation implémente l'interface IDeserializationCallback, la méthode OnSerialization est automatiquement appelée une fois que le graphe complet de l'objet est désérialisé. À ce stade, tous les objets enfants référencés sont totalement restaurés. Une table de hachage est l'exemple classique de classe qu'il est difficile de désérialiser sans utiliser le programme de réception d'événement (event listener) décrit plus haut. Il est facile de récupérer les paires clé/valeur durant la désérialisation, mais la réintégration de ces objets dans la table de hachage peut provoquer des problèmes car il n'est pas garanti que des classes dérivées de la table de hachage aient été désérialisées. Il est donc déconseillé d'appeler des méthodes sur une table de hachage à ce moment du processus.

Les phases du processus de sérialisation

Lorsque le méthode Serialize est appelée sur un formateur, la sérialisation d'objet se déroule selon les règles suivantes :

  • Un contrôle est effectué pour déterminer si le formateur a un sélecteur de substitution. Si c'est le cas, vérifiez si ce sélecteur gère des objets du type donné. Si le sélecteur gère le type d'objet en question, l'interface ISerializable.GetObjectData est appelée sur le sélecteur.
  • S'il n'y a pas de sélecteur de substitution ou s'il ne gère pas le type d'objet, un contrôle est effectué pour déterminer si l'objet est marqué avec l'attribut Serializable. S'il ne l'est pas, une exception SerializationException est générée.
  • Si l'objet est marqué comme étant sérialisable, vérifiez s'il implémente l'interface ISerializable. Dans l'affirmative, la méthode GetObjectData est appelée sur l'objet.
  • S'il n'implémente pas ISerializable, la méthode de sérialisation par défaut est appliquée, avec une sérialisation de tous les champs qui ne sont pas marqués NonSerialized.

Gestion des versions

.NET Framework prend en charge la gestion des versions et l'exécution côte à côte, et toutes les classes fonctionnent avec plusieurs versions si les interfaces des classes restent les mêmes. Comme la sérialisation s'applique à des variables de membre et non à des interfaces, soyez prudent lorsque vous ajoutez ou supprimez des variables de membre dans des classes qui seront sérialisées sur plusieurs versions. C'est notamment le cas pour les classes qui n'implémentent pas l'interface ISerializable. Toute modification de l'état de la version en cours, telle que l'ajout de variables de membre, la modification du type des variables ou la modification de leur nom signifie qu'il sera impossible de désérialiser des objets existants de même type s'ils ont pas été sérialisés avec une version antérieure.

Si l'état d'un objet doit être modifié entre deux versions, les auteurs de classe ont deux possibilités :

  • Implémenter ISerializable, ce qui permet de contrôler précisément les processus de sérialisation et désérialisation en autorisant l'ajout et l'interprétation correcte du futur état lors de la désérialisation.
  • Marquer les variables de membre non essentielles avec l'attribut NonSerialized. Cette option doit être adoptée uniquement si vous n'envisagez que des modifications minimes entre les différentes versions d'une classe. Par exemple, si une nouvelle variable a été ajoutée dans la nouvelle version d'une classe, la variable peut être marquée avec l'attribut NonSerialized afin de garantir que la classe reste compatible avec les versions antérieures.

Principes directeurs de la sérialisation

Vous devez envisager la sérialisation dès le développement de nouvelles classe, car une classe ne peut pas être déclarée sérialisable une fois qu'elle est compilée. Vous devez vous poser certaines questions telles que : dois-je envoyer cette classe sur différents domaines d'application ? Cette classe sera-t-elle un jour utilisée avec Remoting ? Que feront les utilisateurs avec cette classe ? Peut-être vont-ils, à partir de ma classe, dériver une nouvelle classe qui devra être sérialisée ? Dans le doute, déclarez la classe comme étant sérialisable. Il vaut sans doute mieux marquer toutes les classes comme étant sérialisables, sauf dans les cas suivants :

  • Elles ne seront jamais transférées vers un autre domaine d'application. Si la sérialisation n'est pas nécessaire et si la classe doit sortir d'un domaine d'application, dérivez-la de MarshalByRefObject.
  • La classe stocke des pointeurs spéciaux qui ne sont applicables qu'à son instance en cours. Si une classe contient de la mémoire non gérée ou des descripteurs de fichier par exemple, assurez-vous que ces champs ont l'attribut NonSerialized ou choisissez de ne pas sérialiser la classe.
  • Certains membres de données contiennent des informations sensibles. Dans ce cas, il est probablement plus sage d'implémenter l'interface ISerializable et de ne sérialiser que les champs nécessaires.


Dernière mise à jour le lundi 7 janvier 2002



Pour en savoir plus
Afficher:
© 2014 Microsoft