MSDN Magazin > Home > Ausgaben > 2007 > June >  Tiefe Einblicke in CLR: Reflexionen über R...
Tiefe Einblicke in CLR
Reflexionen über Reflektion
Mike Repass

Codedownload verfügbar unter: CLRInsideOut2007_06.exe (163 KB)
Browse the Code Online
Ihr Ziel einer sauberen Aufteilung in Komponenten wird von der Notwendigkeit durchkreuzt, zu viele Typeninformationen zwischen mehreren Bibliotheken zur gemeinsamen Nutzung freigeben zu müssen. Sie wollen eine effiziente, stark typisierte Datenspeicherung erzielen, aber es ist zu kostspielig, bei jeder Weiterentwicklung des Objektmodells das Datenbankschema zu aktualisieren, und Sie möchten daher das Typenschema zur Laufzeit ableiten. Sie müssen Komponenten liefern, die beliebige Benutzerobjekte akzeptieren und sie auf eine typenintelligente Art verarbeiten. Sie wünschen sich, dass Aufrufer Ihrer Bibliothek ihre Typen programmgesteuert beschreiben können.
Wenn Sie sich in der Situation befinden, stark typisierte Datenstrukturen beibehalten zu wollen und gleichzeitig die Laufzeitflexibilität zu maximieren, dann sollten Sie darüber nachdenken, wie Ihre Software durch Reflektion verbessert werden kann. In diesem Artikel wird der System.Reflection-Namespace in Microsoft® .NET Framework sowie seine Einsatzmöglichkeiten im Entwicklungsprozess untersucht. Dabei dienen einfache Beispiele zur Veranschaulichung. Anschließend wird das realistische Szenario der Serialisierung behandelt und aufgezeigt, wie Reflektion und CodeDom zusammenarbeiten können, um eine effiziente Verarbeitung von Laufzeitendaten zu erreichen.
Vor einer genaueren Betrachtung von System.Reflection soll zu Anfang die programmatische Reflektion im Allgemeinen erörtert werden. Reflektion kann zunächst als jede von einem Programmiersystem angebotene Funktionalität definiert werden, die es dem Programmierer ermöglicht, Codeentitäten zu prüfen und zu bearbeiten, ohne vorher ihre Identifikation oder ihre formale Struktur zu kennen. Das ist ein recht großer Brocken, und daher werde ich ihn Stück für Stück auseinandernehmen.
Was hat Reflektion zu bieten? Wozu kann sie verwendet werden? Ich ziehe es vor, typische reflektionsorientierte Aufgaben in zwei Kategorien einzuteilen: Prüfung und Bearbeitung. Eine Prüfung umfasst das Analysieren von Objekten und Typen, um strukturierte Informationen zu ihrer Definition und ihrem Verhalten zu sammeln. In der Regel erfolgt dieser Vorgang ohne bzw. mit sehr begrenztem Wissen über diese Objekte und Typen, abgesehen von einigen Grundlagen. (So erbt beispielsweise in .NET Framework alles von System.Object, daher ist ein typisierter Objektverweis in der Regel der Ausgangspunkt für Reflektion).
Bei der Bearbeitung werden die durch eine Prüfung gewonnenen Informationen dazu verwendet, Code dynamisch aufzurufen, neue Instanzen von entdeckten Typen zu erstellen oder sogar Typen und Objekte dynamisch neu zu strukturieren. Es sollte unbedingt darauf hingewiesen werden, dass bei den meisten Systemen das Bearbeiten von Typen und Objekten zur Laufzeit im Vergleich zu entsprechenden Vorgängen, die statisch am Quellcode vorgenommen werden, zu Leistungseinbußen führen. Dies ist aufgrund der dynamischen Natur der Reflektion ein notwendiger Kompromiss, aber es stehen viele Tricks und Best Practices zur Verfügung, mit denen die Reflektionsleistung optimiert werden kann (detailliertere Informationen zum Optimieren der Verwendung der Reflektion finden Sie unter msdn.microsoft.com/msdnmag/issues/05/07/Reflection).
Was ist das Ziel der Reflektion? Was prüft oder bearbeitet der Programmierer eigentlich? In meiner Definition von Reflektion habe ich den neuartigen Begriff „Codeentitäten“ verwendet, um die Tatsache hervorzuheben, dass das Reflektionsverfahren aus der Sicht eines Programmierers manchmal die konventionelle Unterscheidung zwischen Objekten und Typen verwischen kann. Eine typische reflektionsorientierte Aufgabe könnte beispielsweise folgendermaßen aussehen:
  1. Beginnen Sie mit einem Handle zu einem Objekt O, und verwenden Sie Reflektion, um ein Handle zu der zugehörigen Definition vom Typ T zu erwerben.
  2. Prüfen Sie Typ T, und erwerben Sie ein Handle zu seiner Methode M.
  3. Rufen Sie Methode M für ein anderes Objekt namens O’ auf, das ebenfalls zum Typ T gehört.
Sie gelangen also von einer Instanz zu dem ihr zugrunde liegenden Typ und von diesem Typ zu einer Methode. Danach verwenden Sie ein Handle zu dieser Methode, um sie für eine andere Instanz aufzurufen. Bei konventionellen Verfahren der C#-Programmierung im Quellcode ist diese Vorgehensweise nicht möglich. Dieses Szenario wird später anhand eines konkreten Beispiels näher erläutert. Zunächst soll aber System.Reflection in .NET Framework untersucht werden.
Einige Programmiersprachen bieten Reflektion intern über ihre Syntax an, während Reflektion von anderen Plattformen und Frameworks (z. B. von .NET Framework) als Systembibliothek verfügbar gemacht wird. Unabhängig davon, wie Reflektion verfügbar gemacht wird, sind die Möglichkeiten zur Verwendung des Reflektionsverfahrens in einem bestimmten Szenario ziemlich kompliziert. Die Fähigkeit eines Programmiersystems, Reflektion bereitzustellen, hängt von vielen Faktoren ab: Hat der Programmierer die Konzepte mithilfe der Funktionen der Programmiersprache gut ausgedrückt? Bettet der Compiler genug strukturelle Informationen (Metadaten) in der Ausgabe ein, um eine spätere Interpretation zu ermöglichen? Steht ein Laufzeitsubsystem oder Hostinterpreter zur Verfügung, um diese Metadaten zu nutzen? Macht eine Plattformbibliothek die Ergebnisse dieser Interpretation in einer für den Programmierer nützlichen Weise verfügbar?
Wenn Sie theoretisch mit einem komplexen, objektorientierten Typensystem arbeiten, aber den Code in einfachen Funktionen des C-Stils ohne formale Datenstrukturen ausdrücken, kann das Programm unmöglich dynamisch ableiten, dass der Zeiger in einer Variable v1 auf eine Objektinstanz eines Typs T verweist, denn Typ T liegt ja schließlich nur als theoretisches Konzept vor, das nie explizit in den Programmieranweisungen formuliert wurde. Wenn Sie dagegen 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 Ideen in eine Form um, die später von einer möglicherweise vom CLR-Interpreter (Common Language Runtime, gemeinsame Sprachlaufzeit) oder einem Interpreter einer dynamischen Sprache bereitgestellten geeigneten Logik verstanden werden könnte.
Ist Reflektion ausschließlich eine dynamische Laufzeittechnologie? Einfach gesagt: Nein, das ist sie nicht. Es gibt viele Stadien im Entwicklungs- und Ausführungszyklus, in denen Reflektion für den Entwickler verfügbar und nützlich sein könnte. Einige Programmiersprachen werden von eigenständigen Compilern implementiert, die höheren Code in Anweisungen umwandeln, die vom Computer direkt verstanden werden können. Die Ausgabedatei besteht allein aus der übersetzten Eingabe, und zur Laufzeit steht keine Unterstützungslogik zur Verfügung, um die Definition eines undurchsichtigen Objekts dynamisch zu analysieren. Dies ist genau das Szenario vieler herkömmlicher C-Compiler. Da in der ausführbaren Zieldatei nur wenig Unterstützungslogik verfügbar ist, kann nicht viel dynamische Reflektion durchgeführt werden, jedoch bietet der Compiler oft statische Reflektion an. So ermöglicht beispielsweise der gebräuchliche Operator „typeof“ dem Programmierer, beim Kompilieren die Typenidentität zu überprüfen.
Am anderen Ende des Spektrums stehen interpretierte Programmiersprachen, die immer von einem Hostprozess ausgeführt werden (zu dieser Kategorie gehören Skriptsprachen). Da die volle Definition des Programms (als Eingabequellcode) zur Verfügung steht und mit der vollständigen Sprachimplementierung (dem Interpreter selbst) verknüpft ist, steht die gesamte erforderliche Technologie zur Unterstützung einer Selbstanalyse bereit. Derartige dynamische Sprachen bieten häufig eine umfassende Reflektionsfunktionalität, die Ihnen einen vielfältigen Satz an Tools zur dynamischen Analyse und Bearbeitung des Programms zur Verfügung stellt.
Die .NET Framework-CLR und ihre gehosteten Sprachen wie beispielsweise C# befinden sich etwa in der Mitte des Spektrums. Ein Compiler wird verwendet, um Quellcode in IL (Intermediate Language) und Metadaten umzuwandeln, die noch immer einen großen Teil der abstrakten strukturellen Informationen und Typeninformationen enthalten, obwohl sie auf einer niedrigeren Ebene liegen und vielleicht weniger „logisch“ als der ursprüngliche Quellcode sind. Wenn das Programm gestartet und von der CLR gehostet wird, kann die System.Reflection-Bibliothek der Basisklassenbibliothek (Base Class Library, BCL) diese Informationen nutzen und Informationen zum Typ eines Objekts, zu Mitgliedern eines Typs, zur Signatur eines Mitglieds und so weiter zurückgeben. Darüber hinaus kann sie auch Aufrufe einschließlich solcher mit später Bindung unterstützen.

Reflektion in .NET
Um beim Programmieren mit .NET Framework Reflektion zu nutzen, verwenden Sie den System.Reflection-Namespace. Dieser Namespace stellt Klassen bereit, die viele Laufzeitkonzepte beinhalten, z. B. Assemblys, Module, Typen, Methoden, Konstruktoren, Felder und Eigenschaften. Die Tabelle in Abbildung 1 zeigt, wie die Klassen in System.Reflection ihren konzeptionellen Laufzeitäquivalenten entsprechen.

Sprachkomponente Entsprechende .NET-Klasse
Assembly System.Reflection.Assembly
Modul System.Reflection.Module
Abstraktes Mitglied System.Reflection.MemberInfo (Basisklasse für alles Folgende)
Typ System.Type
Eigenschaft System.Reflection.PropertyInfo
Feld System.Reflection.FieldInfo
Ereignis System.Reflection.EventInfo
Abstrakte Methode System.Reflection.MethodBase (Basisklasse für alles Folgende)
Methode System.Reflection.MethodInfo
Konstruktor System.Reflection.ConstructorInfo
System.Reflection.Assembly und System.Reflection.Module sind zwar wichtig, werden aber in erster Linie für das Suchen und Laden von neuem Code in die Laufzeit verwendet. Sie werden daher in diesem Artikel außer Acht gelassen, und es wird davon ausgegangen, dass der gesamte relevante Code bereits geladen ist.
Zum Prüfen und Bearbeiten von geladenem Code konzentrieren Sie sich in der Regel auf System.Type. Zunächst wird mithilfe von Object.GetType für den Laufzeittyp, der Sie interessiert, eine System.Type-Instanz abgerufen. Danach verwenden Sie die verschiedenen Methoden von System.Type, um die Definition des Typs zu untersuchen und Instanzen der anderen Klassen innerhalb von System.Reflection zu erwerben. Wenn Sie sich beispielsweise für eine bestimmte Methode interessieren, werden Sie für diese Methode wahrscheinlich (evtl. mithilfe von Type.GetMethod) eine Instanz von System.Reflection.MethodInfo erwerben. Wenn Sie sich für ein bestimmtes Feld interessieren, könnten Sie in ähnlicher Weise (evtl. mithilfe von Type.GetField) für dieses Feld eine Instanz von System.Reflection.FieldInfo erwerben.
Wenn alle erforderlichen Reflektionsinstanzobjekte vorliegen, führen Sie die notwendige Prüfung oder Bearbeitung durch. Im Falle einer Prüfung verwenden Sie die verschiedenen beschreibenden Eigenschaften der Reflektionsklassen, um die benötigten Informationen zu erhalten. (Handelt es sich um einen generischen Typ? Handelt es sich um eine Instanzmethode?) Im Falle einer Bearbeitung können Sie die Ausführung von Methoden dynamisch aufrufen, neue Objekte durch Aufrufen von Konstruktoren erstellen und so weiter.

Prüfen von Typen und Mitgliedern
Anhand eines Codebeispiels soll nun untersucht werden, wie einfache Reflektion zum Prüfen eingesetzt wird. Dabei liegt der Schwerpunkt auf der Typenanalyse. Für ein bestimmtes Objekt wird der Typ abgerufen. Anschließend werden einige interessante Mitglieder untersucht (siehe Abbildung 2).

Code
 
using System;
using System.Reflection;

// use Reflection to enumerate some basic properties of a type...

namespace Example1
{
    class MyClass
    {
        private int MyField = 0;
        public void MyMethod1() { return; }
        public int MyMethod2(int i) { return i; }
        public int MyProperty { get { return MyField; } }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Reflection Demo Example 1");

            MyClass mc = new MyClass();
            Type t = mc.GetType();
            Console.WriteLine("Type Name: {0}", t.Name);
    
            foreach(MethodInfo m in t.GetMethods())
                Console.WriteLine("Method Name: {0}", m.Name);

            foreach (PropertyInfo p in t.GetProperties())
                Console.WriteLine("Property Name: {0}", p.Name);
        }
    }
}
Ausgabe
 
Reflection Demo Example 1
Type Name: MyClass
Method Name: MyMethod1
Method Name: MyMethod2
Method Name: get_MyProperty
Method Name: GetType
Method Name: ToString
Method Name: Equals
Method Name: GetHashCode
Property Name: MyProperty 
Es ist auffallend, dass für die Beschreibung von Methoden viel mehr Zeilen verwendet werden, als man beim ersten Blick auf die Klassendefinition erwarten würde. Woher kommen diese zusätzlichen Methoden? Wer sich in der Objekthierarchie von .NET Framework auskennt, wird die Methoden erkennen, die von der universalen Basisklasse von Object selbst geerbt wurden. (Tatsächlich habe ich Object.GetType verwendet, um den Typ abzurufen.) Außerdem ist die Get-Funktion für die Eigenschaft zu sehen. Was ist zu tun, wenn Sie nur diejenigen Funktionen erhalten möchten, die explizit in MyClass definiert werden? Mit anderen Worten: Wie können die geerbten Funktionen ausgeblendet werden? Vielleicht wollen Sie auch nur die explizit definierten Instanzfunktionen erhalten.
Ein schneller Abstecher zu MSDN® Online zeigt, dass die zweite Überladung von GetMethods verwendet werden sollte, die einen Parameter namens „BindingFlags“ akzeptiert. Durch das Kombinieren verschiedener Werte aus der BindingFlags-Enumeration können Sie die Funktion anweisen, nur die gewünschte Teilmenge von Methoden zurückzugeben. Ersetzen Sie den Aufruf von GetMethods durch einen Aufruf von:
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | 
           BindingFlags.Public)
Als Ergebnis erhalten Sie die folgende Ausgabe (beachten Sie, dass keine statische Hilfsfunktion und von System.Object geerbte Funktionen vorliegen):
Reflection Demo Example 1
Type Name: MyClass
Method Name: MyMethod1
Method Name: MyMethod2
Method Name: get_MyProperty
Property Name: MyProperty
Wie ist vorzugehen, wenn die vollqualifizierten Namen eines Typs und eines Mitglieds bereits vorher bekannt sind? Wie wechseln Sie von der Typenenumeration zum Typenabruf? Das Beispiel in Abbildung 3 zeigt die Verwendung von Zeichenfolgenliteralen, die Typeninformationen beschreiben, zum Abrufen ihrer eigentlichen Codeentsprechungen über Object.GetType und Type.GetMethod. Durch Hinzuziehen des Codes aus den beiden ersten Beispielen ergeben sich die grundlegenden Komponenten für die Implementierung eines primitiven Browsers. Sie können eine Laufzeitentität über ihren Namen erkennen und danach ihre verschiedenen relevanten Eigenschaften auflisten.
using System;
using System.Reflection;

// use Reflection to retrieve references to a type and method via name

namespace Example2
{
    class MyClass
    {
        public void MyMethod1() { return; }
        public void MyMethod2() { return; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(“Reflection Demo Example 2”);

            // note that we must use the fully qualified name...
            Type t = Type.GetType(“Example2.MyClass”);
            MethodInfo m = t.GetMethod(“MyMethod1”);

            Console.WriteLine(“Type Name: {0}”, t.Name);
            Console.WriteLine(“Method Name: {0}”, m.Name);
        }
    }
}

Dynamisches Aufrufen von Code
Bisher wurden Handles für Laufzeitobjekte (z. B. Typen und Methoden) zu rein beschreibenden Zwecken (z. B. zum Ausgeben ihrer Namen) erworben. Wie lässt sich dies erweitern? Wie kann eine Methode tatsächlich aufgerufen werden? Abbildung 4 zeigt, wie Sie für ein Mitglied eines Typs eine MethodInfo erwerben und danach MethodInfo.Invoke verwenden, um die Methode tatsächlich dynamisch aufzurufen.
using System;
using System.Reflection;

// use Reflection to retrieve a MethodInfo for an 
// instance method and invoke it upon many object instances

namespace Example3
{
    class MyClass
    {
        private int id = -1;

        public MyClass(int id) { this.id = id; }

        public void MyMethod2(object p)
        {
            Console.WriteLine(
                “MyMethod2 is being invoked on object with “ +
                “id {0} with parameter {1}...”, 
                    id.ToString(), p.ToString());
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(“Reflection Demo Example 3”);

            MyClass mc1 = new MyClass(1);
            MyClass mc2 = new MyClass(2);
    
            Type t = mc1.GetType();
            MethodInfo method = t.GetMethod(“MyMethod2”);

            for(int i = 1; i <= 5; i++)
                method.Invoke(mc2, new object[]{i});
        }
    }
}
Für dieses Beispiel sind einige wichtige Schritte erforderlich. Zuerst rufen Sie eine System.Type-Instanz aus einer Instanz von MyClass (mc1) ab. Danach rufen Sie aus diesem Typ eine MethodInfo-Instanz ab. Wenn Sie schließlich MethodInfo aufrufen, binden Sie MethodInfo an eine andere Instanz von MyClass (mc2), indem Sie es als ersten Parameter von Invoke übergeben.
Wie bereits an früherer Stelle erwähnt, verwischt dieses Beispiel die Grenzen zwischen Typen- und Objektverwendung, die Sie von typischem Quellcode möglicherweise erwarten. In logischer Hinsicht rufen Sie ein Handle für eine Methode ab und rufen danach diese Methode so auf, als würde sie zu einem anderen Objekt gehören. Dies gehört für einen Programmierer, der mit einer funktionalen Programmiersprache vertraut ist, möglicherweise bereits zur Routine, aber einem Programmierer, der nur C# kennt, könnte es vielleicht widerstreben, die Objektimplementierung von der Objektinstanziierung abzukoppeln.

Verknüpfung des Ganzen
Nachdem nun die Grundlagen von Prüfung und Aufruf untersucht wurden, soll jetzt an einem konkreten Beispiel alles miteinander verbunden werden. Angenommen, Sie möchten eine Bibliothek mit einer statischen Hilfsfunktion liefern, die Objekte verarbeiten muss, wissen aber in der Entwurfsphase noch nichts darüber, zu welchen Typen diese Objekte gehören. Der Aufrufer der Funktion muss der Funktion mitteilen, wie sie aus diesen Objekten sinnvolle Informationen extrahieren kann. Die Funktion akzeptiert eine Sammlung von Objekten und eine Zeichenfolgenbeschreibung einer Methode. Danach führt sie eine Iteration durch die Sammlung durch, wobei sie für jedes Objekt die Methode aufruft und durch eine Funktion die Rückgabewerte sammelt (siehe Abbildung 5).
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Example4
{
    class Program
    {
        static void Main(string[] args)
        {
            // prepare some objects for our function to process
            object[] objs = new object[] {
                new IntReturner(1), new IntReturner(2), 
                new IntReturner(3), new SonOfIntReturner(4), 
                new SonOfIntReturner(5), new SonOfIntReturner(6),
            };

            Console.WriteLine(
                “Attempting to compute average, “ + 
                “passing in array with {0} elements.”, objs.Length);

            int average = ComputeAverage(objs, “GetInteger”);

            Console.WriteLine(“Found an average of {0}!”, average);
        }

        public static int ComputeAverage( 
            IEnumerable<object> objs, string methodname)
        {
            int sum = 0, count = 0;

            Type firstType = null;
            MethodInfo firstMethod = null;

            foreach (object o in objs)
            {
                if (firstMethod == null)
                {
                    firstType = o.GetType();
                    firstMethod = firstType.GetMethod(methodname);
                }

                sum += (int)firstMethod.Invoke(o, null);
                count++;
            }

            // note that we use integer division here (not floating point)
            if (count == 0) return 0;
            return sum / count; 
        }
    }

    class IntReturner
    {
        protected int value = -1;
        public IntReturner(int i) { value = i; }
        public virtual int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of IntReturner, “
                “I’m returning {0}!”, value);
            return value;
        }
    }

    class SonOfIntReturner : IntReturner
    {
        public SonOfIntReturner(int i) : base(i) { }
        public override int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of SonOfIntReturner, “
                “I’m returning {0}!”, this.value);
            return value;
        }
    }

    class EnemyOfIntReturner
    {
        protected int value = -1;
        public EnemyOfIntReturner(int i) { value = i; }
        public virtual int GetInteger()
        {
            Console.WriteLine(
                “GetInteger called on instance of EnemyOfIntReturner, “
                “I’m returning {0}!”, value);
            return value;
        }
    }
}
Für dieses Beispiel werden im Folgenden einige Einschränkungen dokumentiert. Zunächst akzeptiert die Methode, die vom Zeichenfolgenparameter beschrieben und notwendigerweise durch den jedem Objekt zugrunde liegenden Typ implementiert wird, keine Parameter und gibt eine Ganzzahl zurück. Der Code führt eine Iteration durch die Objektsammlung durch, ruft bei jeder Wiederholung die angegebene Methode auf und berechnet schrittweise den Durchschnitt aller Werte. Da dies kein Produktionscode ist, ist es nicht erforderlich, beim Zusammenstellen der Summe Parameter zu überprüfen oder Vorsorge für einen möglichen Ganzzahlüberlauf zu treffen.
Beachten Sie beim Durchsehen des Beispielcodes, dass das Protokoll zwischen der Main-Funktion und der statischen Hilfsfunktion ComputeAverage abgesehen von der universalen Basisklasse von Object selbst von keinen Typeninformationen abhängig ist. Anders ausgedrückt: Sie könnten den Typ und die Struktur der Objekte, die Sie übergeben, komplett ändern, doch solange Sie immer eine Zeichenfolge verwenden können, um eine Methode zu beschreiben, die eine Ganzzahl zurückgibt, funktioniert die ComputeAverage-Funktion.
In diesem letzten Beispiel ist im Zusammenhang mit MethodInfo (und der Reflektion generell) ein wichtiger Punkt versteckt. Beachten Sie, dass innerhalb der foreach-Schleife von ComputeAverage der Code nur aus dem ersten Objekt der Sammlung MethodInfo einliest und es danach für jedes nachfolgende Objekt für den Aufruf bindet. Im vorliegenden Programm funktioniert dies hervorragend. Dies ist ein einfaches Beispiel für die MethodInfo-Zwischenspeicherung. Hier ist jedoch eine grundlegende Einschränkung zu beachten. Eine MethodInfo-Instanz kann nur für Instanzen von Typen aus derselben Hierarchie wie das Objekt, aus dem sie abgerufen wurde, aufgerufen werden. In unserem Beispiel funktioniert es, weil Sie Instanzen sowohl von IntReturner als auch von SonOfIntReturner (der von IntReturner erbt) übergeben.
Im Beispielcode wurde eine Klasse namens „EnemyOfIntReturner“ eingeschlossen, die dasselbe grundlegende Protokoll wie die beiden anderen Klassen implementiert, aber keinen allgemein freigegebenen Typ gemeinsam hat. Mit anderen Worten: Die Schnittstelle ist logisch gleichwertig, aber es kommt zu keiner Überlappung in der Typenhierarchie. Um die Verwendung von MethodInfo in diesem Szenario zu untersuchen, fügen Sie der Sammlung als weiteres Objekt eine Instanz hinzu, die mit „new EnemyOfIntReturner(10)“ erworben wurde, und führen Sie danach den Beispielcode erneut aus. Sie erhalten den Ausnahmefehler, dass MethodInfo nicht verwendet werden kann, um Invoke am angegebenen Object auszuführen, weil es in keinerlei Beziehung zu dem ursprünglichen Typ steht, aus dem MethodInfo erworben wurde (obwohl der Methodenname und das grundlegende Protokoll äquivalent sind). Für einen Code mit Produktionsqualität müssen Sie das Problem in diesem Szenario beheben.
Eine mögliche Lösung könnte darin bestehen, den Typ aller eingehenden Objekte selbst zu analysieren und eine Interpretation der freigegebenen Typenhierarchie der Objekte (sofern vorhanden) zu verwalten. Wenn das nächste Objekt von einem Typ ist, der von jeder bekannten Typenhierarchie abweicht, müssen Sie eine neue MethodInfo erwerben und speichern. Eine andere Lösung könnte darin bestehen, die TargetException abzufangen und einfach erneut eine MethodInfo-Instanz zu erwerben. Beide hier erwähnten Lösungen haben Vor- und Nachteile. Joel Pobar hat in der Ausgabe dieses Magazins vom Juli 2005 einen hervorragenden Artikel über MethodInfo-Zwischenspeicherung und Reflektionsleistung geschrieben, den ich sehr empfehlen kann.
Anhand dieses Beispiels wurde hoffentlich aufgezeigt, dass ein Produkt durch Hinzufügen von Reflektion zu einer Anwendung oder einem Framework erhöhte Flexibilität für die spätere Anpassung oder für Erweiterbarkeit erhält. Zugegebenermaßen ist die Verwendung von Reflektion etwas mühselig, wenn man sie mit der gleichwertigen Logik in Ihrer eigenen Programmiersprache vergleicht. Wenn Sie meinen, dass das Hinzufügen reflektionsbasierter Bindung zu Ihrem Code Ihnen oder Ihren Kunden zu viel Kummer bereitet (schließlich müssen die Kunden ihre Typen ja irgendwie beschreiben und für Ihr Framework programmieren), dann wäre es vielleicht möglich, eine Kompromisslösung mit genau dem richtigen Maß an Flexibilität zu finden.

Effiziente Typenverarbeitung für die Serialisierung
Nachdem nun die Grundlagen der .NET-Reflektion anhand von Beispielen durchlaufen wurden, soll jetzt ein realistisches Szenario in Angriff genommen werden. Wenn Ihre Software über Webdienste oder eine andere prozessexterne Remotetechnologie mit anderen Systemen interagiert, sind Sie wahrscheinlich bereits auf Serialisierung gestoßen. Serialisierung ist im Wesentlichen die Übersetzung eines aktiven, im Speicher enthaltenen Objekts in ein Datenformat, das zum Übertragen über das Netzwerk oder Speichern auf einem Datenträger geeignet ist.
Der System.Xml.Serialization-Namespace in .NET Framework stellt mit XmlSerializer ein leistungsfähiges Serialisierungsmodul bereit, das beliebige verwaltete Objekte nutzen und in XML konvertieren kann, und zwar mit der Option, die XML-Daten zu einem späteren Zeitpunkt wieder in typisierte Objektinstanzen zu übersetzen, was als Deserialisierung bezeichnet wird. Die XmlSerializer-Klasse ist eine leistungsfähige, für den Unternehmenseinsatz bereite Software, für die Sie sich entscheiden sollten, wenn Sie bei Ihrem Projekt mit Serialisierung arbeiten. Zum Zweck der Wissenserweiterung soll jetzt untersucht werden, wie Serialisierung (oder eine ähnliche Art der Laufzeittypverarbeitung) bewerkstelligt werden könnte.
Das Szenario: Sie liefern ein Framework, das Objektinstanzen beliebiger Benutzertypen verwendet und in ein intelligentes Datenformat konvertiert. Nehmen wir beispielsweise einmal an, Sie haben folgendes Objekt des Typs Adress im Speicher vorliegen:
(pseudocode)
class Address
{
    AddressID id;
    String Street, City;
    StateType State;
    ZipCodeType ZipCode;
}
Wie generieren Sie eine geeignete Datendarstellung zur späteren Nutzung? Vielleicht wäre ein einfaches Textrendering ausreichend:
Address: 123
    Street: 1 Microsoft Way
    City: Redmond
    State: WA
    Zip: 98052
Wenn Sie vorher (zum Beispiel beim Schreiben des Programms) genau wissen, welche formalen Datentypen Sie benötigen, dann ist es ganz einfach:
foreach(Address a in AddressList)
{
    Console.WriteLine(“Address:{0}”, a.ID);
    Console.WriteLine(“\tStreet:{0}”, a.Street);
    ... // and so on
}
Es wird jedoch interessant, wenn Ihnen vorher nicht bekannt ist, mit welchen Typen Sie zur Laufzeit interagieren müssen. Wie schreibt man einen derartigen allgemeinen Frameworkcode?
MyFramework.TranslateObject(object input, MyOutputWriter output)
Zunächst müssen Sie entscheiden, welche Typmembers für die Serialisierung von Interesse sind. Möglichkeiten dazu wären das Erfassen von Mitgliedern bestimmter Typen (z. B. primitiver Systemtypen) oder das Bereitstellen eines Mechanismus, durch den der Autor des Typs beschreiben kann, welche Mitglieder serialisiert werden müssen (z. B. mithilfe benutzerdefinierter Attribute als Markup für Typmembers). Sie könnten nur Mitglieder bestimmter Typen (z. B. primitive Systemtypen) erfassen, oder der Autor des Typs könnte beschreiben, welche Mitglieder serialisiert werden müssen, indem er beispielsweise benutzerdefinierte Attribute als Markup für Typmembers verwendet.
Wenn Sie dokumentiert haben, welche Datenstrukturmitglieder übersetzt werden, müssen Sie eine Logik schreiben, die diese Mitglieder auflisten und sie aus einem eingehenden Objekt abrufen kann. Reflektion leistet hierbei die Schwerarbeit, indem Ihnen ermöglicht wird, sowohl die Datenstruktur als auch den Datenwert abzufragen.
Zur Vereinfachung wollen wir ein simples Übersetzungsmodul entwickeln, das ein Objekt einliest, die Werte aller öffentlichen Eigenschaften dieses Objekts erfasst, diese Werte durch direkten Aufruf von ToString in Zeichenfolgen umwandelt und die Werte anschließend serialisiert. Der Algorithmus dafür würde bei einem Objekt namens „input“ ungefähr folgendermaßen aussehen:
  1. Rufen Sie input.GetType auf, um eine System.Type-Instanz abzurufen, die eine der Eingabe zugrunde liegende Struktur beschreibt.
  2. Verwenden Sie Type.GetProperties mit einem geeigneten BindingFlags-Parameter, um die öffentlichen Eigenschaften als PropertyInfo-Instanzen abzurufen.
  3. Verwenden Sie PropertyInfo.Name und PropertyInfo.GetValue, um die Eigenschaften als Schlüssel-/Wertpaare abzurufen.
  4. Rufen Sie für jeden Wert Object.ToString auf, um den Wert (grob) in das Zeichenfolgenformat zu konvertieren.
  5. Packen Sie den Namen des Objekttyps zusammen mit der Sammlung von Eigenschaftennamen und Zeichenfolgenwerten in das richtige Serialisierungsformat.
Dieser Algorithmus vereinfacht die ganze Angelegenheit beträchtlich, stellt aber alle Bedingungen bereit, um eine Laufzeitdatenstruktur zu erfassen und in selbstbeschreibende Daten umzuwandeln. Es gibt allerdings einen Haken: die Leistung. Wie bereits erwähnt, ist Reflektion für die Typenverarbeitung und das Abrufen von Werten eine kostspielige Angelegenheit. In diesem Beispiel wird die volle Typenanalyse bei jeder Instanz des Typs durchgeführt, der bereitgestellt wird.
Wie wäre es, wenn Ihr Verständnis der Struktur des Typs irgendwie erfasst oder beibehalten werden könnte, um es später einfach abzurufen und neue Instanzen dieses Typs effizient zu verarbeiten, indem Sie im Beispielalgorithmus zu Schritt 3 springen? Die gute Nachricht ist, dass dies mithilfe der Funktionalität von .NET Framework absolut möglich ist. Wenn Sie die Datenstruktur des Typs verstehen, können Sie mithilfe von CodeDom dynamisch Code generieren, der an diese Datenstruktur gebunden ist. Sie generieren somit eine Hilfsassembly mit einer Hilfsklasse und einer Hilfsmethode, die auf den eingehenden Typ verweist und (wie jeder andere Verweis in verwaltetem Code) direkt auf seine Eigenschaften zugreift. Dies hat zur Folge, dass die Leistungseinbuße für die Typenüberprüfung nur einmal in Kauf genommen werden muss.
Nun soll der Algorithmus abgeändert werden. Führen Sie Folgendes für einen neuen Typ aus:
  1. Erwerben Sie eine System.Type-Instanz, die diesem Typ entspricht.
  2. Verwenden Sie die verschiedenen Accessoren von System.Type, um das Schema, oder zumindest die für die Serialisierung interessante Teilmenge des Schemas (wie Eigenschaftennamen, Feldnamen und so weiter) abzurufen.
  3. Verwenden Sie die Schemainformationen, um (über CodeDom) eine Hilfsassembly zu generieren, die mit dem neuen Typ verknüpft ist und Instanzen effizient verarbeitet.
  4. Verwenden Sie den Code in der Hilfsassembly, um die Daten der Instanz zu extrahieren.
  5. Serialisieren Sie die Daten nach Bedarf.
Bei allen eingehenden Instanzen eines bestimmten Typs können Sie zu Schritt 4 springen, um die Leistung beträchtlich zu steigern, statt jede Instanz explizit zu prüfen.
Ich habe eine einfache Serialisierungsbibliothek namens „SimpleSerialization“ entwickelt, die diesen Algorithmus mit Reflektion und CodeDom implementiert. Die Bibliothek ist als Bestandteil des Downloads für diesen Artikel verfügbar. Die Hauptkomponente besteht aus einer Klasse namens „SimpleSerializer“, die der Benutzer mit einer System.Type-Instanz erstellt. Innerhalb des Konstruktors analysiert die neue Instanz von SimpleSerializer den bereitgestellten Typ und generiert eine temporäre Assembly mit einer Hilfsklasse. Diese Hilfsklasse ist eng an den bereitgestellten Datentyp gebunden und kann Instanzen genauso verarbeiten, als hätten Sie den Code selbst geschrieben und dabei schon vorher genau gewusst, mit welchem Typ Sie zu tun haben.
Die SimpleSerializer-Klasse besitzt das folgende Layout:
class SimpleSerializer
{
    public class SimpleSerializer(Type dataType);

    public void Serialize(object input, SimpleDataWriter writer);
}
Erstaunlich einfach! Der Konstruktor erledigt den größten Teil der Arbeit: Er verwendet Reflektion, um die Struktur des Typs zu analysieren und danach mit CodeDom eine Hilfsassembly zu generieren. Die SimpleDataWriter-Klasse ist lediglich eine Datensenke zur Veranschaulichung des allgemeinen Serialisierungsmusters.
Wenn Instanzen einer einfachen Address-Klasse serialisiert werden müssen, lässt sich dies mit folgendem Pseudocode erreichen:
SimpleSerializer mySerializer = new SimpleSerializer(typeof(Address));
SimpleDataWriter writer = new SimpleDataWriter();
mySerializer.Serialize(addressInstance, writer);

Schlussbemerkung
Ich empfehle Ihnen wärmstens, mit dem Beispielcode und vor allem mit der SimpleSerialization-Bibliothek zu experimentieren. Die interessanten Teile von SimpleSerializer enthalten meine Kommentare, und ich hoffe, dass diese für Sie aufschlussreich sind. Wenn Sie eine ernsthafte Serialisierung in Produktionscode durchführen müssen, sollten Sie sich allerdings unbedingt auf die Technologien verlassen, die in .NET Framework bereitgestellt werden (z. B. XmlSerializer). Wenn es aber darum geht, zur Laufzeit beliebige Typen zu nutzen und effizient zu verarbeiten, kann meine SimpleSerialization-Bibliothek hoffentlich an Ihr eigenes Szenario angepasst werden.
Ich danke den CLR-Entwicklern Weitao Su (Reflektion) und Pete Sheill (CodeDom) für ihre Hilfe und ihr Feedback.

Senden Sie Ihre Fragen und Kommentare an  clrinout@microsoft.com.


Mike Repass ist Programmmanager des .NET Framework-CLR-Teams. Er arbeitet an Reflektion, CodeDom und verschiedenen Teilen des Ausführungsmoduls. Sie können ihn über sein Blog unter der URL blogs.msdn.com/mrepass erreichen.

Page view tracker