Wird das Ziel einer klaren Komponentisierung dadurch zunichte gemacht, dass zu viele Typinformationen zwischen Bibliotheken ausgetauscht werden? Möglicherweise benötigen Sie einen effizienten, stark typisierten Datenspeicher, aber es wäre sehr teuer, wenn Sie Ihr Datenbankschema jedes Mal aktualisieren müssten, wenn sich das Objektmodell weiterentwickelt
MöchtenSie
lieber das Typschema zur Laufzeit ableiten? Möchten Sie, dass der Compiler der Bibliothek Ihnen programmgesteuert mitteilt, um welche Typen es sich handelt?
Um stark typisierte Datenstrukturen beizubehalten und gleichzeitig die Laufzeitflexibilität zu maximieren, möchten Sie wahrscheinlich Reflexion in Betracht ziehen und wie diese Ihre Software verbessern kann. In dieser Kolumne erkunde ich den System.Reflection-Namespace im Microsoft .NET Framework und wie er Ihre Entwicklungserfahrung verbessern kann. Ich beginne mit einigen einfachen Beispielen und schließe mit dem Umgang mit realen Serialisierungssituationen ab. Unterwegs zeige ich, wie Reflection und CodeDom zusammenarbeiten, um Laufzeitdaten effizient zu verarbeiten.
Bevor ich mich mit System.Reflection befasse, möchte ich die reflexive Programmierung im Allgemeinen besprechen. Erstens kann Reflexion als jede von einem Programmiersystem bereitgestellte Funktionalität definiert werden, die es Programmierern ermöglicht, Codeeinheiten ohne vorherige Kenntnis ihrer Identität oder formalen Struktur zu überprüfen und zu manipulieren. In diesem Abschnitt gibt es viel zu besprechen, daher werde ich einen nach dem anderen darauf eingehen.
Erstens: Was bietet Reflexion? Was kann man damit machen? Ich neige dazu, typische reflexionszentrierte Aufgaben in zwei Kategorien zu unterteilen: Inspektion und Manipulation. Bei der Inspektion müssen Objekte und Typen analysiert werden, um strukturierte Informationen über deren Definition und Verhalten zu sammeln. Abgesehen von einigen Grundbestimmungen erfolgt dies häufig ohne Vorkenntnisse. (In .NET Framework erbt beispielsweise alles von System.Object, und ein Verweis auf einen Objekttyp ist oft der allgemeine Ausgangspunkt für die Reflexion.)
Vorgänge rufen Code dynamisch auf, indem sie Informationen verwenden, die durch Inspektion, Erstellen neuer Instanzen oder sogar gesammelt wurden Typen und Objekte können einfach dynamisch umstrukturiert werden. Ein wichtiger Punkt ist, dass die Manipulation von Typen und Objekten zur Laufzeit bei den meisten Systemen zu Leistungseinbußen führt, verglichen mit der statischen Ausführung der entsprechenden Vorgänge im Quellcode. Dies ist aufgrund der dynamischen Natur der Reflexion ein notwendiger Kompromiss, es gibt jedoch viele Tipps und bewährte Methoden zur Optimierung der Leistung der Reflexion (ausführlichere Informationen zur Optimierung finden Sie unter msdn.microsoft.com/msdnmag/issues/05). die Verwendung von Reflexion /07/Reflexion).
Was ist also das Ziel der Reflexion? Was prüft und manipuliert der Programmierer tatsächlich? In meiner Definition von Reflexion habe ich den neuen Begriff „Code-Entität“ verwendet, um die Tatsache hervorzuheben, dass Reflexionstechniken aus Sicht des Programmierers manchmal die Grenzen zwischen ihnen verwischen traditionelle Objekte und Typen. Eine typische reflexionszentrierte Aufgabe könnte beispielsweise wie folgt aussehen:
Beginnen Sie mit einem Handle für Objekt O und verwenden Sie Reflektion, um ein Handle für die zugehörige Definition (Typ T) zu erhalten.
Untersuchen Sie Typ T und erhalten Sie ein Handle für seine Methode M.
Rufen Sie die Methode M eines anderen Objekts O' (ebenfalls vom Typ T) auf.
Beachten Sie, dass ich von einer Instanz zu ihrem zugrunde liegenden Typ pendele, von diesem Typ zu einer Methode und dann das Handle der Methode verwende, um sie auf einer anderen Instanz aufzurufen – offensichtlich ist dies die Verwendung traditioneller C#-Programmierung im Quellcode. Technologie kann dies nicht erreichen. Nachdem ich weiter unten die System.Reflection des .NET Frameworks besprochen habe, erläutere ich diese Situation noch einmal anhand eines konkreten Beispiels.
Einige Programmiersprachen bieten Reflexion nativ über die Syntax, während andere Plattformen und Frameworks (z. B. .NET Framework) sie als Systembibliothek bereitstellen. Unabhängig davon, wie Reflexion bereitgestellt wird, sind die Möglichkeiten, Reflexionstechnologie in einer bestimmten Situation einzusetzen, recht komplex. Die Fähigkeit eines Programmiersystems zur Reflexion hängt von vielen Faktoren ab: Nutzt der Programmierer die Funktionen der Programmiersprache gut, um seine Konzepte auszudrücken? Bettet der Compiler genügend strukturierte Informationen (Metadaten) in die Ausgabe ein, um zukünftige Analysen zu erleichtern? Interpretation? Gibt es ein Laufzeitsubsystem oder einen Host-Interpreter, der diese Metadaten verarbeitet? Stellt die Plattformbibliothek die Ergebnisse dieser Interpretation auf eine Weise dar, die für Programmierer nützlich ist
? als einfache Funktion im C-Stil im Code erscheint und keine formale Datenstruktur vorhanden ist, ist es für Ihr Programm offensichtlich unmöglich, dynamisch zu schließen, dass der Zeiger einer bestimmten Variablen v1 auf eine Objektinstanz eines bestimmten Typs T zeigt . Denn schließlich ist Typ T ein Konzept in Ihrem Kopf; er taucht nie explizit in Ihren Programmieranweisungen auf. Wenn Sie jedoch eine flexiblere objektorientierte Sprache (z. B. C#) verwenden, um die abstrakte Struktur des Programms auszudrücken, und das Konzept des Typs T direkt einführen, wandelt der Compiler Ihre Idee in etwas um, das später durchgeleitet werden kann geeignete Logik zum Verständnis der Form, wie sie von der Common Language Runtime (CLR) oder einem dynamischen Sprachinterpreter bereitgestellt wird.
Handelt es sich bei der Reflexion ausschließlich um eine dynamische Laufzeittechnologie? Einfach ausgedrückt: Ist sie es nicht? Im Verlauf des Entwicklungs- und Ausführungszyklus gibt es viele Momente, in denen Reflexion verfügbar und für Entwickler nützlich ist. Einige Programmiersprachen werden durch eigenständige Compiler implementiert, die High-Level-Code direkt in Anweisungen umwandeln, die die Maschine verstehen kann. Die Ausgabedatei enthält nur kompilierte Eingaben und die Laufzeit verfügt über keine unterstützende Logik zum Akzeptieren undurchsichtiger Objekte und zum dynamischen Analysieren ihrer Definitionen. Genau das ist bei vielen herkömmlichen C-Compilern der Fall. Da die ausführbare Zieldatei nur wenig unterstützende Logik enthält, können Sie nicht viel dynamische Reflexion durchführen, aber Compiler stellen von Zeit zu Zeit statische Reflexion bereit – beispielsweise ermöglicht der allgegenwärtige Operator „typeof“ Programmierern, Typbezeichner zur Kompilierungszeit zu überprüfen.
Eine völlig andere Situation besteht darin, dass interpretierte Programmiersprachen immer über den Hauptprozess ausgeführt werden (Skriptsprachen fallen normalerweise in diese Kategorie). Da die vollständige Definition des Programms (als Eingabequellcode) in Kombination mit der vollständigen Sprachimplementierung (als Interpreter selbst) verfügbar ist, sind alle zur Unterstützung der Selbstanalyse erforderlichen Techniken vorhanden. Diese dynamische Sprache bietet häufig umfassende Reflexionsfähigkeiten sowie einen umfangreichen Satz an Werkzeugen für die dynamische Analyse und Manipulation von Programmen.
Die .NET Framework CLR und ihre Hostsprachen wie C# liegen in der Mitte. Der Compiler wird zum Konvertieren des Quellcodes in IL und Metadaten verwendet. Letzterer ist auf einer niedrigeren Ebene oder weniger „logisch“ als der Quellcode, behält aber dennoch viele abstrakte Struktur- und Typinformationen bei. Sobald die CLR dieses Programm startet und hostet, kann die System.Reflection-Bibliothek der Basisklassenbibliothek (BCL) diese Informationen verwenden und Informationen über den Objekttyp, Typmitglieder, Mitgliedssignaturen usw. zurückgeben. Darüber hinaus können auch Anrufe unterstützt werden, einschließlich Anrufe mit später Bindung.
Reflexion in .NET
Um die Vorteile der Reflexion beim Programmieren mit dem .NET Framework zu nutzen, können Sie den System.Reflection-Namespace verwenden. Dieser Namespace stellt Klassen bereit, die viele Laufzeitkonzepte kapseln, z. B. Assemblys, Module, Typen, Methoden, Konstruktoren, Felder und Eigenschaften. Die Tabelle in Abbildung 1 zeigt, wie die Klassen in System.Reflection ihren konzeptionellen Laufzeitgegenstücken zugeordnet werden.
Obwohl wichtig, werden System.Reflection.Assembly und System.Reflection.Module hauptsächlich zum Suchen und Laden von neuem Code in die Laufzeit verwendet. In dieser Kolumne gehe ich nicht auf diese Teile ein und gehe davon aus, dass der gesamte relevante Code bereits geladen wurde.
Das typische Muster zum Überprüfen und Bearbeiten geladenen Codes ist hauptsächlich System.Type. Normalerweise erhalten Sie zunächst eine System.Type-Instanz der gewünschten Laufzeitklasse (über Object.GetType). Anschließend können Sie verschiedene Methoden von System.Type verwenden, um die Typdefinition in System.Reflection zu untersuchen und Instanzen anderer Klassen abzurufen. Wenn Sie beispielsweise an einer bestimmten Methode interessiert sind und eine System.Reflection.MethodInfo-Instanz dieser Methode erhalten möchten (vielleicht über Type.GetMethod). Ebenso, wenn Sie an einem Feld interessiert sind und eine System.Reflection.FieldInfo-Instanz dieses Felds erhalten möchten (vielleicht über Type.GetField).
Sobald Sie über alle erforderlichen Reflexionsinstanzobjekte verfügen, können Sie fortfahren, indem Sie die Schritte zur Überprüfung oder Bearbeitung nach Bedarf ausführen. Bei der Überprüfung verwenden Sie verschiedene beschreibende Eigenschaften in der reflektierenden Klasse, um die benötigten Informationen zu erhalten (Ist dies ein generischer Typ? Ist dies eine Instanzmethode?). Während des Betriebs können Sie Methoden dynamisch aufrufen und ausführen, neue Objekte durch Aufruf von Konstruktoren erstellen usw.
Überprüfen von Typen und Mitgliedern
Lassen Sie uns in den Code eintauchen und untersuchen, wie eine Überprüfung mithilfe der einfachen Reflexion durchgeführt wird. Ich werde mich auf die Typanalyse konzentrieren. Ich beginne mit einem Objekt, rufe seinen Typ ab und untersuche dann einige interessante Mitglieder (siehe Abbildung 2).
Als Erstes ist zu beachten, dass es in der Klassendefinition auf den ersten Blick so aussieht, als gäbe es viel mehr Platz für die Beschreibung der Methoden, als ich erwartet hatte. Woher kommen diese zusätzlichen Methoden? Jeder, der sich mit der .NET Framework-Objekthierarchie auskennt, wird diese Methoden erkennen, die von der gemeinsamen Basisklasse Object selbst geerbt wurden. (Tatsächlich habe ich zuerst Object.GetType verwendet, um seinen Typ abzurufen.) Außerdem können Sie die Getter-Funktion für die Eigenschaft sehen.
Was ist, wenn Sie nurdie
explizit definierten Funktionen von MyClass selbst benötigen?
Ich habe festgestellt, dass jeder bereit ist, die zweite überladene Methode von GetMethods zu verwenden, die den Parameter BindingFlags akzeptiert. Durch die Kombination verschiedener Werte aus der BindingFlags-Enumeration können Sie dafür sorgen, dass eine Funktion nur die gewünschte Teilmenge von Methoden zurückgibt. Ersetzen Sie den GetMethods-Aufruf durch:
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |BindingFlags.Public)
Als Ergebnis erhalten Sie die folgende Ausgabe (beachten Sie, dass es keine statischen Hilfsfunktionen und von System.Object geerbten Funktionen gibt).
Reflection-Demo-Beispiel 1
Typname: MyClass
Methodenname: MyMethod1
Methodenname: MyMethod2
Methodenname: get_MyProperty
Eigenschaftsname: MyProperty
Was passiert, wenn Sie den Typnamen (vollständig qualifiziert) und die Mitglieder vorher kennen? Konvertierung? Mit dem Code in den ersten beiden Beispielen verfügen Sie bereits über die grundlegenden Komponenten, um einen Browser für primitive Klassen zu implementieren. Sie können eine Laufzeitentität anhand ihres Namens suchen und dann ihre verschiedenen zugehörigen Eigenschaften aufzählen.
Code dynamisch aufrufen
Bisher habe ich Handles für Laufzeitobjekte (z. B. Typen und Methoden) nur zu beschreibenden Zwecken erhalten, z. B. zum Drucken ihrer Namen. Aber wie kann man noch mehr tun? Wie ruft man eine Methode tatsächlich auf?
Einige wichtige Punkte in diesem Beispiel sind: Zuerst wird eine System.Type-Instanz von einer Instanz von MyClass, mc1, abgerufen, und dann wird eine MethodInfo-Instanz abgerufen dieser Typ. Wenn MethodInfo schließlich aufgerufen wird, wird es an eine andere MyClass-Instanz (mc2) gebunden, indem es als erster Parameter des Aufrufs übergeben wird.
Wie bereits erwähnt, verwischt dieses Beispiel die Unterscheidung zwischen Typen und Objektverwendung, die Sie im Quellcode erwarten würden. Logischerweise rufen Sie ein Handle für eine Methode ab und rufen die Methode dann auf, als ob sie zu einem anderen Objekt gehörte. Für Programmierer, die mit funktionalen Programmiersprachen vertraut sind, mag dies ein Kinderspiel sein; für Programmierer, die nur mit C# vertraut sind, ist es möglicherweise nicht so intuitiv, Objektimplementierung und Objektinstanziierung zu trennen.
Alles zusammenfassen
Bisher habe ich die Grundprinzipien des Checkens und Callens besprochen, und jetzt werde ich sie anhand konkreter Beispiele zusammenstellen. Stellen Sie sich vor, Sie möchten eine Bibliothek mit statischen Hilfsfunktionen bereitstellen, die Objekte verarbeiten müssen. Aber zum Zeitpunkt des Entwurfs haben Sie keine Vorstellung von den Typen dieser Objekte. Es hängt von den Anweisungen des Funktionsaufrufers ab, wie er aus diesen Objekten aussagekräftige Informationen extrahieren möchte. Die Funktion akzeptiert eine Sammlung von Objekten und einen String-Deskriptor der Methode. Anschließend wird die Sammlung durchlaufen, die Methoden jedes Objekts aufgerufen und die Rückgabewerte mit einer Funktion aggregiert.
Für dieses Beispiel werde ich einige Einschränkungen deklarieren. Erstens akzeptiert die durch den String-Parameter beschriebene Methode (die vom zugrunde liegenden Typ jedes Objekts implementiert werden muss) keine Parameter und gibt eine Ganzzahl zurück. Der Code durchläuft die Sammlung von Objekten, ruft die angegebene Methode auf und berechnet nach und nach den Durchschnitt aller Werte. Da es sich schließlich nicht um Produktionscode handelt, muss ich mir beim Summieren keine Gedanken über die Parametervalidierung oder den Ganzzahlüberlauf machen.
Beim Durchsuchen des Beispielcodes können Sie erkennen, dass die Vereinbarung zwischen der Hauptfunktion und dem statischen Hilfsprogramm ComputeAverage nicht auf anderen Typinformationen als der gemeinsamen Basisklasse des Objekts selbst beruht. Mit anderen Worten: Sie können den Typ und die Struktur des zu übertragenden Objekts vollständig ändern. Solange Sie jedoch immer eine Zeichenfolge verwenden können, um eine Methode zu beschreiben, die eine Ganzzahl zurückgibt, funktioniert ComputeAverage
einwandfrei ist versteckt in Das letzte Beispiel bezieht sich auf MethodInfo (allgemeine Reflexion). Beachten Sie, dass der Code in der foreach-Schleife von ComputeAverage nur eine MethodInfo vom ersten Objekt in der Sammlung abruft und diese dann an den Aufruf für alle nachfolgenden Objekte bindet. Wie die Codierung zeigt, funktioniert es einwandfrei – dies ist ein einfaches Beispiel für MethodInfo-Caching. Aber hier gibt es eine grundlegende Einschränkung. Eine MethodInfo-Instanz kann nur von einer Instanz desselben hierarchischen Typs wie das abgerufene Objekt aufgerufen werden. Dies ist möglich, weil Instanzen von IntReturner und SonOfIntReturner (geerbt von IntReturner) übergeben werden.
Im Beispielcode wurde eine Klasse mit dem Namen EnemyOfIntReturner eingefügt, die das gleiche Grundprotokoll wie die beiden anderen Klassen implementiert, jedoch keine gemeinsamen gemeinsamen Typen verwendet. Mit anderen Worten: Die Schnittstellen sind logisch äquivalent, es gibt jedoch keine Überschneidung auf Typebene. Um die Verwendung von MethodInfo in dieser Situation zu erkunden, versuchen Sie, der Sammlung ein weiteres Objekt hinzuzufügen, eine Instanz über „new EnemyOfIntReturner(10)“ abzurufen und das Beispiel erneut auszuführen. Sie werden auf eine Ausnahme stoßen, die darauf hinweist, dass MethodInfo nicht zum Aufrufen des angegebenen Objekts verwendet werden kann, da es absolut nichts mit dem ursprünglichen Typ zu tun hat, von dem MethodInfo abgerufen wurde (auch wenn der Methodenname und das zugrunde liegende Protokoll gleichwertig sind). Um Ihren Code produktionsbereit zu machen, müssen Sie auf diese Situation vorbereitet sein.
Eine mögliche Lösung könnte darin bestehen, die Typen aller eingehenden Objekte selbst zu analysieren und dabei die Interpretation ihrer gemeinsamen Typhierarchie (sofern vorhanden) beizubehalten. Wenn sich der Typ des nächsten Objekts von einer bekannten Typhierarchie unterscheidet, muss eine neue MethodInfo abgerufen und gespeichert werden. Eine andere Lösung besteht darin, die TargetException abzufangen und erneut eine MethodInfo-Instanz abzurufen. Beide hier genannten Lösungen haben ihre Vor- und Nachteile. Joel Pobar hat für die Mai-Ausgabe 2007 dieses Magazins einen hervorragenden Artikel über die Pufferung und Reflexionsleistung von MethodInfo geschrieben, den ich wärmstens empfehlen kann.
Hoffentlich demonstriert dieses Beispiel das Hinzufügen von Reflektion zu einer Anwendung oder einem Framework, um mehr Flexibilität für zukünftige Anpassungen oder Erweiterbarkeit zu schaffen. Zugegebenermaßen kann die Verwendung von Reflektion im Vergleich zu gleichwertiger Logik in nativen Programmiersprachen etwas umständlich sein. Wenn Sie der Meinung sind, dass das Hinzufügen einer reflexionsbasierten späten Bindung zu Ihrem Code für Sie oder Ihre Kunden zu umständlich ist (schließlich müssen ihre Typen und ihr Code irgendwie in Ihrem Framework berücksichtigt werden), dann ist dies möglicherweise nur aus Gründen der Moderationsflexibilität erforderlich um ein gewisses Gleichgewicht zu erreichen.
Effiziente Typverarbeitung für die Serialisierung
Nachdem wir nun die Grundprinzipien der .NET-Reflexion anhand mehrerer Beispiele behandelt haben, werfen wir einen Blick auf eine reale Situation. Wenn Ihre Software über Webdienste oder andere Out-of-Process-Remoting-Technologien mit anderen Systemen interagiert, sind wahrscheinlich Serialisierungsprobleme aufgetreten. Bei der Serialisierung werden im Wesentlichen aktive, speicherbelegende Objekte in ein Datenformat umgewandelt, das für die Online-Übertragung oder Festplattenspeicherung geeignet ist.
Der System.Xml.Serialization-Namespace in .NET Framework stellt mit XmlSerializer eine leistungsstarke Serialisierungs-Engine bereit, die jedes verwaltete Objekt in XML konvertieren kann (XML-Daten können in Zukunft auch wieder in eine typisierte Objektinstanz konvertiert werden. Dieser Prozess wird Deserialisierung genannt). Die XmlSerializer-Klasse ist eine leistungsstarke, unternehmenstaugliche Software, die Ihre erste Wahl ist, wenn Sie in Ihrem Projekt auf Serialisierungsprobleme stoßen. Aber zu Bildungszwecken wollen wir untersuchen, wie die Serialisierung (oder andere ähnliche Instanzen zur Laufzeittypbehandlung) implementiert wird.
Bedenken Sie Folgendes: Sie liefern ein Framework, das Objektinstanzen beliebiger Benutzertypen übernimmt und sie in ein intelligentes Datenformat konvertiert. Angenommen, Sie haben ein speicherresidentes Objekt vom Typ Adresse wie unten gezeigt:
(Pseudocode)
Klassenadresse
{
AddressID-ID;
String Street, Stadt;
StateType State;
ZipCodeType ZipCode;
}
Wie kann eine geeignete Datendarstellung für die spätere Verwendung generiert werden? Vielleicht kann eine einfache Textdarstellung dieses Problem lösen:
Adresse: 123
Straße: 1 Microsoft Way
Stadt: Redmond
Bundesstaat: WA
Postleitzahl: 98052
Wenn die formalen Daten, die konvertiert werden müssen, vollständig verstanden sind im Voraus eingeben (z. B. wenn Sie den Code selbst schreiben), wird es ganz einfach:
foreach(Address a in AddressList)
{
Console.WriteLine(“Adresse:{0}”, a.ID);
Console.WriteLine(“tStreet:{0}”, a.Street);
... // und so weiter
}
Richtig interessant kann es jedoch werden, wenn man nicht im Voraus weiß, auf welche Datentypen man zur Laufzeit stößt. Wie schreibt man allgemeinen Framework-Code wie diesen?
MyFramework.TranslateObject (Objekteingabe, MyOutputWriter-Ausgabe)
Zunächst müssen Sie entscheiden, welche Typmitglieder für die Serialisierung nützlich sind. Zu den Möglichkeiten gehört die Erfassung nur von Mitgliedern eines bestimmten Typs, z. B. primitive Systemtypen, oder die Bereitstellung eines Mechanismus für Typautoren, um anzugeben, welche Mitglieder serialisiert werden müssen, z. B. die Verwendung benutzerdefinierter Eigenschaften als Markierungen für Typmitglieder. Sie können nur Mitglieder eines bestimmten Typs erfassen, beispielsweise primitive Systemtypen, oder der Typautor kann angeben, welche Mitglieder serialisiert werden müssen (möglicherweise durch die Verwendung benutzerdefinierter Eigenschaften als Markierungen für die Typmitglieder).
Sobald Sie die Datenstrukturelemente dokumentiert haben, die konvertiert werden müssen, müssen Sie die Logik schreiben, um sie aufzuzählen und aus den eingehenden Objekten abzurufen. Reflection übernimmt hier die Schwerstarbeit und ermöglicht Ihnen die Abfrage sowohl von Datenstrukturen als auch von Datenwerten.
Der Einfachheit halber entwerfen wir eine einfache Konvertierungs-Engine, die ein Objekt nimmt, alle seine öffentlichen Eigenschaftswerte abruft, sie durch direkten Aufruf von ToString in Zeichenfolgen konvertiert und die Werte dann serialisiert. Für ein bestimmtes Objekt namens „input“ sieht der Algorithmus ungefähr wie folgt aus:
Rufen Sie input.GetType auf, um eine System.Type-Instanz abzurufen, die die zugrunde liegende Struktur der Eingabe beschreibt.
Verwenden Sie Type.GetProperties und den entsprechenden BindingFlags-Parameter, um öffentliche Eigenschaften als PropertyInfo-Instanzen abzurufen.
Eigenschaften werden als Schlüssel-Wert-Paare mithilfe von PropertyInfo.Name und PropertyInfo.GetValue abgerufen.
Rufen Sie Object.ToString für jeden Wert auf, um ihn (im Grunde) in das String-Format zu konvertieren.
Packen Sie den Namen des Objekttyps und die Sammlung von Eigenschaftsnamen und Zeichenfolgewerten in das richtige Serialisierungsformat.
Dieser Algorithmus vereinfacht die Dinge erheblich und erfasst gleichzeitig den Sinn und Zweck, eine Laufzeitdatenstruktur in selbstbeschreibende Daten umzuwandeln. Aber es gibt ein Problem: Leistung. Wie bereits erwähnt, ist die Reflexion sowohl für die Typverarbeitung als auch für das Abrufen von Werten sehr kostspielig. In diesem Beispiel führe ich eine vollständige Typanalyse für jede Instanz des bereitgestellten Typs durch.
Was wäre, wenn es möglich wäre, Ihr Verständnis der Struktur eines Typs zu erfassen oder zu bewahren, sodass Sie sie später mühelos abrufen und neue Instanzen dieses Typs effizient verarbeiten könnten? Mit anderen Worten: Fahren Sie mit Schritt 3 im Beispielalgorithmus fort Neu ist, dass dies mithilfe von Funktionen im .NET Framework möglich ist. Sobald Sie die Datenstruktur eines Typs verstanden haben, können Sie CodeDom verwenden, um dynamisch Code zu generieren, der an diese Datenstruktur gebunden wird. Sie können eine Hilfsassembly generieren, die eine Hilfsklasse und Methoden enthält, die auf den eingehenden Typ verweisen und direkt auf seine Eigenschaften zugreifen (wie jede andere Eigenschaft in verwaltetem Code), sodass sich die Typprüfung nur einmal auf die Leistung auswirkt.
Jetzt werde ich diesen Algorithmus reparieren. Neuer Typ:
Rufen Sie die System.Type-Instanz ab, die diesem Typ entspricht.
Verwenden Sie die verschiedenen System.Type-Accessoren, um das Schema (oder zumindest die für die Serialisierung nützliche Teilmenge des Schemas) abzurufen, z. B. Eigenschaftsnamen, Feldnamen usw.
Verwenden Sie die Schemainformationen, um eine Hilfsassembly (über CodeDom) zu generieren, die mit dem neuen Typ verknüpft ist und Instanzen effizient verarbeitet.
Verwenden Sie Code in einer Hilfsassembly, um Instanzdaten zu extrahieren.
Serialisieren Sie Daten nach Bedarf.
Für alle eingehenden Daten eines bestimmten Typs können Sie mit Schritt 4 fortfahren und eine enorme Leistungsverbesserung gegenüber der expliziten Überprüfung jeder Instanz erzielen.
Ich habe eine grundlegende Serialisierungsbibliothek namens SimpleSerialization entwickelt, die diesen Algorithmus mithilfe von Reflection und CodeDom implementiert (herunterladbar in dieser Kolumne). Die Hauptkomponente ist eine Klasse namens SimpleSerializer, die vom Benutzer mit einer Instanz von System.Type erstellt wird. Im Konstruktor analysiert die neue SimpleSerializer-Instanz den angegebenen Typ und generiert mithilfe von Hilfsklassen eine temporäre Assembly. Die Hilfsklasse ist eng an den angegebenen Datentyp gebunden und behandelt die Instanz so, als ob Sie den Code mit vollständiger Vorkenntnis des Typs schreiben würden.
Die SimpleSerializer-Klasse hat das folgende Layout:
Klasse SimpleSerializer
{
öffentliche Klasse SimpleSerializer(Type dataType);
public void Serialize(object input, SimpleDataWriter-Writer);
}
Einfach erstaunlich! Der Konstruktor übernimmt die schwere Arbeit: Er analysiert die Typstruktur mithilfe von Reflektion und generiert dann mithilfe von CodeDom die Hilfsassembly. Die SimpleDataWriter-Klasse ist lediglich eine Datensenke, die zur Veranschaulichung gängiger Serialisierungsmuster verwendet wird.
Um eine einfache Address-Klasseninstanz zu serialisieren, verwenden Sie den folgenden Pseudocode, um die Aufgabe abzuschließen:
SimpleSerializer mySerializer=
new
SimpleSerializer
(typeof(Address
));
Wir empfehlen Ihnen, den Beispielcode selbst auszuprobieren, insbesondere die SimpleSerialization-Bibliothek. Ich habe Kommentare zu einigen interessanten Teilen von SimpleSerializer hinzugefügt und hoffe, dass das hilft. Wenn Sie im Produktionscode eine strikte Serialisierung benötigen, müssen Sie sich natürlich unbedingt auf die im .NET Framework bereitgestellten Technologien verlassen (z. B. XmlSerializer). Wenn Sie jedoch feststellen, dass Sie zur Laufzeit mit beliebigen Typen arbeiten und diese effizient verarbeiten müssen, hoffe ich, dass Sie meine SimpleSerialization-Bibliothek als Lösung übernehmen.