Objektserialisierung in .NET

Veröffentlicht: 01. Nov 2001 | Aktualisiert: 07. Nov 2004
Von Piet Obermeyer

Welche Gründe sprechen für den Einsatz der Serialisierung? Die beiden wichtigsten Gründe für die Serialisierung sind: Der Status eines Objekts soll auf einem Speichermedium gespeichert werden, damit zu einem späteren Zeitpunkt eine genaue Kopie des Objekts erstellt werden kann, und das Objekt soll nach Wert von einer Anwendungsdomäne an die nächste gesendet werden. Die Serialisierung wird z.B. verwendet, um den Sitzungsstatus in ASP.NET zu speichern und Objekte in die Zwischenablage von Windows Forms zu kopieren. Sie wird zudem im Remoteverfahren verwendet, um Objekte nach Wert von einer Anwendungsdomäne an die andere zu übergeben. Dieser Artikel bietet einen Überblick über die in Microsoft .NET verwendete Serialisierung.

* * *

Auf dieser Seite

Einführung Einführung
Persistente Speicherung Persistente Speicherung
Marshallen nach Wert Marshallen nach Wert
Standardserialisierung Standardserialisierung
Selektive Serialisierung Selektive Serialisierung
Benutzerdefinierte Serialisierung Benutzerdefinierte Serialisierung
Schritte im Serialisierungsprozess Schritte im Serialisierungsprozess
Versionsprüfung Versionsprüfung
 Serialisierungsrichtlinien  Serialisierungsrichtlinien

Einführung

Serialisierung kann als der Prozess zum Speichern des Status einer Objektinstanz auf einem Speichermedium bezeichnet werden. Während dieses Prozesses werden die öffentlichen und privaten Felder des Objekts und der Name der Klasse, einschließlich der Assembly, die die Klasse enthält, in einen Strom von Bytes konvertiert, der dann in einen Datenstrom geschrieben wird. Wenn das Objekt anschließend deserialisiert wird, wird ein exakter Klon des Originalobjekts erstellt.

Wenn Sie einen Serialisierungsmechanismus in eine objektorientierte Umgebung implementieren, müssen Sie eine Reihe von Kompromissen zwischen einfacher Verwendung und Flexibilität schließen. Der Prozess kann zum großen Teil automatisiert werden, sofern Sie ausreichende Kontrolle über den Prozess besitzen. Es können z.B. Situationen entstehen, in denen eine einfache binäre Serialisierung nicht ausreichend ist, oder ggf. muss aus einem bestimmten Grund entschieden werden, welche Felder in einer Klasse serialisiert werden müssen. In den folgenden Abschnitten wird der stabile Serialisierungsmechanismus untersucht, der vom .NET Framework bereitgestellt wird. Zudem werden eine Reihe von wichtigen Features hervorgehoben, mit denen Sie den Prozess an Ihre Anforderungen anpassen können.

Persistente Speicherung

Es ist häufig erforderlich, den Wert der Felder eines Objekts auf einem Datenträger zu speichern und diese Daten dann zu einem späteren Zeitpunkt abzurufen. Obwohl dies einfach zu erzielen ist, ohne die Serialisierung einzusetzen, ist dieser Ansatz oft umständlich und fehleranfällig und wird in zunehmendem Maß komplexer, wenn Sie eine Hierarchie von Objekten verfolgen müssen. Stellen Sie sich vor, dass Sie eine umfangreiche Geschäftsanwendung mit mehreren tausend Objekten entwickeln und Code schreiben müssen, um die Felder und Eigenschaften für jedes Objekt auf einem Datenträger zu speichern und von einem Datenträger abzurufen. Die Serialisierung stellt einen bequemen Mechanismus bereit, um dieses Ziel mit minimalem Aufwand zu erreichen.

Die gemeinsame Sprachlaufzeit (CLR = Common Language Runtime) verwaltet die Anordnung der Objekte im Speicher, und das .NET Framework stellt einen automatisierten Serialisierungsmechanismus bereit, indem eine Spiegelung verwendet wird. Wenn ein Objekt serialisiert wurde, werden der Name der Klasse, die Assembly und alle Datenelemente der Klasseninstanz in den Speicher geschrieben. Objekte speichern oft Verweise auf andere Instanzen in Elementvariablen. Wenn die Klasse serialisiert ist, dann verfolgt das Serialisierungsmodul alle referenzierten Objekte, um sicherzustellen, dass dasselbe Objekt nur einmal serialisiert wird. Die durch das .NET Framework bereitgestellte Serialisierungsarchitektur behandelt die Objektgrafiken und zirkulären Verweise automatisch korrekt. Die einzige Anforderung, die bei Objektgrafiken beachtet werden muss, ist, dass alle Objekte, die von dem serialisierten Objekt referenziert werden, auch mit Hilfe des Serializable-Attributs als serialisierbar gekennzeichnet werden müssen (siehe Standardserialisierung). Wenn dies nicht erfolgt, wird eine Ausnahme ausgegeben, wenn der Serialisierer versucht, das nicht gekennzeichnete Objekt zu serialisieren.

Wenn die serialisierte Klasse deserialisiert wird, wird die Klasse neu erstellt, und die Werte aller Datenelemente werden automatisch wiederhergestellt.

Marshallen nach Wert

Objekte sind nur in der Anwendungsdomäne gültig, in der sie erstellt wurden. Alle Versuche, das Objekt als Parameter zu übergeben oder es als Ergebnis zurückzugeben, schlagen fehl, sofern das Objekt nicht von einem MarshalByRefObject abgeleitet oder als Serialisierbar gekennzeichnet ist. Falls das Objekt mit dem Serializable-Attribut als serialisierbar gekennzeichnet ist, wird das Objekt automatisch serialisiert, von einer Anwendungsdomäne an die andere übergeben und anschließend deserialisiert, um eine exakte Kopie des Objekts in der zweiten Anwendungsdomäne zu erstellen. Dieser Prozess wird i.d.R. als "Marshallen nach Wert" bezeichnet.

Wenn ein Objekt von MarshalByRefObject abgeleitet ist, wird anstelle des Objekts ein Objektverweis von einer Anwendungsdomäne an eine andere übergeben. Ein von MarshalByRefObject abgeleitetes Objekt kann auch mit dem Serializable-Attribut als serialisierbar gekennzeichnet werden. Wenn dieses Objekt im Remoteverfahren verwendet wird, übernimmt der für die Serialisierung verantwortliche Formatierer, der mit SurrogateSelector vorkonfiguriert wurde, die Kontrolle über den Serialisierungsprozess und ersetzt alle von MarshalByRefObject abgeleiteten Objekte durch einen Proxy. Wenn der Formatierer nicht mit SurrogateSelector vorkonfiguriert ist, befolgt die Serialisierungsarchitektur die Standardserialisierungsregeln (siehe Schritte im Serialisierungsprozess weiter unten).

Standardserialisierung

Der einfachste Weg, eine Klasse serialisierbar zu machen, besteht darin, sie wie folgt mit dem Serializable-Attribut zu kennzeichnen:

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

Der unten angezeigte Codeauszug veranschaulicht, wie eine Instanz dieser Klasse in eine Datei serialisiert werden kann:

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

In diesem Beispiel wird zur Serialisierung ein Binärformatierer verwendet. Sie müssen lediglich eine Instanz des zu verwendenden Stroms und Formatierers erstellen und dann die Serialize-Methode für den Formatierer aufrufen. Der Strom und die Objektinstanz, die serialisiert werden sollen, werden als Parameter für diesen Aufruf bereitgestellt. Obwohl dies nicht explizit in diesem Beispiel veranschaulicht wird, werden alle Elementvariablen einer Klasse serialisiert – sogar die als privat gekennzeichneten Variablen. In dieser Hinsicht unterscheidet sich die binäre Serialisierung vom XML-Serialisierungsprogramm, das nur öffentliche Felder serialisiert.

Es ist genau so einfach, das Objekt wieder in den vorherigen Zustand zu versetzen. Erstellen Sie zunächst einen Formatierer und einen Strom zum Lesen, und fordern Sie den Formatierer dann auf, das Objekt zu deserialisieren. Der unten angezeigte Codeauszug zeigt, wie dies durchgeführt wird.

IFormatter formatter = new BinaryFormatter(); 
Stream stream = new FileStream("MyFile.bin", FileMode.Open,  
FileAccess.Read, FileShare.Read); 
MyObject obj = (MyObject) formatter.Deserialize(fromStream); 
stream.Close(); 
// Hier ist der Beweis 
Console.WriteLine("n1: {0}", obj.n1); 
Console.WriteLine("n2: {0}", obj.n2); 
Console.WriteLine("str: {0}", obj.str);

Der oben verwendete BinaryFormatter (Binärformatierer) ist sehr effizient und erstellt einen sehr kompakten Bytestrom. Alle mit diesem Formatierer serialisierten Objekte können mit demselben Formatierer auch deserialisiert werden, was ihn zu einem idealen Tool zum Serialisieren von Objekten macht, die auf der .NET-Plattform deserialisiert werden. Es ist wichtig zu wissen, dass keine Konstruktoren aufgerufen werden, wenn ein Objekt deserialisiert wurde. Diese Einschränkung bei der Deserialisierung besteht aus Leistungsgründen. Dies verstößt jedoch gegen einige der üblichen Vereinbarungen, die zwischen der Laufzeit und dem Objektersteller bestehen, und die Entwickler sollten sicherstellen, dass sie sich über die Auswirkungen im Klaren sind, wenn sie ein Objekt als serialisierbar kennzeichnen.

Wenn Portabilität erforderlich ist, verwenden Sie stattdessen SoapFormatter. Ersetzen Sie im oben dargestellten Code einfach den Formatierer durch SoapFormatter, und rufen Sie wie zuvor Serialize und Deserialize auf. Dieser Formatierer erstellt die folgende Ausgabe für das oben verwendete Beispiel.

<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">Zeichenfolge</str> 
    </a1:MyObject> 
  </SOAP-ENV:Body> 
</SOAP-ENV:Envelope>

Es sollte unbedingt beachtet werden, dass das Serializable-Attribut nicht vererbt werden kann. Wenn wir von MyObject eine neue Klasse ableiten, muss die neue Klasse auch mit dem Attribut gekennzeichnet sein, da sie andernfalls nicht serialisiert werden kann. Wenn Sie z.B. versuchen, eine Instanz der unten angezeigten Klasse zu serialisieren, erhalten Sie eine SerializationException-Ausnahme, die Sie darüber informiert, dass der MyStuff-Typ nicht als serialisierbar gekennzeichnet ist.

public class MyStuff : MyObject  
{ 
  public int n3; 
}

Das Verwenden des Serialisierungsattributs ist zwar sehr bequem, bringt aber auch Einschränkungen mit sich, wie oben veranschaulicht. Lesen Sie die Richtlinien (siehe Serialisierungsrichtlinien unten) bezüglich der Kennzeichnung einer Klasse für die Serialisierung, da die Serialisierung einer Klasse nicht nach der Kompilierung hinzugefügt werden kann.

Selektive Serialisierung

Eine Klasse enthält oft ein Feld, das nicht serialisiert werden sollte. Nehmen wir z.B. an, eine Klasse speichert eine Thread-ID in einer Elementvariablen. Wenn die Klasse deserialisiert wird, dann kann der Thread, der bei der Serialisierung der Klasse für die ID gespeichert wurde, ggf. nicht mehr ausgeführt werden, so dass die Serialisierung dieses Wertes keinen Sinn macht. Sie können vermeiden, dass Elementvariablen serialisiert werden, indem Sie sie wie folgt mit dem NonSerialized-Attribut kennzeichnen:

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

Benutzerdefinierte Serialisierung

Sie können den Serialisierungsprozess anpassen, indem Sie die ISerializable-Schnittstelle eines Objekts anpassen. Dies ist besonders nützlich, falls der Wert einer Elementvariablen nach der Deserialisierung ungültig ist, die Variable jedoch mit einem Wert bereitgestellt werden muss, um den vollständigen Status des Objekts wiederherzustellen. Die Implementierung von ISerializable umfasst das Implementieren der GetObjectData-Methode und eines spezielles Konstruktors, der verwendet wird, wenn das Objekt deserialisiert wird. Der unten dargestellte Beispielcode veranschaulicht, wie ISerializable der MyObject-Klasse implementiert wird, die bereits in einem vorherigen Abschnitt verwendet wurde.

[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); 
  } 
}

Wenn GetObjectData während der Serialisierung aufgerufen wird, sind Sie für das Auffüllen des SerializationInfo-Objekts verantwortlich, das beim Methodenaufruf bereitgestellt wird. Fügen Sie einfach die zu serialisierenden Variablen als Name/Wert-Paare hinzu. Als Name kann ein beliebiger Text verwendet werden. Sie können frei entscheiden, welche Elementvariablen zu SerializationInfo hinzugefügt werden, sofern eine ausreichende Datenmenge serialisiert wurde, um das Objekt während der Deserialisierung wiederherzustellen. Abgeleitete Klassen sollten die GetObjectData-Methode des Basisobjekts aufrufen, wenn Letzteres ISerializable implementiert.

An dieser Stelle möchte ich nochmal besonders darauf hinweisen, dass Sie sowohl GetObjectData als auch den speziellen Konstruktor implementieren müssen, wenn ISerializable einer Klasse hinzugefügt wird. Der Compiler gibt eine Warnmeldung aus, wenn GetObjectData fehlt, aber da es nicht möglich ist, die Implementierung eines Konstruktors zu erzwingen, werden keine Warnmeldungen ausgegeben, wenn kein Konstruktor vorhanden ist. Ein Ausnahmefehler tritt auf, wenn versucht wird, eine Klasse ohne Konstruktor zu deserialisieren. Der aktuelle Entwurf wurde gegenüber der SetObjectData-Methode favorisiert, um potenzielle Sicherheits- und Versionsprobleme zu vermeiden. So muss eine SetObjectData-Methode z.B. als öffentliche Methode deklariert sein, wenn sie als Teil einer Schnittstelle definiert ist. Folglich müssen die Benutzer Code schreiben, um zu verhindern, dass die SetObjectData-Methode mehrmals aufgerufen wird. Sie können sich vorstellen, welche Probleme möglicherweise durch eine böswillige Anwendung verursacht werden können, die die SetObjectData-Methode eines Objekts aufruft, das gerade eine bestimmte Operation ausführt.

Während der Deserialisierung wurde SerializationInfo mit Hilfe des Konstruktors, der für diesen Zweck bereitgestellt wird, an die Klasse übergeben. Alle Sichtbarkeitseinschränkungen, die für den Konstruktor gelten, werden ignoriert, wenn das Objekt deserialisiert wird, so dass Sie die Klasse als öffentlich, geschützt, intern oder privat kennzeichnen können. Es ist empfehlenswert, den Konstruktor als geschützt zu kennzeichnen, sofern die Klasse nicht versiegelt ist. In diesem Fall sollte der Konstruktor als privat gekennzeichnet werden. Sie können den Status des Objekts wiederherstellen, indem Sie einfach die Werte der Variablen unter Verwendung der während der Serialisierung verwendeten Namen von SerializationInfo abrufen. Wenn die Basisklasse ISerializable implementiert, sollte der Basiskonstruktor aufgerufen werden, um es dem Basisobjekt zu ermöglichen, seine Variablen wiederherzustellen.

Wenn Sie eine neue Klasse von einer Klasse ableiten, die ISerializable implementiert, muss die abgeleitete Klasse sowohl den Konstruktor als auch die GetObjectData-Methode implementieren, wenn diese über Variablen verfügt, die serialisiert werden müssen. Der unten angezeigte Codeauszug veranschaulicht, wie dies mit Hilfe der MyObject-Klasse, die bereits zuvor verwendet wurde, durchgeführt wird.

[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); 
  } 
}

Vergessen Sie nicht, die Basisklasse im Deserialisierungskonstruktor aufzurufen. Wenn dies nicht erfolgt, wird der Konstruktor der Basisklasse niemals aufgerufen, und das Objekt wird nach der Deserialisierung nicht vollständig erstellt.

Objekte werden von innen nach außen erstellt, und das Aufrufen von Methoden während der Deserialisierung kann unerwünschte Nebeneffekte haben, da die aufgerufenen Methoden ggf. auf Objektverweise verweisen, die zum Zeitpunkt des Methodenaufrufs noch nicht deserialisiert waren. Wenn die deserialisierte Klasse IDeserializationCallback implementiert, wird automatisch die OnSerialization-Methode aufgerufen, falls die gesamte Objektgrafik deserialisiert wurde. Zu diesem Zeitpunkt wurden alle untergeordneten referenzierten Objekte vollständig wiederhergestellt. Eine Hashtabelle ist ein typisches Beispiel für eine Klasse, deren Deserialisierung ohne Verwendung des oben beschriebenen Ereignisüberwachungsvorgangs nicht einfach durchzuführen ist. Es ist einfach, während der Deserialisierung Schlüssel/Wert-Paare abzurufen, aber das erneute Hinzufügen dieser Objekte zur Hashtabelle kann Probleme verursachen, da nicht garantiert werden kann, dass von der Hashtabelle abgeleitete Klassen deserialisiert wurden. Daher ist es zu diesem Zeitpunkt nicht empfehlenswert, Methoden für eine Hashtabelle aufzurufen.

Schritte im Serialisierungsprozess

Wenn die Serialize-Methode von einem Formatierer aufgerufen wird, findet die Objektserialisierung unter Anwendung der folgenden Regeln statt:

  • Es wird geprüft, ob der Formatierer über eine Ersatzauswahl verfügt. Trifft dies zu, sollten Sie prüfen, ob die Ersatzauswahl Objekte eines bestimmten Typs behandelt. Wenn die Auswahl den Objekttyp behandelt, wird ISerializable.GetObjectData für die Ersatzauswahl aufgerufen.

  • Wenn keine Ersatzauswahl vorhanden ist oder wenn die Auswahl den Typ nicht behandelt, wird geprüft, ob das Objekt mit dem Serializable-Attribut gekennzeichnet ist. Trifft dies nicht zu, wird eine SerializationException-Ausnahme ausgegeben.

  • Falls das Objekt korrekt gekennzeichnet ist, prüfen Sie, ob das Objekt ISerializable implementiert. Trifft dies zu, wird GetObjectData für das Objekt aufgerufen.

  • Wenn das Objekt ISerializable nicht implementiert, wird die Richtlinie zur Standardserialisierung verwendet, wobei alle Felder, die nicht mit dem NonSerialized-Attribut gekennzeichnet sind, serialisiert werden.

Versionsprüfung

Das .NET Framework unterstützt die Versionsprüfung und parallele Ausführung, und alle Klassen funktionieren versionsübergreifend, falls die Schnittstellen der Klassen unverändert bleiben. Da die Serialisierung mit Elementvariablen und nicht mit Schnittstellen arbeitet, sollten Sie vorsichtig sein, wenn Sie Elementvariablen zu Klassen, die versionsübergreifend serialisiert werden, hinzufügen oder daraus entfernen. Dies trifft besonders auf Klassen zu, die ISerializable nicht implementieren. Jede Änderung am Status der aktuellen Version, z.B. Hinzufügen von Elementvariablen bzw. Ändern der Variablentypen oder -namen, hat zur Folge, dass vorhandene Objekte desselben Typs nicht erfolgreich deserialisiert werden können, wenn sie mit einer vorherigen Version serialisiert wurden.

Wenn der Status eines Objekts zwischen den Versionen geändert werden muss, haben Sie als Skriptautor zwei Möglichkeiten:

  • Sie können ISerializable implementieren. Dies ermöglicht es Ihnen, die Serialisierung und Deserialisierung präzise zu steuern, so dass der zukünftige Status während der Serialisierung korrekt hinzugefügt und interpretiert werden kann.

  • Kennzeichnen Sie weniger wichtige Elementvariablen mit dem NonSerialized-Attribut. Entscheiden Sie sich für diese Möglichkeit, wenn Sie davon ausgehen, dass zwischen den verschiedenen Versionen nur geringfügige Änderungen zu erwarten sind. Wenn z.B. einer späteren Version einer Klasse eine neue Variable hinzugefügt wurde, kann die Variable mit dem NonSerialized-Attribut gekennzeichnet werden, um sicherzustellen, dass die Klasse mit vorherigen Versionen kompatibel bleibt.

Serialisierungsrichtlinien

Sie sollten die Serialisierung beim Entwerfen neuer Klassen berücksichtigen, da eine Klasse nicht serialisierbar gemacht werden kann, nachdem sie kompiliert wurde. Folgende Fragen können gestellt werden: Ist es erforderlich, die Klasse anwendungsdomänenübergreifend zu senden? Wird die Klasse jemals im Remoteverfahren verwendet? Zu welchem Zweck verwenden die Benutzer diese Klasse? Es ist möglich, dass Sie aus dieser Klasse eine neue Klasse ableiten, die serialisiert werden muss. Wenn Sie sich nicht sicher sind, kennzeichnen Sie die Klasse als serialisierbar. Es ist vermutlich besser, alle Klassen als serialisierbar zu kennzeichnen, sofern nicht Folgendes zutrifft:

  • Die Klassen werden zu keinem Zeitpunkt anwendungsdomänenübergreifend verwendet. Wenn keine Serialisierung erforderlich ist und die Klasse anwendungsübergreifend verwendet wird, leiten Sie die Klasse von MarshalByRefObject ab.

  • Die Klasse speichert spezielle Zeiger, die nur auf die aktuelle Instanz der Klasse angewendet werden können. Wenn eine Klasse z.B. nichtverwaltete Speicher oder Dateihandles enthält, stellen Sie sicher, dass diese Felder mit dem NonSerialized-Attribut gekennzeichnet sind, oder serialisieren Sie die Klasse gar nicht erst.

  • Einige der Datenelemente enthalten vertrauliche Informationen. In diesem Fall ist es vermutlich ratsam, ISerializable zu implementieren und nur die erforderlichen Felder zu serialisieren.


Anzeigen: