Reflections in C#

Es kann zur Laufzeit eines Programms notwendig sein, dass das Programm sich selbst analysieren und ggf. verändern kann. In C# kann dies mir der Funktionalität von Reflections erreicht werden.

Reflection „reflektiert” zur Laufzeit aufrufbare Assemblys, Module und Typen und bietet so dynamischen Zugriff auf Objekte, Methoden und Metadaten, welche erst zur Laufzeit bekannt werden.
Eine Programmiersprache, welche diese Möglichkeit zur “Reflektion” der eigenen Programminhalte hat, kann ihre eigene Struktur selbst zur Laufzeit analysieren und auf sie Einfluss nehmen.

Reflection als Methode zum dynamischen Aufruf von Klassen, Methoden, Variablen, Ressourcen usw. ist abgesehen von C# (und anderen .NET-Programmiersprachen) auch in Java bekannt und in noch einigen weiteren Programmiersprachen in ähnlicher Form verbreitet. Aber auch die Reflection-Funktionalität von Java und C# sind nicht absolut identisch. Während Java auf Klassenebene „reflektiert” und generell auch Klassen als Informationsempfänger verwendet, kommt C# nicht über Klassen, sondern über Assemblies an die gewünschten Informationen heran.

Grundsätzlich sollte der Einsatz von Reflections vermieden werden, denn der dynamische Aufruf ist wesentlich langsamer, als der direkte Aufruf. Insgesamt ist der Einsatz dieser Funktionalität mit übermäßigen Performance-Einbußen verbunden. Mit der Anwendung von Reflections erkauft sich der Programmierer Flexibilität zur Laufzeit, nimmt damit aber Leistungseinbußen in Kauf.
Nur wenn ein dynamischer Aufruf unbedingt notwendig ist, gibt es für Reflections grundsätzlich keine Alternative. Beispiele für Anwendungsgebiete sind Schnittstellen für Plugins, der Debugger und auch die automatische Textvervollständigung in Entwicklungsumgebungen (z. B. Microsofts IntelliSense).

Um Reflections in C# nutzen zu können, sind Klassen aus dem Namespace System.Reflection notwendig, welcher daher in das Projekt eingebunden werden muss.

[csharp] using System.Reflection;
[/csharp]

Auch die Klasse System.Type ist für Reflections von ausschlaggebender Bedeutung. Sie steht im Namespace System zur Verfügung.

System.Type repräsentiert u.a. Klassen, Interfaces und Wertetypen.

Um das Objekt eines Typen nach System.Type zu erhalten, dienen der typeof() Operator bzw. zur Laufzeit eines Objekts die Instanzmethode [object].GetType() als Typlieferanten. (Siehe hierzu typeof() vs getType())

Das Type-Objekt stellt wichtige Informationen und Zugriffsmöglichkeiten, u.a. auf Methoden, Konstruktoren und Variablen bereit.

Von Interesse könnte für den Programmier beispielweise sein, wie die eigene Assembly heißt. Der Name der Assembly wird mit der Methode myAssemblyInfo() ausgegeben.

[csharp] private void myAssemblyInfo()
{
Assembly myAssembly = System.Reflection.Assembly.GetExecutingAssembly();

Console.Writeline(myAssembly.GetName().FullName);
}
[/csharp]

Diese Methode kann in der neuen Klasse (z.B. im Konstruktor) aufgerufen werden.

[csharp]myAssemblyInfo();[/csharp]

Ausgabe:

ReflectionExample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Ausgegeben wurden alle Informationen über den Namen dieser Assembly (Fullname). Die Assembly selbst heißt ReflectionExample (dies ist der Name des Projekts).

Reflections in C# an Hand eines Beispiels:

Die nachfolgende Klasse Calculator im Namensraum Reflection_Test soll mit Reflections analysiert werden.

Die Klasse Calculator hat eine Konstante und Eigenschaft (Property) sowie einige Methoden.

[csharp] using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Windows.Forms;
using System.Reflection;

namespace Reflection_Test
{
class Calculator
{
private const float pi = 3.14f;

public float PI
{
get
{
return pi;
}
}

public Calculator()
{
}

public int DivideAndWrite(double d)
{
double result = d / 2;

return (int)result;
}

public void WriteText(string text)
{
Console.WriteLine(text);
}

private void WriteText2(string text) // invisible, not able to reflect
{
Console.WriteLine(text);
}

public void doThis()
{
// do anything possible…

Console.WriteLine(“We do this!”);
}

public void doThat()
{
// do anything (else) possible

Console.WriteLine(“We do that!”);
}
}
}
[/csharp]

Dazu wird eine neue Klasse aufgestellt, in welche je nach Bedarf nachfolgenden Methoden implementiert und aufgerufen werden können.

Wissenswert ist auch, welche Datei (und in welchem Pfad sie liegt) zu der Klasse Reflection_Class gehört.
Die Methode myAssemblyFiles() gibt alle Dateien namentlich aus, welche zu dieser Klasse gehören.

[csharp] private void myAssemblyFiles()
{
Assembly asm = Assembly.GetAssembly(typeof(Reflection_Class));

FileStream[] files = asm.GetFiles();

foreach (FileStream file in files)
{
Console.WriteLine(“Filename: ” + file.Name);
}
}
[/csharp]

Nach Aufruf der Methode myAssembyFiles() in der neuen Klasse (z.B. im Konstruktor)…
[csharp] myAssemblyFiles();
[/csharp]

erfolgt folgende Ausgabe:

Filename: C:\Dokument…daten\Temporary Projects\Reflection\bin\Debug\Reflection.exe

Die Ausgabe zeigt den Namen der ausführbaren Datei mit absoluter Pfadangabe.

Eine weitere Methode namens getMetaDataByTypeName() bekommt einen Typennamen (Name einer Klasse) als Parameter und gibt die Namen aller Methoden aus.

[csharp] private void getMetaDataByTypename(string typeName)
{
Type ctype = Type.GetType(typeName);

MethodInfo[] mis = ctype.GetMethods();

foreach (MethodInfo mi in mis)
{
Console.WriteLine(“Methodename: ‘” + mi.Name.ToString() + “‘”);
}
}
[/csharp]

Diese Methode kann wie folgt aufgerufen werden:

[csharp] this.getMetaDataByTypeName(“Reflection_Test.Calculator”);
[/csharp]

Ausgabe:

Methodename: ‘get_PI’
Methodename: ‘DivideAndWrite’
Methodename: ‘WriteText’
Methodename: ‘doThis’
Methodename: ‘doThat’
Methodename: ‘GetType’
Methodename: ‘ToString’
Methodename: ‘Equals’
Methodename: ‘GetHashCode’

Zu beachten ist hier, dass die private Methode writeText2() nicht gefunden wurde. Die get()-Methode der Eigenschaft PI wurde mitgezählt.

Eine andere Methode reflectProperty() greift auf den Wert der Eigenschaft PI zu und zeigt diesen Wert an.
Dabei wird vor dem Zugriff mit der Eigenschaft CanRead der Instanz vom Typ PropertyInfo auch geprüft, ob die Eigenschaft PI Lesezugriff bietet.

Die Methode reflectProperty() verlang hierzu die Klasse als Parameter, welche die Eigenschaft PI hat.

[csharp] private void reflectProperty(object rClass)
{
PropertyInfo propi = rClass.GetType().GetProperty(“PI”, typeof(float));

if (null != propi && propi.CanRead)
{
Console.WriteLine(“PI: ” + propi.GetValue(rClass, new object[] { }));
}
}
[/csharp]

Aufgerufen wird diese Methode beispielsweise mit einer flüchtigen Instanz der Klasse Calculator.

[csharp] reflectProperty(new Reflection_Test.Calculator());
[/csharp]

Ausgabe:

PI: 3,14

Reflektierte Methoden aufrufen

Häufiger Wunsch ist es, nicht nur Metadaten einer Assembly abzufragen, sondern auch bestimmte Methoden aufzurufen.

Dies geschieht mit der Instanzmethode des Objekts vom Typ MethodInfo.
Die Methode reflectAndInvokeMethode() sucht nach Methoden nach ihren Namen und ruft sie nacheinander auf.

[csharp] private void reflectAndInvokeMethod(object rClass, string[] methodnames)
{
foreach (string methodname in methodnames)
{
MethodInfo mi = rClass.GetType().GetMethod(methodname);

if (null != mi)
{
mi.Invoke(rClass, new Object[] { });
}
}
}
[/csharp]

Aufgerufen wird die Methode beispielsweise wie folgt, dabei wird ihr eine flüchtige Instanz der Klasse Calculator sowie zwei Namen von Methoden übergeben.

[csharp] reflectAndInvokeMethod(new Reflection_Test.Calculator(), new String[] { “doThis”, “doThat” });
[/csharp]

Ausgabe:

We do this!
We do that!

Aufruf einer reflektierten Methode mit Rückgabewert

Eine “reflektierte” Methode liefert ihren Rückgabewert direkt von der Invoke-Methode zurück und kann einer Variable zugewiesen werden.

[csharp] Type ctype = typeof(Reflection_Test.Calculator);

MethodInfo[] mi3 = ctype.GetMethods();

foreach (MethodInfo mi in mi3)
{
Reflection_Test.Calculator Calculator = new Reflection_Test.Calculator();

if (mi.Name.Equals(“DivideAndWrite”))
{

// Rückgabewert von reflektierter Methode empfangen
double returnvalue = Convert.ToDouble(mi.Invoke(Calculator, new Object[] { 5.0 }));

Console.WriteLine(“Value: ” + returnvalue.ToString());
}
}
[/csharp]

Ausgabe:

Value: 2