C# Seminar: Reflection im .NET Framework
Transcription
C# Seminar: Reflection im .NET Framework
C# Seminar: Reflection im .NET Framework Johannes Roith johannes@jroith.de Abstract: Das .NET Framework enthält Programmierschnittstellen (APIs) mit denen die Struktur von C#-Programmen zur Laufzeit untersucht und verändert werden kann. Wie die APIs eingesetzt werden, wo das .NET Framework davon Gebrauch macht und warum viele C#-Programme von ihnen profitieren können wird in dem vorliegenden Dokument behandelt. 1 Einführung Als Reflection bezeichnet man in diversen modernen Programmiersprachen die Möglichkeit Programmstrukturen zur Laufzeit zu untersuchen. Diese entsprechen mehr oder weniger dem logischen Aufbau der Quelltexte. 1 Insbesondere bei virtuellen Maschinen ist die Implementierung von Reflection praktikabel, da diese ohnehin recht abstrakten und Plattform-unabhängigen Code ausführen der erst zur Laufzeit in Maschinencode übersetzt wird. So haben zum einen Optimierungen des Compilers die Programmstruktur noch nicht stark verändert, zum anderen sind auch die konkreten Instruktionen auf jeder Plattform die selben. Folglich bleibt Code, der seine eigene Struktur untersucht oder verändert, portabel. Die reflektierende Analyse hat eine Reihe von Vorteilen: • Code-Generatoren können u.U. direkt aus der Programmstruktur benötigte Informationen ermitteln und brauchen keine getrennte Beschreibung mehr, die in einer separaten Datei gepflegt werden müsste. • Performance-Optimierungen durch dynamische Codeerzeugung zur Laufzeit werden möglich. • Fehleranfällige manuelle Auflistungen von bestimmten Klassen und Funktionen an zentraler Stelle im Programm sind vermeidbar; Statt beispielsweise Handler/Plugins etc. in einer Manager-Klasse zu registrieren, könnte das Programm entsprechende Plugins selbst auffinden. 1 C# wurde ursprünglich parallel mit der Common Language Runtime entwickelt und das Mapping von Sprachkonstrukten stand praktisch in einem 1:1 Verhältnis zu den Strukturen in der Assembly; Seit .NET 3.5 wurden viele C#-Erweiterungen eingeführt die abwärtskompatibel zur .NET 2.0 Runtime sein sollten, z.B. C# Extension Methoden, Lambda-Ausdrücke, LINQ, das dynamic-Keyword, usw. Hier weicht die Struktur des Codes in den Assemblies teilweise gravierend von der C#-Struktur ab. Abbildung 1: Quell-Code, MSIL-Code (Console), und dekompilierter Code einer Assembly (Hintergrund) • Eigene Metadaten (Attribute) können direkt Programmstrukturen zugeordnet werden, beispielsweise einer Methode. Die Lesbarkeit des Codes kann so oft verbessert werden. • Die Zusammenarbeit mit Software die in dynamischen Programmiersprachen (wie Ruby oder Python) vorliegt wird erleichtert. • Statische Codeanalyse-Tools die bestimmte Regeln/Konventionen prüfen können recht leicht programmiert werden. 2 Grundlagen von Reflection in .NET Programme (in kompilierter Form) die auf dem Microsoft .NET Framework aufsetzen liegen in plattformunabhängigen Binär-Dateien, sogenannten Assemblies, vor. Diese enthalten neben den Instruktionen auch Informationen über die Programmstruktur, z.B. über Typen und deren Eigenschaften und Methoden. Die Klassenbibliothek enthält eine API mit der .NET Programme die Struktur von anderen .NET Programmen oder von sich selbst analysieren können. Dazu werden die entsprechenden Informationen aus den Assemblies ausgelesen - bzw. aus dem Hauptspeicher, falls die Assembly bereits geladen ist. 2.1 Zusammenhang mit dem .NET Typ-System Die Reflection API ist eng mit dem .NET-Typ-System verknüpft. Da die Common Language Runtime (CLR) streng objektorientiert gestaltet ist, sind alle logischen Programmstrukturen, also etwa Eigenschaften, Methoden oder Felder immer innerhalb eines Typs definiert. Physisch sind Typen selbst in Modulen enthalten, diese schließlich in einer Assembly, also einer Binär-Datei in einem .NET-spezifischen Format deren Dateiname mit .dll oder .exe endet. Es ist hilfreich zu verstehen in welcher Form .NET-Objekte zur Laufzeit innerhalb der CLR existieren: Eine Instanz eines .NET-Typs wird im Hauptspeicher des Rechners an einer bestimmten Adresse abgelegt. Vor den Werten der Felder der Instanz befindet sich u.A. ein Zeiger auf eine Struktur, die den Typ des Objekts repräsentiert. Dieser Zeiger verweist für alle Instanzen eines Typs auf die selbe Adresse; Es gibt also zur Laufzeit nur eine solche Struktur pro Typ. [1, S. 81] Außerdem kann die CLR auf die Typ-Struktur auch direkt zugreifen, sofern der Name des Typs bekannt ist. Die genannte Typ-Struktur enthält weitere Verweise auf Informationen die den Typ und seine Attribute, Methoden und Eigenschaften beschreiben. .NET Code kann über die Klasse System.Type auf diese Informationen zugreifen. System.Type-Objekte bilden daher den Einstiegspunkt für Reflection-Aufrufe. public class MeinTyp { private int einFeld = 42; protected float EineProperty { get; set; }; public void Methode1 (int a, int b) { /* ... */ } public List<string> Methode2 () { /* ... */ } } 1 2 3 4 5 6 7 Listing 1: Eine Beispiel-Klasse Ein solches Objekt erhält man über eine Methode GetType () die auf jeder Instanz eines beliebigen Objekts bereitgestellt wird. Für die Klasse aus Listing 1 sieht das so aus: MeinTyp instanz = new MeinTyp (); Type t = instanz.GetType (); Liegt von einem Typ keine Instanz vor so kann das entsprechende Type-Objekt in C# mit dem typeof-Operator dennoch ermittelt werden: Type t = typeof (MeinTyp); Bei Arrays kann der Typ des Arrays zum Typ der Elemente ermittelt werden und umgekehrt. Type arrayTyp = typeof (double).MakeArrayType (); Type elemTyp = (new int [5]).GetType ().GetElementType (); 2.2 Typen aus Assemblies dynamisch laden Die in den obigen Beispielen verwendeten Typen müssen dem Compiler bekannt sein, d.h. er muss in der Lage sein die Definition der Typen in den eingebundenen Namensräumen zu finden; Die entsprechenden Assemblies müssen beim Kompilieren referenziert worden sein. Es ist aber auch möglich dynamisch Typen zu laden von denen nur der Name einschließlich des Namens der Assembly, in der sie sich befinden, bekannt ist. Type t = Type.GetType ("MeinTyp, MeineAssembly"); Ferner ist es möglich alle Typen zu ermitteln, die sich in einer Assembly befinden. Dazu wird zunächst die Assembly aus der entsprechenden Datei geladen: Assembly asm = Assembly.LoadFrom ("MeineAssembly.dll"); Type[] types = asm.GetTypes (); Schließlich gibt es noch die Option über Typen zu reflektieren ohne die entsprechende Assembly zu aktivieren. Das kann beispielsweise aus Sicherheitsgründen wichtig sein, da beim Laden der Assembly bereits Code ausgeführt werden kann. LoadFrom muss dazu einfach durch ReflectionOnlyLoadFrom ersetzt werden. Sollte man versuchen einen so geladenen Typen zu instanziieren, so kommt es zu einem Laufzeitfehler. 2.3 Reflection-Objekt-Modell Ein Type-Objekt ermöglicht unter anderem eine Liste der enthaltenen Member abzurufen. Darauf wird unten in Abschnitt 3.1 genauer eingegangen. So wie Typen durch das System.Type-Objekt repräsentiert werden, so gibt es auch Klassen um Informationen über die unterschiedlichen Member zugänglich zu machen. Die Liste der Member ist ein Array von solchen MemberInfo-Objekten. 3 Verwendung & Einsatzmöglichkeiten Im Folgenden wird genauer untersucht wie ausgehend von Type-Objekten die ReflectionAPI verwendet werden kann. Anschließend werden Attribute eingeführt und genauer behandelt. Schließlich werden einige fortgeschrittene Techniken gezeigt um Microsoft Intermediate Language (MSIL) zu analysieren und zur Laufzeit dynamisch zu erzeugen. System.Reflection System MemberInfo FieldInfo MethodBase ContructorInfo PropertyInfo EventInfo Type MethodInfo Abbildung 2: Das Reflection-Objekt-Modell 3.1 Analyse von Typ-Informationen Im Abschnitt 2 wurde bereits erläutert, dass Typen durch System.Type-Objekte beschrieben werden. 3.1.1 Interessante Typ-Eigenschaften Nützliche Methoden und Eigenschaften (für System.Type-Objekte t, t2): t.BaseType; // Referenz auf den Type-Objekt des Basis-Typs t.IsInstanceOfType (t2) // Wahr, wenn eine Instanz von t2. t.IsSubclassOf (t2) // Wahr, wenn t Unterklasse von t2. t.IsAssignableFrom (t2) // Wahr wenn t Unterklasse oder ein entsprechendes Interface implementiert wird. Um im Typ enthaltene Elemente zu ermitteln gibt es eine Reihe von Methoden auf dem System.Type-Objekt. Gelesen werden können alle Elemente (mit GetMembers ()), gefilterte Teilmengen oder auch einzelne Elemente mit GetMember (string name), GetNestedType (), GetConstructor (object[] prms), GetField (string name), GetProperty (string name) oder GetMethod (string name). Manche davon sind überladen und akzeptieren weitere Parameter. Methoden die einzelne Elemente auffinden geben ein MemberInfo-Objekt oder ein davon abgeleitetes Objekt zurück. Beispielsweise liefert GetProperty (string name) ein PropertyInfo-Objekt. Methoden die mehrere Elemente liefern werden analog verwendet. 3.2 Zugriff auf Methoden und Eigenschaften Methoden werden durch MethodInfo-Objekte beschrieben. Das folgende Beispiel ermittelt die Namen und Typen der Parameter der Methode Methode1 aus Listing 1: Type t = typeof (MeinTyp); MethodInfo mi = t.GetMethod ("Methode1"); ParameterInfo[] pis = mi.GetParameters (); foreach (var pi in pis) Console.WriteLine (String.Format ("{0}#{1}", pi.Name, pi.Type); Ausgabe: a#System.Int32 b#System.Int32 Selbstverständlich gibt es zahlreiche weitere Eigenschaften/Methoden des MethodInfoObjekts, die in der MSDN-Dokumentation [6] genauer beschrieben werden. 3.2.1 Zugriff auf Properties und Felder Im Reflection-Modell werden Properties durch PropertyInfo-Objekte, Felder durch FieldInfo-Objekte repräsentiert. Aus Platzgründen wird auf eine beispielhafte Beschreibung verzichtet. Details finden sich auch hier in der MSDN-Dokumentation [6]. 3.2.2 Umgang mit generischen Typen Generische Typen kommen im Rahmen von Reflection in zwei Formen vor: Bei geschlossenenen generischen Typen wurden bereits alle Typ-Parameter definiert, bei offenen nicht. Offene generische Typen können deshalb auch per Reflection nicht direkt instanziiert werden; Beispielsweise könnte man eine Methode mit Rückgabetyp List’int’ definieren. Hier ist der Rückgabeparameter gebunden, nämlich als int. Ermittelt man per Reflection das System.Type-Objekt des Rückgabetyps, so liefert die darauf definierte Property IsGenericTypeDefinition false. Betrachtet man die Klasse List’T selbst, so ist der T-Parameter offensichtlich noch offen und IsGenericTypeDefinition liefert true. Offene System.Type-Objekte können auch direkt erzeugt werden: Type t = typeof (List<>); // offene Liste Type t2 = typeof (Dictionary<,>); // offenes Dictionary Generische Typen, ob offen oder geschlossen, können mit der Property IsGenericType von anderen Typen unterschieden werden. Offene Typen können zur Laufzeit geschlossen werden: Type offen = typeof (List<>); Type geschlossen = offen.MakeGenericType (typeof (float), ... ); Bei geschlossenen Typen ermittelt man zur Laufzeit die zugrundeliegende offene Definition so: Type geschlossen = typeof (List<int>); Type offen = geschlossen.GetGenericTypeDefinition (); Ähnliches gilt übrigens auch für generische Methoden, die auch erst aufgerufen werden können nachdem sie konkretisiert wurden. Generische Methoden werden allerdings im Rahmen dieses Dokuments nicht weiter behandelt. 3.2.3 Mapping von C#-Features Nicht alle C#-Sprachfeatures haben eine direkte Entsprechung in der CLR. Sie werden daher auf allgemeinere CLR Strukturen gemappt. Teilweise bilden auch mehrere CLRStrukturen gemeinsam ein C#-Feature ab: Enums C#-Enums werden durch statische Klassen die von System.Enum abgeleitet sind und ein statisches Feld je Wert enthalten repräsentiert. Operatoren Operatoren wie +, -, *, / usw. werden auf Methoden mit best. Namen (op , z.B. op Addition, etc.) abgebildet. Events, Properties Abbildung durch Meta-Daten in EventInfo-, PropertyInfo-Objekten und je zwei Methoden der Form get , set ). Indexer Wird wie eine Property (mit Parametern) behandelt und erhält ein Attribut [DefaultMember]. Finalizer 3.3 Überschreibt eine Methode, nämlich die mit dem Namen Finalize. Dynamische Instanziierung und dynamischer Aufruf Eine Instanz eines Typs kann am einfachsten mit der Hilfsklasse Activator erzeugt werden: Type t = ... object instanz = Activator.CreateInstance (t, arg1, arg2, ...); Alternativ könnte auch der passende Konstruktor aufgerufen werden. Um Methoden aufrufen zu können benötigt man zuerst das passende MethodInfo-Objekt. Auf diesem kann dann Invoke aufgerufen werden. Erster Parameter ist eine Instanz des Objekts auf dem der Call erfolgen soll oder null falls es sich um eine statische Methode handelt. Alle weiteren Parameter werden an die aufzurufende Methode weitergegeben. Das folgende Beispiel erzeugt dynamisch ein Objekt vom Typ StringBuilder und ruft dynamisch die Append-Methode auf. Type t = typeof (StringBuilder); StringBuilder sb = (StringBuilder) Activator.CreateInstance (t); MethodInfo mi = t.GetMethod ("Append"); mi.Invoke (sb, "blub"); Console.WriteLine (sb.ToString ()); Ähnlich verhält es sich mit Properties: PropertyInfo: object GetValue (object instanz, object[] args); PropertyInfo pi = typeof (StringBuilder).GetProperty ("Length"); var sb = new StringBuilder (); sb.Append ("<"); int length = (int) pi.GetValue (sb, null); Console.WriteLine (length); Ausgabe: 1 Typen und Methoden die aus einer Assembly stammen, die mit Assembly.ReflectionOnlyLoadFrom geladen wurden können nicht per Reflection instanziiert bzw. aufgerufen werden (vgl. Abschnitt 2.2). 3.4 Attribute Attribute können - im allgemeinen - als Eigenschaften von MemberInfo-Objekten verstanden werden. Sie charakterisieren in der Regel Programmstrukturen (Typen, Felder, Properties, Methoden, usw.) und sagen weniger über das Problem, das von einem Programm gelöst wird und mehr über die Struktur der Problemlösung. Man spricht deshalb auch von Metadaten. Ist beispielsweise ein Feld als public definiert, so wird diese Information in der Assembly gespeichert und kann von der Runtime bzw. vom Compiler ausgewertet und auch per Reflection mit dem entsprechenden FieldInfo-Objekt gelesen werden. .NET erlaubt aber auch die Definition von beliebigen Attributen die nicht Teil der Syntax einer .NET-Programmiersprache sind. Wenn man im Rahmen von .NET von Attributen spricht meint man meistens diese Art von Custom-Attributen. Einige sind Bestandteil der .NET Klassenbibliothek und spielen eine zentrale Rolle für grundlegende Aufgaben wie z.B. die Serialisierung von Objekten. • Objekte über die reflektiert werden kann haben oft konstante Eigenschaften (z.B. Zugriffserlaubnis, Überschreibbarkeit, usw ...) • Attribute erlauben die Definition zahlreicher Eigenschaften (z.B. Serialisierbarkeit, Steuerung des Compilers, Steuerung des Debuggers, usw.) • Neue Attribute können definiert werden • Abwärtskompatible Erweiterung der CLR wird erleichtert • Ermöglicht “aspekt-orientierte” Programmierung, Cross-Cutting Concerns Im .NET Framework sind Attribute überall anzutreffen. Sie werden beispielsweise für die Serialisierung eingesetzt, um C-Bibliotheken aufrufen zu können, im Rahmen der COM-Interoperabilität ebenso wie für Remoting, WebServices oder zur Kennzeichnung von Unit-Tests. 3.4.1 Anwendung von Attributen Die Anwendung von bereits definierten Attributen ist sehr einfach. Der Name des anzuwendenden Attributes wird in eckige Klammern gesetzt und vor die Definition des ZielElements geschrieben. Der Name aller Attribute endet oft auf Attribute, dieses Suffix kann daher bei der Anwendung des Attributs weggelassen werden. [Serializable] class Blub { } Listing 2: Beispiel: Serialisierung Manche Attribute akzeptieren auch Parameter. Zu beachten ist, dass es sich um Werte handeln muss, die bereits vom Compiler als Konstanten eingebettet werden können. [WebMethod (MessageName="add")] public int Sum (int a, int b) { return a + b; } Listing 3: Beispiel: Web Services 3.4.2 Attribute lesen Attribute sind eine passive Einheit. Damit sie den Ablauf eines Programms beeinflussen können, müssen sie per Reflection ausgewertet werden. Dennoch ist man sich dieser Tatsache nicht immer bewusst und Attribute scheinen so manchmal auf fast magische Weise aktiv zu werden. Selbstverständlich ist das nicht der Fall und um hier Klarheit zu schaffen unterscheide ich folgende praktische (nicht technische) Formen in denen Attribute auftreten: • Auswertung durch eigenen Code: Der direkte, flexibelste und vollständig transparente Weg ist es, im Rahmen des normalen Programmablaufs Attribute per ReflectionAPI selbst zu suchen und entsprechend zu reagieren. Listing 4 zeigt ein Beispiel, das alle Attribute eines bestimmten Attribut-Typs (hier SerializableAttribute) auf der Test-Klasse abruft und auf der Konsole ausgibt. using System; using System.Runtime.Serialization; using System.Reflection; 1 2 3 4 [Serializable] 5 public class Test 6 { 7 public static void Main (string[] args) 8 { 9 object [] attributes = typeof (Test).GetCustomAttributes ( 10 typeof (SerializableAttribute), false); foreach (var current in attributes) { 11 SerializableAttribute attribute = (SerializableAttribute)12 current; Console.WriteLine (attribute); 13 } 14 } 15 } 16 Listing 4: Beispiel zur Serialisierung Ausgabe: System.SerializableAttribute • Auswertung durch Library-Code: Das selbst geschriebene Programm enthält Attribute und an mindestens einer Stelle einen Aufruf einer Funktion der Klassenbibliothek, bei dem in der Regel ein Typ-Objekt einer selbst definierten Klasse übergeben wird. Die Funktion in der Klassenbibliothek untersucht den Typ dann per Reflection und wertet bestimmte, eventuell vorhandene Attribute aus. Ein Beispiel ist die (Xml-)Serialsierung: Attribute steuern wie das Test-Objekt in XML abgebildet wird. Der Reflection-Code ist dabei in der Serialize-Methode des Frameworks gekapselt. using using using using System; System.Xml; System.Xml.Serialization; System.IO; 1 2 3 4 5 public class Test { public string Feld1; // wird ein Element [XmlAttribute] public string Feld2; // wird ein Attribut 6 7 8 9 10 11 public static void Main () { XmlSerializer serializer using (TextWriter writer Test test = new Test serializer.Serialize writer.Close (); } } 12 = new XmlSerializer (typeof(Test)); 13 = new StreamWriter ("test.xml")) { 14 (); 15 (writer, test); 16 } 17 18 19 20 Listing 5: Xml-Serialisierung • Auswertung durch Framework-Code : In diesem Fall ruft der Programmierer überhaupt keine Funktion auf, die das Laufzeit-Verhalten aufgrund eines Attributes beeinflussen könnte und untersucht auch selbst seinen Code nicht per Reflection. Dennoch können Attribute nicht selbst Code ausführen. Hier steuert das Programm nicht direkt seinen Lebenszyklus (beginnend in der Main-Methode), sondern es wird im Rahmen eines Frameworks aktiviert; Das Framework kann entsprechend vor und während der Ausführung des Programms Attribute analysieren und Aktionen veranlassen. Ein klassisches Beispiel, das seit .NET 1.0 vorhanden ist, sind die ASP.NET Web Services. Dabei wird ein WebService definiert, der per SOAP angesprochen werden kann. Der Entwickler definiert auf einer Klasse bzw. einigen Methoden Attribute und deklariert diese damit als WebService-Funktionen. Das ASP.NET Framework decodiert per HTTP ankommende SOAP-Anfragen, ermittelt per Reflection die Ziel-Methode mit dem entsprechenden Attribut, ruft sie auf und sendet dem Client eine entsprechend codierte Antwort. In Listing 3 (oben) wurde bereits ein Beispiel gezeigt. • Auswertung durch den Compiler: In wenigen Ausnahmefällen kann bereits der Compiler Attribute im geparsten Quelltext berücksichtigen und dementsprechend anderen MSIL-Code erzeugen. Da zu dem Zeitpunkt noch keine erzeugte Assembly vorliegt kann das auch nicht immer per Reflection geschehen. Der C#-Compiler erkennt zum Beispiel Conditional-Attribute auf Methoden im Syntax-Baum. Unter bestimmten Bedingungen erzeugt er im Ergebnis die Methode nicht und entfernt auch alle Aufrufe im übrigen Code. Dieses Feature wird daher zum Konfigurationsmanagement verwendet, etwa um manche Konsolenausgaben nur in Debug-Builds zu erzeugen. 1 #define EIN_SYMBOL using System; using System.Diagnostics; 2 3 4 5 public class Blah { [Conditional("EIN_SYMBOL")] public static void Debug (string msg) { Console.WriteLine (msg); } } 6 7 8 9 10 11 12 13 Listing 6: Verwendung des Conditional-Attributs • Auswertung durch die IDE: Visual Studio analysiert eine Reihe von Attributen die den Debugger, den Code-Editor oder auch den Windows.Forms-Designer [4, S. 58] beeinflussen. • Durch Post-Prozessoren im Build-Skript: Schließlich gibt es noch die Möglichkeit Assemblies nachträglich zu verändern. Das passiert im Build-Prozess direkt nachdem der Compiler die Quelltexte übersetzt hat. Das ist der klassische Ansatz bei aspekt-orientierter Programmierung. Im .NET Framework 4.0 wurde dieser Ansatz verfolgt um die Regeln der neuen Code-Contracts umzusetzen. Listing 7 zeigt ein Beispiel-Programm mit einer definierten Invarianten-Zusicherung. class Test { private int x; private int y; 1 2 3 4 5 [ContractInvariantMethod] private void Invariant () { Contract.Invariant (y >= 0); } public int Y { get { return y; } set { y = value; } } public int Divide () { return x/y; } } 6 7 8 9 10 11 12 13 14 15 16 17 Listing 7: Code Contracts in .NET 4.0 Das Programm ccrewrite.exe (Bestandteil des Microsoft .NET Framework 4.0) findet das ContractInvariantMethodAttribut und verändert die Assembly direkt, d.h. durch Einfügen von MSIL-Instruktionen. Listing 8 zeigt den C#-Code, der logisch äquivalent zu den Veränderungen ist. Dabei wird nach jedem öffentlichen MemberZugriff, der den Zustand der Klasse potentiell verändert, die Invariante erneut geprüft. class Test { private int x; private int y; 1 2 3 4 5 [ContractInvariantMethod] private void Invariant () { Contract.Invariant (y >= 0); } public int Y { get { return y; } set { y = value; Invariant (); } } public int Divide () { var res = x/y; Invariant (); return res; } } 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Listing 8: Umgeschriebener Code der den Änderungen entspricht die ccrewrite.exe vornimmt. 3.4.3 Eigene Attribute definieren Alle Custom-Attribute sind als Klassen definiert, die von der Basis-Klasse System.Attribute abgeleitet sind. Per Konvention sollte der Name der Klasse ferner mit den Suffix Attribute enden. Als Beispiel betrachten wir die Definition eines Attributs, das im Rahmen von .NET Remoting eingesetzt wird [5, S. 226] : [AttributeUsage (AttributeTarget.Class) public class InterceptableAttribute : ContextAttribute { public InterceptableAttribute () : base ("C.I.") {} 1 2 3 4 5 public override bool IsContextOK (Context ctx, IConstructionCallMessage ctorMsg) { return (ctx.GetProperty ("Interception") != null); } [...] } 6 7 8 9 10 11 Listing 9: Eine Beispiel-Klasse für ein definiertes Attribut In diesem Beispiel wird ein InterceptableAttribute definiert, das von einer Klasse ContextAttribute abgeleitet ist, die letztlich selbst von System.Attribute erbt. Beachtenswert ist, dass das Attribute hier tatsächlich Programm-Code in Methoden enthält, nicht nur Eigenschaften. Ausführbar sind die Methoden aber nur, wenn per Reflection eine Instanz des Attributs abgerufen wurde. Das Verhalten des Attributs bzgl. bestimmter Aspekte kann gesteuert werden, indem auf der Klasse die das Attribut definiert ein AttributeUsage-Attribut gesetzt wird. So steuert die Eigenschaft Inherited, ob ein Attribut, das auf eine Klasse K angewendet wurde auch automatisch auf Klassen existiert, die von K abgeleitet sind. Ebenfalls steuerbar ist die Mehrfachanwendbarkeit auf dem selben Element (mittels der Eigenschaft AllowMultiple). Schließlich besitzt das AttributeUsage-Attribut noch eine Target-Eigenschaft: Oft ist ein Attribut nur auf bestimmten Sprachelementen sinnvoll. Entsprechend kann die Anwendbarkeit auf einige dieser Element-Arten beschränkt werden. 3.5 Reflection über Methoden-Code Obwohl .NET prinzipiell auch die Analyse von Programm-Code, d.h. von MSIL-Instruktionen unterstützt, ist dies ein eher selten genutztes Feature. Die Gründe sind darin zu suchen, dass der erzeugte Code vom Compiler abhängt und dem Programmierer also nicht unbedingt exakt bekannt ist. Außerdem ist der Vorgang komplex und oft auch einfach nicht sinnvoll anwendbar. Dennoch ist es gelegentlich hilfreich, beispielsweise für Werkzeuge zur statischen CodeAnalyse. Das angegebene Beispiel in Listing 10 prüft ob in einer Methode eine bestimmte andere Methode mindestens einmal aufgerufen wird. Bei Programmen, die redundanten Code enthalten, aber dennoch nicht automatisch generiert werden können, sind solche Tests manchmal sinnvoll. private MethodReference CheckForCall (MethodBody mbody, string callName) {1 foreach (Instruction instr in mbody.Instructions) { if (instr.OpCode.Name == "callvirt") { var methRef = instr.Operand as MethodReference; if (methRef != null && methRef.Name == callName) return methRef; } } return null; } 2 3 4 5 6 7 8 9 10 Listing 10: Eine Methode die prüft ob eine bestimmte Methode aufgerufen wird 3.6 Code-Erzeugung mit Reflection.Emit Prinzipiell gibt es mittlerweile im .NET Framework drei Optionen um zur Laufzeit Code zu erzeugen. Neben den sehr einfach zu benutzenden (LINQ) Expression Trees, und dem CodeDom, das Quellcode in verschiedenen .NET Sprachen erzeugen kann gibt es eine Reihe von Klassen, die sich im Namespace System.Reflection.Emit befinden. Diese Klassen sind die älteste Methode und gleichzeitig die flexibelste, allerdings auch die komplexeste. Um den Rahmen nicht zu sprengen wird hier nur ein Beispiel angegeben, das einen Teil einer Methode erzeugt. Eine umfangreiche Untersuchung der Codeerzeugung im Rahmen von .NET enthält das Buch Compiling for the .NET Common Language Runtime [3]. private void PushItemFromListFieldOnStack<T> (ILGenerator methodCode, FieldInfo moduleField, int index) { methodCode.Emit (OpCodes.Ldarg_0); // Push object! methodCode.Emit (OpCodes.Ldfld, moduleField); // Load module object from list ... methodCode.Emit (OpCodes.Ldc_I4, index); var listIndexer = typeof (List<T>).GetMethod ("get_Item"); methodCode.Emit (OpCodes.Callvirt, listIndexer); } Listing 11: Ein Beispiel zur dynamischen Codeerzeugung Code wie in Listing 11 findet sich im .NET Framework in der Klassenbibliothek. Beispielsweise erzeugt Windows Communication Foundation dynamisch Stub-Klassen aus Interfaces, ASP.NET generiert Glue-Code und Regular Expressions werden in MSIL übersetzt um die Ausführungsgeschwindigkeit zu steigern. 4 Zusammenfassung Mit .NET Reflection kann sich ein Programm selbst untersuchen oder verändern. Wir haben gesehen, dass dies ein mächtiges Werkzeug ist, das in vielen Fällen zu einfacher verständlicherem oder - im Fall der Code-Erzeugung - zu schnellerem Programmcode führt. Dieses Modell wurde kurz vorgestellt und in Bezug zum .NET Typ-Modell gesetzt. Ein weiteres .NET Feature, das stark auf Reflection angewiesen ist, die Attribute, wurde genauer untersucht. Wir haben festgestellt, dass Attribute eine zentrale Rolle für einige Funktionen spielen die von der Klassenbibliothek angeboten werden. Sie eignen sich auch gut um das Modell der aspekt-orientierten Programmierung (AOP) in .NET einzusetzen. Schließlich ist es noch möglich mit der Reflection API zur Laufzeit dynamisch Code zu erzeugen. Dazu sind aber Kenntnisse des Modells der .NET Virtuellen Maschine notwendig, insbesondere muss der Programmierer mit MSIL vertraut sein. Daher wurde das Thema nur kurz angeschnitten. Literatur [1] Don Box with Chris Sells, Essential .NET Volume 1. Addison Wesley, 2003. [2] Joseph Albahari & Ben Albahari, C# 4.0 in a Nutshell. O’Reilly, Fourth Edition, 2010. [3] John Gough, Compiling for the .NET Common Language Runtime. Prentice Hall, 2002. [4] Json Bock and Tom Barnaby, Applied .NET Attributes. APress, 2003. [5] M. Kuhrmann J. Calamé E. Horn, Verteilte Systeme mit .NET Remoting. Spektrum Akademischer Verlag, 1. Auflage, 2004. [6] Microsoft Developer Network (MSDN), MethodInfo Class. http://msdn.microsoft.com/enus/library/system.reflection.methodinfo.aspx , November 2010