C# Seminar: Reflection im .NET Framework

Die Seite wird erstellt Haimo-Haio Heck
 
WEITER LESEN
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 Infor-
      mationen ermitteln und brauchen keine getrennte Beschreibung mehr, die in einer
      separaten Datei gepflegt werden müsste.
    • Performance-Optimierungen durch dynamische Codeerzeugung zur Laufzeit wer-
      den 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 Co-
des in den Assemblies teilweise gravierend von der C#-Struktur ab.
Abbildung 1: Quell-Code, MSIL-Code (Console), und dekompilierter Code einer Assembly (Hin-
tergrund)

    • Eigene Metadaten (Attribute) können direkt Programmstrukturen zugeordnet wer-
      den, 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 ent-
halten 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 entsprechen-
den 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 Lan-
guage Runtime (CLR) streng objektorientiert gestaltet ist, sind alle logischen Programm-
strukturen, also etwa Eigenschaften, Methoden oder Felder immer innerhalb eines Typs
definiert. Physisch sind Typen selbst in Modulen enthalten, diese schließlich in einer As-
sembly, 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 Klas-
se System.Type auf diese Informationen zugreifen. System.Type-Objekte bilden daher den
Einstiegspunkt für Reflection-Aufrufe.
public class MeinTyp                                                                      1
{                                                                                         2
        private int einFeld = 42;                                                         3
        protected float EineProperty { get; set; };                                       4
        public void Methode1 (int a, int b) { /* ... */ }                                 5
        public List Methode2 () { /* ... */ }                                     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 umge-
kehrt.
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 Sys-
tem.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 Reflection-
API verwendet werden kann. Anschließend werden Attribute eingeführt und genauer be-
handelt. Schließlich werden einige fortgeschrittene Techniken gezeigt um Microsoft Inter-
mediate Language (MSIL) zu analysieren und zur Laufzeit dynamisch zu erzeugen.
System.Reflection                            System

                                MemberInfo

        FieldInfo   MethodBase           PropertyInfo     EventInfo        Type

            ContructorInfo        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 beschrie-
ben 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 ()), gefilter-
te Teilmengen oder auch einzelne Elemente mit GetMember (string name), GetNestedTy-
pe (), 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 Proper-
tyInfo-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 MethodInfo-
Objekts, 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 Field-
Info-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 geschlos-
senenen generischen Typen wurden bereits alle Typ-Parameter definiert, bei offenen nicht.
Offene generische Typen können deshalb auch per Reflection nicht direkt instanziiert wer-
den; 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 IsGene-
ricTypeDefinition 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 Defini-
tion so:
Type geschlossen = typeof (List);
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 CLR-
Strukturen gemeinsam ein C#-Feature ab:
              Enums    C#-Enums werden durch statische Klassen die von System.Enum abge-
                       leitet 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 At-
                       tribut [DefaultMember].

           Finalizer   Überschreibt eine Methode, nämlich die mit dem Namen Finalize.

3.3     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 Metho-
de handelt. Alle weiteren Parameter werden an die aufzurufende Methode weitergegeben.
Das folgende Beispiel erzeugt dynamisch ein Objekt vom Typ StringBuilder und ruft dy-
namisch 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 ("
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 anzu-
wendenden Attributes wird in eckige Klammern gesetzt und vor die Definition des Ziel-
Elements 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 techni-
sche) Formen in denen Attribute auftreten:

   • Auswertung durch eigenen Code: Der direkte, flexibelste und vollständig transpa-
     rente Weg ist es, im Rahmen des normalen Programmablaufs Attribute per Reflection-
     API 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;                                                                   1
        using System.Runtime.Serialization;                                             2
        using System.Reflection;                                                        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 At-
  tribute und an mindestens einer Stelle einen Aufruf einer Funktion der Klassen-
  bibliothek, 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 abgebil-
  det wird. Der Reflection-Code ist dabei in der Serialize-Methode des Frameworks
  gekapselt.
  using   System;                                                                    1
  using   System.Xml;                                                                2
  using   System.Xml.Serialization;                                                  3
  using   System.IO;                                                                 4
                                                                                     5
  public class Test                                                                  6
  {                                                                                  7
      public string Feld1; // wird ein Element                                       8
      [XmlAttribute]                                                                 9
      public string Feld2; // wird ein Attribut                                      10
                                                                                     11
       public static void Main () {                                                  12
           XmlSerializer serializer         = new XmlSerializer (typeof(Test)); 13
           using (TextWriter writer         = new StreamWriter ("test.xml")) { 14
               Test test = new Test         ();                                 15
               serializer.Serialize         (writer, test);                     16
               writer.Close ();                                                      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 Attribu-
  tes 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 veran-
  lassen. 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 At-
  tribute und deklariert diese damit als WebService-Funktionen. Das ASP.NET Fra-
  mework decodiert per HTTP ankommende SOAP-Anfragen, ermittelt per Reflecti-
  on 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 Assemb-
  ly 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 Konfigurations-
  management verwendet, etwa um manche Konsolenausgaben nur in Debug-Builds
  zu erzeugen.
                                                                                   1
  #define EIN_SYMBOL                                                               2
  using System;                                                                    3
  using System.Diagnostics;                                                        4
                                                                                   5
  public class Blah                                                                6
  {                                                                                7
      [Conditional("EIN_SYMBOL")]                                                  8
      public static void Debug (string msg)                                        9
      {                                                                            10
          Console.WriteLine (msg);                                                 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 nach-
  dem 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                                                                             1
        {                                                                                      2
            private int x;                                                                     3
            private int y;                                                                     4
                                                                                               5
               [ContractInvariantMethod]                                                       6
               private void Invariant () {                                                     7
                   Contract.Invariant (y >= 0);                                                8
               }                                                                               9
               public int Y {                                                                  10
                   get { return y; }                                                           11
                   set { y = value; }                                                          12
               }                                                                               13
               public int Divide () {                                                          14
                   return x/y;                                                                 15
               }                                                                               16
        }                                                                                      17

                              Listing 7: Code Contracts in .NET 4.0

        Das Programm ccrewrite.exe (Bestandteil des Microsoft .NET Framework 4.0) fin-
        det 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 Member-
        Zugriff, der den Zustand der Klasse potentiell verändert, die Invariante erneut ge-
        prüft.
        class Test                                                                             1
        {                                                                                      2
            private int x;                                                                     3
            private int y;                                                                     4
                                                                                               5
               [ContractInvariantMethod]                                                       6
               private void Invariant () {                                                     7
                   Contract.Invariant (y >= 0);                                                8
               }                                                                               9
               public int Y {                                                                  10
                   get { return y; }                                                           11
                   set { y = value; Invariant (); }                                            12
               }                                                                               13
               public int Divide () {                                                          14
                   var res = x/y;                                                              15
                   Invariant ();                                                               16
                   return res;                                                                 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 Re-
moting eingesetzt wird [5, S. 226] :
[AttributeUsage (AttributeTarget.Class)                                                    1
public class InterceptableAttribute : ContextAttribute                                     2
{                                                                                          3
    public InterceptableAttribute () : base ("C.I.") {}                                    4
                                                                                           5
      public override bool IsContextOK (Context ctx,                                       6
          IConstructionCallMessage ctorMsg)
      {                                                                                    7
          return (ctx.GetProperty ("Interception") != null);                               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 Con-
textAttribute 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 Ei-
genschaften. 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 Anwend-
barkeit 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 Code-
Analyse. 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) {                                 2
          if (instr.OpCode.Name == "callvirt") {                                          3
              var methRef = instr.Operand as MethodReference;                             4
              if (methRef != null && methRef.Name == callName)                            5
                   return methRef;                                                        6
          }                                                                               7
      }                                                                                   8
      return null;                                                                        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 Rei-
he 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 (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).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. Bei-
spielsweise 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 er-
zeugen. 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 Akademi-
    scher Verlag, 1. Auflage, 2004.

[6] Microsoft Developer Network (MSDN), MethodInfo Class. http://msdn.microsoft.com/en-
    us/library/system.reflection.methodinfo.aspx , November 2010
Sie können auch lesen