Share via


Sérialisation d’objets dans le .NET Framework

 

Piet Obermeyer et Jonathan Hawkins
Microsoft Corporation

Août 2001
Mise à jour de mars 2002

Résumé: Pourquoi voulez-vous utiliser la sérialisation ? Les deux raisons les plus importantes sont de conserver l’état d’un objet sur un support de stockage afin qu’une copie exacte puisse être recréée ultérieurement, et d’envoyer l’objet par valeur d’un domaine d’application à un autre. Par exemple, la sérialisation est utilisée pour enregistrer l’état de session dans ASP.NET et pour copier des objets dans le Presse-papiers dans Windows Forms. Elle est également utilisée par la communication à distance pour passer des objets par valeur d'un domaine d'application à un autre. Cet article fournit une vue d’ensemble de la sérialisation utilisée dans Microsoft .NET Framework. (9 pages imprimées)

Contenu

Introduction
Stockage persistant
Marshaler par valeur
Sérialisation de base
Sérialisation sélective
Sérialisation personnalisée
Étapes du processus de sérialisation
Gestion de version
Indications concernant la sérialisation

Introduction

La sérialisation peut être définie comme le processus de stockage de l’état d’un objet instance sur un support de stockage. Au cours de ce processus, les champs publics et privés de l’objet et le nom de la classe, y compris l’assembly contenant la classe, sont convertis en un flux d’octets, qui est ensuite écrit dans un flux de données. Lorsque l'objet est désérialisé par la suite, un clone exact de l'objet d'origine est créé.

Lorsque vous implémentez un mécanisme de sérialisation dans un environnement orienté objet, vous devez faire plusieurs compromis entre facilité d'utilisation et souplesse. Le processus peut être automatisé en grande partie, à condition que vous puissiez suffisamment le contrôler. Par exemple, dans certaines situations, la sérialisation binaire simple n'est pas suffisante ou une raison particulière peut exiger la définition des champs à sérialiser. Les sections suivantes étudient le mécanisme de sérialisation fiable fourni avec le .NET Framework et mettent en évidence plusieurs fonctionnalités importantes qui vous permettent de personnaliser le processus selon vos besoins.

Stockage persistant

Il est souvent nécessaire de stocker la valeur des champs d’un objet sur le disque, puis de récupérer ces données à un stade ultérieur. Bien que cela soit facile à accomplir sans utiliser la sérialisation, cette méthode est souvent fastidieuse et susceptible d'entraîner des erreurs. Elle devient en outre de plus en plus complexe lorsque vous devez suivre une hiérarchie d'objets. Imaginez l’écriture d’une grande application métier contenant plusieurs milliers d’objets et l’écriture de code pour enregistrer et restaurer les champs et les propriétés vers et depuis le disque pour chaque objet. La sérialisation fournit un mécanisme pratique pour atteindre cet objectif avec un minimum d’effort.

Le Common Language Runtime (CLR) gère la façon dont les objets sont disposés en mémoire et le .NET Framework fournit un mécanisme de sérialisation automatisé à l’aide de la réflexion. Lorsqu'un objet est sérialisé, le nom de la classe, l'assembly et tous les membres de données de l'instance de classe sont écrits sur le support de stockage. Les objets stockent souvent des références à d'autres instances dans les variables membres. Lorsque la classe est sérialisée, le moteur de sérialisation effectue le suivi de tous les objets référencés déjà sérialisés pour s’assurer que le même objet n’est pas sérialisé plus d’une fois. L’architecture de sérialisation fournie avec .NET Framework gère correctement les graphiques d’objets et les références circulaires automatiquement. La seule exigence placée sur les graphiques d’objets est que tous les objets référencés par l’objet en cours de sérialisation doivent également être marqués comme sérialisables (voir Sérialisation de base). Si tel n'est pas le cas, une exception est levée lorsque le sérialiseur tente de sérialiser l'objet non marqué.

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

Marshaler 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 retourner en tant que résultat échoue, sauf si l’objet dérive de MarshalByRefObject ou est marqué comme sérialisable. Si l’objet est marqué comme sérialisable, l’objet est automatiquement sérialisé, transporté d’un domaine d’application à l’autre, puis désérialisé pour produire une copie exacte de l’objet dans le deuxième domaine d’application. Ce processus est généralement appelé marshal par valeur.

Lorsqu’un objet dérive de MarshalByRefObject, une référence d’objet est passée d’un domaine d’application à un autre, plutôt que de l’objet lui-même. Vous pouvez également marquer un objet qui dérive de MarshalByRefObject comme sérialisable. Lorsque cet objet est utilisé avec la communication à distance, le formateur responsable de la sérialisation, qui a été préconfigurée avec un SurrogateSelector prend le contrôle du processus de sérialisation et remplace tous les objets dérivés de MarshalByRefObject par un proxy. Sans surrogateSelector en place, l’architecture de sérialisation suit les règles de sérialisation standard (voir Étapes du processus de sérialisation) ci-dessous.

Sérialisation de base

Le moyen le plus simple de rendre une classe sérialisable consiste à la marquer avec l’attribut Serializable comme suit :

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

L’extrait de code ci-dessous montre comment un instance de cette classe peut être sérialisé 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();

Cet exemple utilise un formateur binaire pour effectuer la sérialisation. Il vous suffit de créer une instance du flux de données et du formateur que vous envisagez d’utiliser, puis d’appeler la méthode Serialize sur le formateur. Le flux et l’objet instance à sérialiser sont fournis en tant que paramètres pour cet appel. Bien que cela ne soit pas explicitement démontré dans cet exemple, toutes les variables membres d’une classe sont sérialisées, même les variables marquées comme privées. Dans cet aspect, la sérialisation binaire diffère du sérialiseur XML, qui sérialise uniquement les champs publics.

La restauration de l'objet à son état précédent est très facile. Tout d’abord, créez un formateur et un flux pour la lecture, puis demandez au formateur de désérialiser l’objet. L’extrait de code ci-dessous montre comment procéder.

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

// Here's the proof
Console.WriteLine("n1: {0}", obj.n1);
Console.WriteLine("n2: {0}", obj.n2);
Console.WriteLine("str: {0}", obj.str);

Le BinaryFormatter utilisé ci-dessus est très efficace et produit un flux d’octets très compact. Tous les objets sérialisés avec ce formateur peuvent également être désérialisés avec lui, ce qui en fait un outil idéal pour sérialiser des objets qui seront désérialisés sur la plateforme .NET. Il est important de noter que les constructeurs ne sont pas appelés lorsqu'un objet est désérialisé. Toutefois, cela enfreint certains des contrats habituels que le temps d’exécution effectue avec l’enregistreur d’objets, et les développeurs doivent s’assurer qu’ils comprennent les conséquences du marquage d’un objet comme sérialisable.

Si la portabilité est requise, utilisez plutôt SoapFormatter . Remplacez simplement le formateur dans le code ci-dessus par SoapFormatter, puis appelez Serialize et Déserialize comme précédemment. Ce formateur génère le résultat suivant pour l'exemple utilisé ci-dessus.

<SOAP-ENV:Envelope
  xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:SOAP- ENC=https://schemas.xmlsoap.org/soap/encoding/
  xmlns:SOAP- ENV=https://schemas.xmlsoap.org/soap/envelope/
  SOAP-ENV:encodingStyle=
  "https://schemas.microsoft.com/soap/encoding/clr/1.0
  https://schemas.xmlsoap.org/soap/encoding/"
  xmlns:a1="https://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, la nouvelle classe doit également être marquée avec l’attribut, sinon elle ne peut pas être sérialisée. Par exemple, lorsque vous tentez de sérialiser un instance de la classe ci-dessous, vous obtenez une exception SerializationException vous informant que le type MyStuff n’est pas marqué comme sérialisable.

public class MyStuff : MyObject 
{
  public int n3;
}

L’utilisation de l’attribut de sérialisation est pratique, mais elle présente des limitations comme illustré ci-dessus. Reportez-vous aux instructions (voir Instructions de sérialisation ci-dessous) concernant le moment où marquer une classe pour la sérialisation, car la sérialisation ne peut pas être ajoutée à une classe après sa compilation.

Sérialisation sélective

Une classe contient souvent des champs qui ne doivent pas être sérialisés. Par exemple, supposez qu'une classe stocke un ID de thread dans une variable membre. Lorsque la classe est désérialisée, le thread stocké pour l’ID lorsque la classe a été sérialisée peut ne plus être en cours d’exécution. Il est donc inutile de sérialiser cette valeur. Vous pouvez empêcher la sérialisation des variables membres 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 implémentant l’interface ISerializable sur un objet. Cela est particulièrement utile dans les cas où la valeur d’une variable membre n’est pas valide après la désérialisation, mais que vous devez fournir une valeur à la variable afin de reconstruire l’état complet de l’objet. L’implémentation d’ISerializable implique l’implémentation de la méthode GetObjectData et d’un constructeur spécial qui sera utilisé lorsque l’objet sera désérialisé. L’exemple de code ci-dessous montre comment implémenter ISerializable sur la classe MyObject d’une section précédente.

[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 GetObjectData est appelé pendant la sérialisation, il vous incombe de remplir l’objet SerializationInfo fourni avec l’appel de méthode. Ajoutez simplement les variables à sérialiser en tant que paires nom/valeur. Tout texte peut être utilisé comme nom. Vous avez la liberté de décider quelles variables membres sont ajoutées à SerializationInfo, à condition que suffisamment de données soient sérialisées pour restaurer l’objet pendant la désérialisation. Les classes dérivées doivent appeler la méthode GetObjectData sur l’objet de base si ce dernier implémente ISerializable.

Il est important de souligner que vous devez implémenter GetObjectData ainsi que le constructeur spécial quand ISerializable est ajouté à une classe. Le compilateur vous avertit si GetObjectData est manquant, mais étant donné qu’il est impossible d’appliquer l’implémentation d’un constructeur, aucun avertissement ne sera donné si le constructeur est absent et une exception sera levée lorsqu’une tentative de désérialisation d’une classe sans le constructeur est effectuée. La conception actuelle a été privilégiée au-dessus d’une méthode SetObjectData pour contourner les problèmes potentiels de sécurité et de contrôle de version. Par exemple, une méthode SetObjectData doit être publique si elle est définie dans le cadre d’une interface. Par conséquent, les utilisateurs doivent écrire du code pour se défendre contre l’appel de la méthode SetObjectData plusieurs fois. On peut imaginer les maux de tête qui peuvent potentiellement être causés par une application malveillante qui appelle la méthode SetObjectData sur un objet qui était en cours d’exécution d’une opération.

Pendant la désérialisation, serializationInfo est passé à la classe à l’aide du constructeur fourni à cet effet. Toutes les contraintes de visibilité placées sur le constructeur sont ignorées lorsque l’objet est désérialisé. Vous pouvez donc marquer la classe comme publique, protégée, interne ou privée. Il est judicieux de protéger le constructeur, sauf si la classe est scellée, auquel cas le constructeur doit être marqué privé. Pour restaurer l’état de l’objet, récupérez simplement les valeurs des variables à partir de SerializationInfo à l’aide des noms utilisés pendant la sérialisation. Si la classe de base implémente ISerializable, le constructeur de base doit être appelé pour permettre à l’objet de base de restaurer ses variables.

Lorsque vous dérivez une nouvelle classe d’une classe qui implémente ISerializable, la classe dérivée doit implémenter à la fois le constructeur ainsi que la méthode GetObjectData si des variables doivent être sérialisées. L’extrait de code ci-dessous montre comment procéder à l’aide de la classe MyObject présentée précédemment.

[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 ; si ce n’est pas le cas, le constructeur de 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 les méthodes appelantes pendant la désérialisation peuvent avoir des effets secondaires indésirables, car les méthodes appelées peuvent faire référence à des références d’objets qui n’ont pas été désérialisées au moment où l’appel est effectué. Si la classe en cours de désérialisation implémente IDeserializationCallback, la méthode OnSerialization est automatiquement appelée lorsque l’ensemble du graphe d’objets a été désérialisé. À ce stade, tous les objets enfants référencés ont été restaurés complètement. Une table de hachage est un exemple typique d’une classe qui est difficile à désérialiser sans utiliser l’écouteur d’événements décrit ci-dessus. Il est facile de récupérer les paires clé/valeur pendant la désérialisation, mais l’ajout de ces objets à la table de hachage peut entraîner des problèmes, car il n’est pas garanti que les classes dérivées de la table de hachage aient été désérialisées. Par conséquent, les méthodes d'appel sur une table de hachage ne sont pas recommandées à ce stade.

Étapes du processus de sérialisation

Lorsque la méthode Serialize est appelée sur un formateur, la sérialisation des objets se poursuit selon les règles suivantes :

  • Une case activée est effectuée pour déterminer si le formateur a un sélecteur de substitution. Si c’est le cas, case activée si le sélecteur de substitution gère les objets du type donné. Si le sélecteur gère le type d’objet, ISerializable.GetObjectData est appelé sur le sélecteur de substitution.
  • S’il n’existe aucun sélecteur de substitution ou s’il ne gère pas le type, une case activée est effectuée pour déterminer si l’objet est marqué avec l’attribut Serializable. Si ce n’est pas le cas, une serializationException est levée.
  • S’il est marqué de manière appropriée, case activée si l’objet implémente ISerializable. Si c’est le cas, GetObjectData est appelé sur l’objet .
  • S’il n’implémente pas ISerializable, la stratégie de sérialisation par défaut est utilisée, sérialisant tous les champs non marqués comme non marqués comme non sérialisés.

Gestion de version

Le .NET Framework prend en charge le contrôle de version et l’exécution côte à côte, et toutes les classes fonctionnent entre les versions si les interfaces des classes restent les mêmes. Étant donné que les sérialisations traitent des variables membres et non des interfaces, soyez prudent lorsque vous ajoutez ou supprimez des variables membres dans des classes qui seront sérialisées entre les versions. Cela est particulièrement vrai pour les classes qui n’implémentent pas ISerializable. Tout changement d’état de la version actuelle, comme l’ajout de variables membres, la modification des types de variables ou la modification de leurs noms, signifie que les objets existants du même type ne peuvent pas être désérialisés correctement s’ils ont été sérialisés avec une version précédente.

Si l’état d’un objet doit changer d’une version à l’autre, les auteurs de classes ont deux choix :

  • Implémentez ISerializable. Cela vous permet de contrôler précisément le processus de sérialisation et de désérialisation, ce qui permet d’ajouter et d’interpréter correctement l’état futur pendant la désérialisation.
  • Marquez les variables membres non essentielles avec l’attribut NonSérialized . Cette option ne doit être utilisée que lorsque vous attendez des modifications mineures entre différentes versions d’une classe. Par exemple, lorsqu’une nouvelle variable a été ajoutée à une version ultérieure d’une classe, la variable peut être marquée comme non sérialisée pour garantir la compatibilité de la classe avec les versions précédentes.

Indications concernant la sérialisation

Vous devez envisager la sérialisation lors de la conception de nouvelles classes, car une classe ne peut pas être rendue sérialisable une fois qu’elle a été compilée. Voici quelques questions à poser : dois-je envoyer cette classe entre les domaines d’application ? Est-ce que cette classe risque d'être utilisée avec la communication à distance ? Que feront mes utilisateurs avec cette classe ? Peut-être qu’ils dérivent une nouvelle classe de la mienne qui doit être sérialisée. En cas de doute, marquez la classe comme sérialisable. Il est probablement préférable de marquer toutes les classes comme sérialisables, sauf si :

  • Ils ne traverseront jamais un domaine d’application. Si la sérialisation n’est pas requise et que la classe doit traverser un domaine d’application, dérivez la classe de MarshalByRefObject.
  • La classe stocke des pointeurs spéciaux qui s’appliquent uniquement aux instance actuelles de la classe. Si une classe contient de la mémoire ou des handles de fichier non managés, par exemple, assurez-vous que ces champs sont marqués comme non sérialisés ou ne sérialisent pas du tout la classe.
  • Certains des membres de données contiennent des informations sensibles. Dans ce cas, il sera probablement conseillé d’implémenter ISerializable et de sérialiser uniquement les champs requis.