Jedes Programm hat eine Problemdomäne zum Gegenstand. In einem Warenwirtschaftssystem umfasst diese Domäne beispielsweise "Artikel", "Kunden" und "Bestellungen". Metaprogramme wiederum sind spezielle Programme, deren Problemdomäne Computerprogramme bilden. Sie manipulieren explizit andere Programme oder deren Repräsentation (z.B. Compiler, virtuelle Maschinen oder Entwicklungsumgebungen). Reflektive Programme sind nun spezielle Metaprogramme, die in der Lage sind, sich selbst zu beobachten (
Introspection) und zu beeinflussen (
Intercession).
Die .NET-Plattform, auf der C# basiert, ist in mehreren Teilen wie u. A. dem
Common Type System (CTS) und der Common Language Infrastructure (CLI) spezifiziert. Bereits auf dieser Ebene wurde ein umfangreiches Metadatenkonzept integriert - was beispielsweise zur Ausführugnsverwaltung und Erstellung selbstbeschreibender Komponenten (
Assemblies) genutzt wird.
Dazu wird eine Implementierung einer Klasse durch den Compiler in eine von der Programmiersprache unabhängige Form umgewandelt, welche vom
Virtual Execution System (VES) ausgeführt werden kann. Diese Form wird als
Common Intermediate Language bezeichnet. Die CIL-Beschreibungen der Klassen werden in Assemblies gespeichert. Bei der Ausführung eines Programms werden diese vom VES in eine interne Datenstruktur geladen. Diese Datenstruktur ist implementierungsspezifisch.
Mittels der
.NET Reflection API kann während der Laufzeit auf Informationen der internen Repräsentationen zugegriffen werden (jedoch nur lesend). Wie das genau geschieht, soll in den folgenden Abschnitten erläutert werden:
- Auslesen von Typinformationen
- Objekte erzeugen und manipulieren
- Besonderheiten bei der Verwendung von Generika
- Selbst definierte Metadaten
- Typen dynamisch zur Laufzeit erzeugen
- Dynamische Methoden
Im Artikel habe ich versucht, einen umfassenden Überblick zu geben und (im Beispielcode) möglichst alle Möglichkeiten zumindest einmal anzuschneiden. Bei Fragen zur API bitte auch die verlinkten (

Doku-)Seiten durchlesen.
Bevor du nach dem Durchlesen aber gleich anfängst, deine Programme mit Reflection vollzupflastern, sei noch folgendes gesagt:
Reflection ist eine mächtige Waffe, aber bei unsachgemäßen Gebrauch kann man sich aber auch schnell in den eigenen Fuß schießen!
Achtung! |
Überlege dir, ob du im jeweiligen Fall wirklich Reflection benutzen musst!- Oft wird z.B. Reflection in normalen Businessklassen benutzt. Das ist aber fast nie angebracht. Reflection ist eher für Infrastrukturklassen gedacht - so z.B. für Serialisierung und das Auslesen von Attributen aus Businessklassen (z.B. Zwecks Darstellung in der GUI).
- Mittels Reflection kann auch auf nicht öffentliche Member zugegriffen werden. Dass man "normalerweise" auf diese Member NICHT zugreifen kann, hat aber immer auch einen Grund. Solltest du also keinen wirklich triftigen Grund haben, gegen diese Kapselung absichtlich zu verstoßen, dann lass es!
Durch nicht mehr durchführbare Compilerprüfungen steht man zudem vor folgenden Herausforderungen:- Typensicherheit gibt's nicht mehr.
- Um Lesbarkeiten muss man sich selbst kümmern.
- Performanz wird ein großes Thema.
- Es können einem viele und unverständliche Exceptions um die Ohren fliegen.
|
Bevor es richtig losgeht, möchte ich mich schon einmal zum einem bei herbivore und den Powerusern (für ihre Kritik und Anregungen beim Review des Artikels) und zum anderen bei Robert Wierschke (ohne den es bestimmte Teile des Artikels in dieser Form nicht geben würde) bedanken. :-)
Reflection-API im Detail
In der Einleitung wurden ja schon die "Infoobjekte" genannt, mit denen man auf Typinformationen (nur lesend!) zugreifen kann: Die Wurzel der gesamten Infoobjekt-Hierarchie ist die Klasse
Type. Diese stellt ein Infoobjekt für Typdeklarationen dar: Klassentypen, Schnittstellentypen, Arraytypen, Werttypen, Enumerationstypen, Typparameter, generische Typdefinitionen und offen oder geschlossen konstruierte generische Typen. Durch u.a. folgende Möglichkeiten kommt man an ein
Type-Objekt:
- Hat man ein Exemplar ("Instanz") einer Klasse: Mit der
GetType()-Methode.
- Ausgehend von einem Typen(namen) kann der
typeof-Operator verwendet werden.
- Wenn man den Namen des Typs kennt (also als Zeichenkette vorliegen hat), kann die Methode
Type.GetType("TypName") verwendet werden.
- Typen sind in Modulen bzw. Assemblies definiert, dementsprechend lassen sich die beinhalteten Typenobjekte auch über Methoden des
Module- bzw.
Assembly-Infoobjekts erzeugen.
C#-Code (Type-Objekt ermitteln): |
Type type1 = "abc".GetType();
Type type2 = typeof(String);
Type type3 = Type.GetType("System.String");
Assembly assembly = Assembly.Load(...);
Type[] types = assembly.GetTypes();
Type type4 = assembly.GetType("CustomType");
|
Auslesen von Typinformationen
Ausgehend von einem
Type-Objekt sind alle zu diesem Typ bzw. zur repräsentierten Klasse gehörenden Infoobjekte erreichbar. Die Reflection-API verwendet dabei eine einheitliche Nomenklatur:
- Infoobjekte werden durch Klassen beschrieben, deren Name mit Info endet, Beispielsweise
MethodInfo,
MemberInfo,
FieldInfo,
PropertyInfo,
ParameterInfo oder
ConstructorInfo.
- Zur Navigation zwischen den Infoobjekten gibt es Methoden der Form
Get***s(). Diese liefent alle entsprechenden Infoobjekte, beispielsweise
GetFields(),
GetMethods() oder
GetParameters().
(Hinweis: Bei der zurückgegebenen Auflistung kann man NICHT von einer bestimmten Sortierung ausgehen. Wenn man die gleiche Get***s()-Methode zweimal hintereinander aufruft, kann das Ergebnis unterschiedlich sortiert sein!)
- Get***("Name") liefert das Infoobjekt mit dem gegebenen Namen, beispielsweise
GetMethod("Main").
Mit Hilfe von so genannten
Bindungs-Flags können zusätzlich die durch die jeweilige
Get*-Methode zurückgelieferten Infoobjekte genauer spezifiziert werden. Dadurch ist es zudem auch möglich, an nicht öffentliche Member zu gelangen.
Hinweis! |
Bei der Verwendung von BindingFlags müssen immer sowohl Zugriffsmodifizier (BindingFlags.Public, BindingFlags.NonPublic, etc.) als auch BindingFlags.Static und/oder BindingFlags.Instance angeben werden! Sonst erhält man nur leere Listen bzw. null zurück. |
Mithilfe der jeweiligen Infoobjekte können nun bestimmte Informationen abgerufen werden, zum Beispiel:
C#-Code (Auslesen von Typinformationen): |
Type type = typeof(string);
Console.WriteLine(type.FullName);
Console.WriteLine(type.Attributes);
Console.WriteLine(type.Assembly.CodeBase);
Console.WriteLine("Vererbungsbaum:");
Type baseType = type.BaseType;
while (baseType!=null)
{
Console.WriteLine(baseType.FullName);
baseType = baseType.BaseType;
}
Console.WriteLine("Interfaces:");
foreach (Type interfaceType in type.GetInterfaces())
Console.WriteLine(interfaceType.FullName);
foreach (MemberInfo memberInfo in type.GetMembers(BindingFlags.Instance|BindingFlags.Static|BindingFlags.Public|BindingFlags.NonPublic))
{
if (memberInfo is ConstructorInfo)
{
ConstructorInfo constructorInfo = (ConstructorInfo)memberInfo;
Console.WriteLine("Konstruktor:");
Console.WriteLine(constructorInfo.Name);
Console.WriteLine(constructorInfo.Attributes);
foreach (ParameterInfo parameterInfo in constructorInfo.GetParameters())
Console.WriteLine(parameterInfo.Name+" ("+parameterInfo.ParameterType.FullName+")");
}
else if (memberInfo is FieldInfo)
{
FieldInfo fieldInfo = (FieldInfo)memberInfo;
Console.WriteLine("Feld:");
Console.WriteLine(fieldInfo.Name+" ("+fieldInfo.FieldType.FullName+")");
Console.WriteLine(fieldInfo.Attributes);
}
else if (memberInfo is MethodInfo)
{
MethodInfo methodInfo = (MethodInfo)memberInfo;
Console.WriteLine("Methode:");
Console.WriteLine(methodInfo.Name+" ("+methodInfo.ReturnType.FullName+")");
Console.WriteLine(methodInfo.Attributes);
foreach (ParameterInfo parameterInfo in methodInfo.GetParameters())
Console.WriteLine(parameterInfo.Name+" ("+parameterInfo.ParameterType.FullName+")");
}
else if (memberInfo is PropertyInfo)
{
PropertyInfo propertyInfo = (PropertyInfo)memberInfo;
Console.WriteLine("Eigenschaft:");
Console.WriteLine(propertyInfo.Name+" ("+propertyInfo.PropertyType.FullName+")");
Console.WriteLine(propertyInfo.Attributes);
if (propertyInfo.CanRead)
Console.WriteLine(propertyInfo.GetGetMethod(true).Name);
if (propertyInfo.CanWrite)
Console.WriteLine(propertyInfo.GetSetMethod(true).Name);
}
Console.WriteLine();
}
|
Objekte erzeugen und manipulieren
Grundlage zum Erzeugen neuer Objekte ist wieder ein
Type-Objekt. Hat man dieses, kann man mit der Methode
Activator.CreateInstance(...) ein neues Exemplar dieses Typs erzeugen.
C#-Code (Objekte anhand von Typinformationen erzeugen): |
Uri uri = (Uri)Activator.CreateInstance(typeof(Uri),"http:
StringBuilder stringBuilder = Activator.CreateInstance<StringBuilder>();
|
Tipp für "Profis": Man kann auch neue Exemplare erzeugen, OHNE dabei den Konstruktor aufzurufen (hilfreich z.B. bei Deserialisierungen):
FormatterServices.GetUninitializedObject(...).
Über die entsprechenden Infoobjekte können Objekte inspiziert und auch manipuliert werden. Da ein Infoobjekt nicht einem speziellen Objekt zugeordnet ist, muss hierbei immer eine Referenz auf das jeweilige Objekt übergeben werden (sofern der Member nicht statisch ist). Um beispielsweise ein Feld eines Objekts zu ändern, muss das entsprechende
FieldInfo ermittelt werden und die Methode
GetValue aufgerufen werden. Das Schreiben eines Feldes geschieht analog mittels
SetValue. Ebenso bei
PropertyInfos, wobei man aber hier beachten muss, dass ein Property nicht immer schreib- oder lesbar ist.
Das Aufrufen einer Methode gelingt über das entsprechende
MethodInfo unter Angabe der Objektreferenz (falls nicht statisch) und ggf. der Parameter des Methodenaufrufs mithilfe der Methode
Invoke.
(
Hinweis: Beim Aufruf/Lesen/Schreiben von statischen Membern übergibt man statt einem existierenden Exemplar einfach null.)
C#-Code (Objekte anhand von Typinformationen manipulieren): |
private class Test
{
public int Field;
private static string property { get; set; }
public void Method() { Console.WriteLine("Hello!"); }
public int Method(int value) { return value; }
}
[...]
Test test = new Test();
Type type = test.GetType();
FieldInfo fieldInfo = type.GetField("Field");
fieldInfo.SetValue(test,42);
Console.WriteLine(fieldInfo.GetValue(test));
PropertyInfo propertyInfo = type.GetProperty("property",BindingFlags.NonPublic|BindingFlags.Static);
propertyInfo.SetValue(null,"23",null);
Console.WriteLine(propertyInfo.GetValue(null,null));
MethodInfo methodInfo1 = type.GetMethod("Method",new Type[]{});
methodInfo1.Invoke(test,null);
MethodInfo methodInfo2 = type.GetMethod("Method",new[] { typeof (int) });
Console.WriteLine(methodInfo2.Invoke(test,new object[]{ 123 }));
|
Achtung! |
Kapselung hat seinen Grund! Solltest du wirklich auf NICHT öffentliche Member zugreifen wollen, solltest du dafür einen noch wichtigeren Grund haben! |
Achtung! |
Nicht auf die Idee kommen, als Funktionszeigerersatz den Namen einer Methode oder ein MethodInfo zu übergeben! Dafür gibt es Delegates. |
Besonderheiten bei der Verwendung von Generika
Mit Generika kann bei der Struktur und dem Verhalten eines Typs von bestimmten Datentypen abstrahiert werden. Zum Beispiel sind Zugriffsmethoden von Listen auf einen konkreten Datentypen angepasst. Durch Verwendung von generischen Typen bzw. Methoden kann eine allgemeine Implementierung realisiert werden, die erst später auf einen bestimmten Datentypen angepasst wird.
Die generischen Typargumente können für einen Typen oder eine Methode über die Methode
GetGenericArguments() des entsprechenden Infoobjekts ermittelt werden. Ebenso haben die Infoobjekte Methoden, um zu ermitteln, ob generische Typargumente verwendet wurden (z.B.
IsGenericParameter,
IsGenericType oder
IsGenericTypeDefinition).
Mit der Methode
GetGenericTypeDefinition() ist es auch möglich, aus einem Typen mit generischen Typargumenten wieder an einen generischen Typen zu gelangen. Ebenso erlaubt der
typeof-Operator, dass man erst gar kein generisches Typargument angibt.
Beim Aufrufen von generischen Methoden muss zudem beachtet werden, dass zunächst der generische Typparameter festgelegt werden muss, bevor die Methode aufgerufen werden kann.
C#-Code (Generika und Typinformationen): |
public class Test<T>
{
public T Field;
public G Method<G>(G param) { return param; }
}
[...]
Type type1 = typeof(Test<string>);
Console.WriteLine(type1.GetGenericArguments()[0].FullName);
Console.WriteLine(type1.GetField("Field").FieldType);
Type type2 = type1.GetGenericTypeDefinition();
Type type3 = typeof(Test<>);
Test<int> test = new Test<int>();
MethodInfo methodInfo = test.GetType().GetMethod("Method");
methodInfo = methodInfo.MakeGenericMethod(typeof(int));
Console.WriteLine(methodInfo.Invoke(test,new object[] { 123 }));
|
Selbst definierte Metadaten
Wie oben gezeigt, werden in .NET Metadaten zur Beschreibung von Typen verwendet. Es ist möglich, diese Beschreibung durch weitere Metadaten zu ergänzen. Solche Metadaten werden in .NET als
Attribute bezeichnet. Die .NET Basisklassenbibliothek verfügt bereits über eine Anzahl solcher Attribute (z.B.:
SerialalizableAttribute,
WebServiceAttribute,
XmlElementAttribute-Klasse), erlaubt aber auch die Definition eigener Attribute.
Attribute können mit der Methode
GetCustomAttributes(...) des entsprechenden Infoobjekts ermittelt werden, wobei Attribute vom
Type-Objekt, über die ganzen
MemberInfos bis hin zum
ParameterInfo ausgelesen werden können.
C#-Code (Auslesen von Attributen): |
public class Foo
{
[XmlElement("foo")]
public string Bar { get; set; }
[XmlElement("bar")]
public string Baz { get; set; }
}
[...]
foreach (PropertyInfo propertyInfo in typeof(Foo).GetProperties())
{
var xmlElementAttribute = (XmlElementAttribute) propertyInfo.GetCustomAttributes(typeof(XmlElementAttribute), false).SingleOrDefault();
if (xmlElementAttribute!=null)
{
string name = xmlElementAttribute.ElementName;
}
}
|
Weiterführende Links dazu:
.
Typen dynamisch zur Laufzeit erzeugen
Bisher wurde gezeigt, wie die Struktur von Objekten zur Laufzeit ausgelesen werden kann und wie man Objekte manipuliert. Diese Manipulationen bezogen sich aber nur auf das Ändern des Zustandes eines bereits bestehenden Objektes. Dessen Struktur und Verhalten zu verändern ist mit diesen Mitteln jedoch nicht möglich. Dynamische Programmiersprachen wie Smalltalk oder Ruby erlauben zum Beispiel das Hinzufügen und Löschen von Methoden oder das Ändern derer Implementierungen. Da C# ein statisches Typsystem besitzt, ist dies hier nicht möglich – was aber in vielen Fällen dadurch kompensiert werden kann, dass man neue Typen dynamisch zur Laufzeit erstellt.
Das .NET-Framework bietet zwei verschiedene Alternativen, um eigene Typen zur Laufzeit zu erstellen:
- Das Erzeugen einer Assembly durch Kompilieren von Quelltext mithilfe einer
ICodeCompiler-Implementierung für eine konkrete .NET-Sprache, die den benutzerdefinierten Typ enthält. Aber das ist was für Anfänger, wir nehmen Methode 2:
- Mit Hilfe von Funktionalitäten aus dem Namensraum
System.Reflection.Emit wird MSIL-Code direkt erzeugt. Dabei werden assemblerähnliche MSIL-Befehle zusammengestellt und mit Metainformationen kombiniert.
System.Reflection.Emit
Ein wesentlicher Bestandteil des System.Reflection.Emit-Namensraums sind verschiedene Builderobjekte. Diese dienen der Konstruktion der verschiedenen Komponenten und können auch als Referenz auf jene benutzt werden. Die Benutzung als Referenz wird später erläutert, zunächst soll die Konstruktion näher betrachtet werden:
Als Grundlage für die dynamische Typerzeugung wird ein
TypeBuilder-Objekt benötigt. Typen sind laut CLI in Modulen enthalten, welche sich wiederum in einer Assembly befinden. Daher muss zunächst ein
AssemblyBuilder-Objekt erstellt werden, mit Hilfe dessen dann ein
ModuleBuilder-Objekt erzeugt werden kann.
Innerhalb des vom
ModuleBuilder-Objekt repräsentierten Moduls kann anschließend der zu erzeugende Typ erstellt werden. Als Parameter können die grundlegenden Merkmale des neuen Typs angegeben werden – u. A. der Name, Zugriffsmodifizierer oder eine Basisklasse.
C#-Code (Builder-Objekte erstellen): |
AssemblyName assemblyName = new AssemblyName("AssemblyName");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName,AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("ModuleName");
TypeBuilder typeBuilder = moduleBuilder.DefineType("TypeName",TypeAttributes.Public|TypeAttributes.Class);
|
Mithilfe des
TypeBuilders können nun weitere Builder erstellt werden, so z.B.
FieldBuilder,
PropertyBuilder,
MethodBuilder oder
ConstructorBuilder.
C#-Code (Mittels TypeBuilder dynamisch Typen erstellen): |
public interface Interface
{
string One { get; set; }
int Two { get; set; }
}
[...]
typeBuilder.AddInterfaceImplementation(typeof(Interface));
foreach (PropertyInfo propertyInfo in typeof(Interface).GetProperties())
{
PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyInfo.Name,PropertyAttributes.HasDefault,propertyInfo.PropertyType,null);
FieldBuilder fieldBuilder = typeBuilder.DefineField(propertyInfo.Name,propertyInfo.PropertyType,FieldAttributes.Private);
MethodAttributes methodAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.Virtual | MethodAttributes.HideBySig;
MethodBuilder setMethodBuilder = typeBuilder.DefineMethod("set_"+propertyInfo.Name,methodAttributes,null,new Type[] { propertyInfo.PropertyType });
typeBuilder.DefineMethodOverride(setMethodBuilder,propertyInfo.GetSetMethod());
[...]
propertyBuilder.SetSetMethod(setMethodBuilder);
MethodBuilder getMethodBuilder = typeBuilder.DefineMethod("get_"+propertyInfo.Name,methodAttributes,propertyInfo.PropertyType,null);
typeBuilder.DefineMethodOverride(getMethodBuilder,propertyInfo.GetGetMethod());
[...]
propertyBuilder.SetGetMethod(getMethodBuilder);
}
|
Bis jetzt haben wir die Methode aber erstmal nur deklariert – der interessante Teil, das zugehörige Verhalten zu implementieren, fehlt noch: Dafür muss der für die Implementierung benötigte MSIL-Code muss zusammengestellt werden. Zu diesem Zweck besitzt das
MethodBuilder-Objekt ein
ILGenerator-Objekt. Durch einen Aufruf der Methode
Emit(...) wird der als Parameter übergebene MSIL-Befehl an die Liste der bereits zusammengestellten Befehle der Methodenimplementierung angehängt. Ein MSIL-Befehl wird dabei durch ein Element der Auflistung
System.Reflection.Emit.OpCodes repräsentiert.
Für bestimmte Befehle werden jedoch auch Metadaten als zusätzlicher Parameter benötigt – so zum Beispiel bei Verweisen (Methodenaufrufe, Feldzugriffe, etc.). Dabei kann entweder ein Builderobjekt verwendet werden – sofern dies vorhanden ist – oder aber ein entsprechendes Infoobjekt, das mittels Reflection ermittelt werden kann.
C#-Code (Mithilfe des ILGenerators einen Methodenrumpf implementieren): |
ILGenerator ilGenerator = methodBuilder.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Stfld,fieldBuilder);
ilGenerator.Emit(OpCodes.Ldarg_1);
MethodInfo mi = typeof(AType).GetMethod("SayHello");
ilGenerator.Emit(OpCodes.Callvirt,mi);
ilGenerator.Emit(OpCodes.Pop);
ilGenerator.Emit(OpCodes.Ret);
|
Der konkrete Typ kann schließlich durch einen Aufruf der Methode
typeBuilder.CreateType() erzeugt werden, welche ein
Type-Objekt zurück gibt.
Achtung! |
Bei diesem Verfahren, Typen dynamisch zur Laufzeit zu erzeugen, muss jedoch beachtet werden, dass wenige Überprüfungen der generierten MSIL-Befehle stattfinden. Es wird zwar geprüft, ob alle Methoden eines Interfaces auch wirklich implementiert wurden, für die eigentlichen Implementierungen können MSIL-Befehle aber beliebig zusammengestellt werden. Fehlerhafte MSIL-Codestücke werden dadurch erst bei der Ausführung der generierten Methode(!) bemerkt. |
Dynamische Methoden
Mit der
DynamicMethod-Klasse kann eine Methode zur Laufzeit generiert und ausführt werden, ohne eine dynamische Assembly oder einen dynamischen Typ erstellen zu müssen, die bzw. der die Methode enthält.
Die Methode wird dann genau wie oben beschrieben implementiert; zum Aufrufen der Methode erhält man dann ein Delegate zurück.
C#-Code (Dynamische Methoden erstellen): |
public delegate string MethodDelegate(int param);
[...]
DynamicMethod dynamicMethod = new DynamicMethod("Method",typeof(string),new Type[] { typeof(int) });
ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
[...]
MethodDelegate methodDelegate = (MethodDelegate)dynamicMethod.CreateDelegate(typeof(MethodDelegate));
string test = methodDelegate(123);
|
weiterführende Links dazu: