Spiegelzauber (Teil 1)

Das .NET-Instrument der Code-Reflection ist beileibe keine Erfindung von Microsoft und in verwalteten Umgebunden, wie .NET nunmal eine ist, auch technisch nicht hoch-innovativ. Trotzdem wissen viele .NET-Entwickler noch immer nicht genau, um was es geht. codingfreaks hilft.

Einführung

Was genau bedeutet Reflection eigentlich. Man könnte sagen, dass der Begriff direkt aus der Psychologie entlehnt wurde, in der es den Terminus der Selbst-Reflektion gibt. Damit ist im Groben gemeint, dass ein Wesen die Möglichkeit hat (oder eben auch nicht) Gedanken über sich selbst anzustellen. Dieses Paradigma wurde nun konsequent auf die Welt der Programmierung übertragen und dort Reflection genannt.

CLRReflection funktioniert erst dann richtig gut, wenn Anwendungen in verwalteten Umgebungen ablaufen. Was heißt aber verwaltet, oder englisch „managed“ eigentlich. Dieser Begriff soll lediglich deutlich machen, dass zwischen dem Betriebssystem als eigentliche native Plattform und der jewieligen Anwendung eine Abstraktions-Schicht angesiedelt ist, im .NET-Fall die CLR (common language runtime).Der Begriff „Betriebssystem“ ist eigentlich gar nicht exakt genug, denn genau genommen ist z.B. Windows selbst nur eine verwaltete Umgebung für den Zugriff auf Hardware, aber das führt hjier zu weit.

Verwaltete Umgebungen können mehr oder weniger komplex implementiert sein. Weniger komplexe Beispiele können Python oder Perl sein – also ehere skript-artige Interpreter-Umgebungen, komplexere Vertreter sind da schon Java oder eben .NET.

Die CLR ist die eigentliche Ausführungsebene für den von uns geschriebenen Code. Der folgende Aufruf verdeutlicht dies:

MessageBox.Show("Hello World!");

Dieser oft gesehene Code führt ja bekanntlich zur Anzeige eines Windows-typischen Meldungs-Dialoges. Wohl gemerkt – „Windows-typisch“. D.h., ohne Neukompilierung unseres .NET-Programmes erscheint unter Windows XP der XP-Dialog, unter Vista der Vista-Dialog usw. Das ist einer der großen Vorteile von verwalteten Umgebungen. Sie abstrahieren halt gegenüber dem Programm die harte Realität. Und wie sähe die in unserem Beispiel aus? Ganz einfach: Eine ganze Reihe von API-Aufrufen an Shell32.dll, das Anhängen an die Ereignisse des Forms, die Zeichenmethoden usw. werden durch die CLR aufgerufen und führen zum gewünschten Ergebnis. Das Tolle: Seit dem es eine verwaltete Umgebung auch für Linux gibt (Mono, span. für Affe), funktioniert das Ganze sogar weitgehend unter Linux. Das Diagramm rechts soll dies verdeutlichen.

Reflection im Kontext der CLR

Ausgehend von den vorangegangenen Erläuterungen lässt sich das Konzept der Reflection in .NET relativ leicht erklären. Nachdem eine Assembly (also ein .NET-Anwendungspaket) in die CLR-Ausführungsschicht geladen wurde, kann diese ihr eine Art Spiegel vorhalten. Die Assembly fragt etwas wie: „Wie heiße ich?“ und die CLR antwortet: „Du heißt Assembly1.“. Oder auch „Wieviele öffentliche Klassen sind in mir enthalten?“, Antwort: „12.“. Die Liste an Beispielen ist unendlich und wir kommen im Verlauf dieses Howto’s noch auf sinnvolle Einsatzgebiete zu sprechen.

Wie geht das denn nun?

Um niemanden zu lange auf die Folter zu spannen, hier schon einmal ein kleines Beispiel.

Wir starten mit einer Konsolen-Anwendung. Dieser Anwendung fügen wir eine Klasse hinzu. Der Einfachheit halber benutzen wir das gängige Beispiel einer Person-Klasse. Im Haupthread der Anwendung (Main) erstellen wir eine Instanz dieser Klasse. Erste Informationen können wir nun schon per Reflection einholen:

using System;
using System.Reflection;

namespace ReflectionTest01
{
    class Program
    {
        static void Main(string[] args)
        {
            Type typPer = typeof(Person);
            Console.WriteLine("Der Typ {0} verfügt über {1} Eigenschaften",
				typPer.Name,
				typPer.GetProperties().Length);
            Console.ReadKey();
        }
    }
}

Das Ergebnis wird die folgende Ausgabe auf der Konsole sein:

Der Typ Person verfügt über 3 Eigenschaften
[/chsarp]

Man muss teilweise zweimal über die Implikationen, die sich aus  diesen Möglichkeiten ergeben, nachdenken. Ich erstelle also ein Programm  und kann während der Programmierung auf die Informationen  zurückgreifen, die ich eben erst programmiert habe. Schon cool!

Doch was zeigt uns das Sample eigentlich genau?
<ol>
	<li>Der Namespace <span style="font-family: courier new,courier;">System.Reflection</span> ist der Schlüssel und sollte eingebunden werden, wenn man mit Reflection arbeitet.</li>
	<li>Der .NET-Datentyp <span style="font-family: courier new,courier;">System.Type </span>spielt offensichtlich eine große Rolle.</li>
</ol>
Zu Aussage Nummer 1 gibt es nicht viel hinzuzufügen. Man tut es halt einfach. <span style="font-family: courier new,courier;">Type </span>ist da schon ein anderes Kaliber. Vielen kennen diesen Objekttyp aus Zeilen wie:

if (sender is typeof(ListView))
{
    MessageBox.Show("Sie haben ein ListView angeklickt!");
}

und haben beileibe noch nie über die tiefere Bedeutung nachgedacht.

Der Befehl typeof(IrgendeinNETTypName) gibt eine Instanz der Klasse System.Type zurück. Dieses Type ist eine der .NET-Klassen mit den meisten Methoden und Eigenschaften überhaupt. Es ist ein reines Info-Monster. Um unserem Beispiel die Krone aufzusetzen, werden wir nun einmal herausbekommen, wie groß das Type-Monster genau ist:

private static void GetTypeInfo()
{
Console.WriteLine("\n\nInformationen zum Typ System.Type\n\n");
Type typPer = typeof(Type);
Console.WriteLine(@"Eigenschaften:   {0}", typPer.GetProperties().Length);
Console.WriteLine(@"Methoden:        {0}", typPer.GetMethods().Length);

int intStatic = 0;
foreach(MethodInfo minfTmp in typPer.GetMethods())
{
if (minfTmp.IsStatic)
intStatic++;
}
Console.WriteLine(@" davon statisch: {0}", intStatic);
}

Das Ergebnis wird unter .NET Framework 3.5 folgendes sein:

Eigenschaften:   58
Methoden:        146
 davon statisch: 17

Das ist schon sehr viel nützlicher, als das Beispiel vorher. Warum? Sehr einfach: Der Objektbrowser von Visual Studio selbst kann uns diese Information nicht liefern. Mit den Mitteln der Reflection können wir uns nun aber ein eigenes kleines Tool schreiben, dass diese Aufgabe übernimmt. Übrigens: Genau mit dieser Technik wurde das sehr erfolgreiche Tool .NET Reflector geschrieben.

Das zweite Beispiel hat uns bereits etwas tiefer in die Geheimnisse von Reflection eingeweiht. Es gibt also innerhalb von System.Reflection noch Spezial-Typen, wie MethodInfo. Diese Klassen existieren logischerweise auch für Properties, Events usw.

Randbemerkungen für Fortgeschrittene (gerne überspringen)

An dieser Stelle sei mir der Hinweis erlaubt, dass Microsoft hier ein Beispiel dafür bringt, wie es seine eigenen Designrichtlinien missachten kann. Der Zugriff auf eine Auflistung von Elementen soll nämlich eigentlich über Eigenschaften und nicht Methoden erfolgen. Man würde also intuitiv eine Aufruf-Synthax wie folgende erwarten:

foreach(MethodInfo minfTmp in typPer.Methods)

Dies würde nämlich auch die inzwischen heiß geliebten delegate-basierten Nutzungen erlauben, z.B.:

foreach(MethodInfo minfTmp in typPer.Methods.FindAll(delegate MethodInfo minfSearch
						     {
							return minfSearch.IsStatic;
						     })

Der Weg zu LINQ ist von hier aus dann auch nicht mehr weit. Hoffentlich rüstet Microsoft System.Type entsprechend um.

Benutzerdefinierte Attribute

Das zugegeben sehr simple Eingangsbeispiel hat bereits einige ziemlich beeindruckende Features von Reflection offenbart. Richtig effektiv wird es aber unter Zuhilfenahme von Attributen. Es gibt bereits viele durchaus nicht unbekannte Produkte, die heftigen Gebrauch von Attributen machen. Überhaupt ist die sog. deklarative Programmierung, in der nicht lediglich imperativ Befehl für Befehl angegeben wird, sondern zusätzlich Metadaten eine Funktion übernehmen können derzeit stark im kommen.

Viele Attribute liefert bereits .NET selbst mit. Dieses hier wird jeder kennen, der schon etwas länger mit .NET gearbeitet hat:

[Serializable]
public class Settings
{
    ///
}

Hier ist [Serializable] das Attribut, das mehr über die Klasse Settings aussagt. In diesem Fall markiert das Attribut als serialisierbar. Andere Klassen des .NET Framework werten dieses Attribut per Reflection aus, um zu bestimmen, ob Instanzen eines Typs in irgendeiner Form in Streams überführt werden können. Attribute können in beliebiger Anzahl kombiniert werden:

[Serializable,
 XmlElement("einstellungen")]
public class Settings
{
    ///
}

Ein zweites Attribut wird einfach durch ein Komma getrennt angehängt.

Was genau sind aber Attribute? Ganz einfach: Attribute sind Klassen, die von der Basisklasse System.Attribute abgeleitet sind.

Mit anderen Worten: Nichts steht dem Erstellen eigener Attribute im Wege!

Die in den Beispielen gezeigten Attribute Serializable und XmlElement sind in Wahrheit Klassen: System.SerializableAttribute und System.Xml.Serialization.XmlElementAttribute. Schnell wird hier ein Benennungsschema deutlich. Die Klasse heißt IrgendwasAttribute, das Attribut heißt Irgendwas. Beide Beispiel-Attribute sind direkt von System.Attribute abgeleitet.

Eigene Attribute erstellen

Gestandene .NET-Entwickler werden anhand der Überschrift und ihrer einschlägigen Erfahrungen bereits tief durchatmen, aber keine Angst! Attribute zu erstellen ist Kinderkram. Man überlegt sich, ob ein Attribut Daten halten muss oder nicht. Ein Beispiel:

Wir wollen ein Attribut schaffen, dass es uns ermöglicht, später mit einem anderen Programm auszulesen, welche Klassen bereits fertig programmiert sind und welche nicht. Sowas schreit nach einem sog. Tag. D.h., man markiert eine Klasse mit etwas. Genauso macht es Microsoft mit System.Serializable. Etwas ist serialisierbar (hat das Attribut) oder nicht (hat kein Attribut Serializable). Fertig! Also los:

  • Wir fügen unserem Projekt eine neue Klasse ComponentReadyAttribute hinzu.
  • Wir leiten die Klasse von Attribute ab.
  • Wir geben per Attribut (lol) an, auf welche Typen von Programmier-Konstrukten das Attribut angewendet werden darf.

Der Code des Beispiels:

[AttributeUsage(AttributeTargets.Class)]
public class ComponentReadyAttribute : Attribute
{
}

Das ist das fertige Attribut! Mit dem Attribut AttributeUsage geben wir an, dass unser Attribut nur über einer Klassendeklaration stehen darf. Prinzipiell dürfen Attribute über jedem Programmier-Konstrukt (Methoden, Ereignis-Deklarationen, Enumerationen usw.) platziert werden.

Als letztes benutzen wir das neue Attribut auf unserer Person-Klasse:

[ComponentReady]
public class Person
{
    // code der Person-Klasse
}

Fertig! Beachtenswert ist vielleicht noch, das .NET es zulässt, den Wortteil „Attribute“ aus unserem Klassennamen wegzulassen.

Eigene Attribute nutzen

Für sich genommen ist es zwar schön, dass uns der Compiler diesen Code abnimmt, etwas Sinnvollles stellt er aber bislang nicht an, da sich niemand für das Attribut interessiert. Das ändern wir nun:

private static void ListFinishedClasses()
{
    Console.WriteLine("\n\nFolgende Klassen in dieser Assembly sind fertig:\n\n");
    Assembly assThis = Assembly.GetExecutingAssembly();
    foreach (Type typTmp in assThis.GetTypes())
    {
        if (typTmp.GetCustomAttributes(typeof(ComponentReadyAttribute), false).Length > 0)
        {
            Console.WriteLine(typTmp.Name);
        }
    }
}

Diese Methode führt eine weitere Neuerung in dieses HowTo ein: Den Typ Assembly. Wenn uns die Klasse Type auch den Einstieg für alle möglichen Informationen ab Klassenebene liefert, so fehlt doch etwas. Wir müssen auch über alle Klassen einer Assembly iterieren können bzw. Eigenschaften von Anwendungen selbst auslesen können. Diese Aufgabe wird einem .NET-Programmierer sogar öfter unter die Fingerkuppen kommen, als man gemeinhin denkt.

Genau so ein Einstiegspunkt ist die Klasse Assembly. Auch sie bietet eine Menge an statischen Methoden und ist gleichzeitig ein Typ. Eine dieser Methoden benutzen wir auch gleich. GetExecutingAssembly() ist eine praktische Methode, die als Rückgabetyp eine Instanz der Anwendung liefert, aus der heraus der Aufruf von GetExecutingAssembly() erfolgte. Mit der Klasse Assembly selbst werden wir uns in späteren Teilen dieser HowTo-Serie befassen.

Unsere Aufgabenstellung erfordert, dass wir alle Typen einer Assembly durchgehen und nur diejenigen auflisten, die das Attribut ComponentReady tragen. Genau das tut Zeile 7. Die Schleife in Zeile 5 benutzt die Methode GetTypes() der Assembly, um über alle Typen zu iterieren. In Zeile 7 nun rufen wir die GetCustomAttributes-Methode der Typ-Klasse auf. Dieser geben wir den Typ useres Attributes mit und stellen ein, dass über Vererbung nicht in Eltern-Objekten Ausschau gehalten werden soll. Das ist wichtig! Doch dazu später und im Video-Tutorial mehr.

Das Ergebnis des Listings sollte der Name der Klasse Person sein.

Resumé und Ausblick

Reflection ist definitiv ein mächtiges Werkzeug und steckt hinter mehr Funktionen, als man gemeinhin glaubt. Reflection zu kennen erspart eine Menge unnützen Code und kann sogar die Zusammenarbeit in Projekten erheblich vereinfachen. Auch Themen, wie Code-Dokumentation, CAS (code access security) oder ORM (objekt-relationales Mapping) setzen letztlich auf Reflection auf. Im nächsten Teil dieser Serie wird es um die Klasse bzw. den Typ System.Reflection.Assembly gehen.

3 Antworten auf „Spiegelzauber (Teil 1)“

  1. Hallo,

    vor allem vielen Dank für die Beiträge. Die ich bisher gelesen habe, sind meiner Meinung nach gut geklärt trotz der Trockenheit einiger Themen.

    In diesem Beitrag ist mir etwas aufgefallen. Im zweiten Code-Schnitt scheint etwas schief gegangen zu sein. Da kommt Erklärungstext als Quellcode und bringt die ganzen HTML-Tags für den Text durcheinander.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.