C# Seminar: Reflection im .NET Framework

Transcription

C# Seminar: Reflection im .NET Framework
C# Seminar: Reflection im .NET Framework
Johannes Roith
[email protected]
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