Laden...

[Artikel] Attributbasierte Programmierung

Erstellt von egrath vor 17 Jahren Letzter Beitrag vor 17 Jahren 34.809 Views
egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren
[Artikel] Attributbasierte Programmierung

Attribut-Basierte Programmierung

In diesem Artikel wird beschrieben, für welche Zwecke man Attribute einsetzen kann und wie man die selben selbst erstellt. Als Aufgabenstellung habe ich zu Demonstrationszwecken die Aufgabe gewählt dass der Quellcode mittels Attributen Dokumentiert wird und diese Dokumentationen anschliessend mittels des Reflectionsmechanismusses wieder ausgelesen werden.

Einführung in Attribute

Wenn Sie schon länger mit der .NET Technologie arbeiten werden Sie vielleicht schon das eine oder andere mal auf Attribute gestossen sein - und sich vielleicht gefragt haben was diese überhaupt machen und wie diese intern umgesetzt werden. Innerhalb des .NET Frameworks gibt es viele vordefinierte Attribute, im folgenden stelle ich Ihnen einige der populärsten unter diesen vor:

Obsolete: Ein mit diesem Attribut gekennzeichnetes Element erzeugt bei der Kompilierung eine Warnmeldung, dass dieses nicht mehr verwendet werden sollte.
Serializable: Ein mit diesem Attribut gekennzeichnetes Element kann zur Laufzeit von Serializer persisitiert (und natürlich auch wiederhergestellt) werden.
DllImport: Ein mit diesem Attribut gekennzeichnetes Element stellt einen sg. Platform/Invoke dar, welcher die Laufzeitumgebung veranlässt das Element aus der nativen API zu benutzen.

Attribute selbst haben keine Bedeutung wenn Sie nicht von irgendeinem Mechanismus zur Laufzeit verarbeitet werden. Die verarbeitende Einheit kann dabei entweder die Applikation selbst sein die diese über den Reflections-Mechanismus ausliest , aber auch das Framework selbst (Beispielsweise bei der Serialisierung) oder andere Tools die mit der Assembly arbeiten (dazu gehört auch der Compiler selbst, wie man beim Attribut "Obsolete" sieht)

Die Daten, welche das Attribut ausmachen werden in der Assembly im Bereich der Metadaten gespeichert. Man kann sich diese Beispielsweise mittels des im Framework enthaltenen Tools "Ildasm" ansehen (Ctrl-M)

Das entwerfen und benutzen neuer Attribute

Um selbst ein neues Attribut zu erstellen, reicht es im groben schon aus eine neue, versiegelte Klasse zu definieren und diese von "System.Attribute" abzuleiten. Per Konvention enden alle Attribute auf "Attribute".


public sealed class DeveloperAttribute : System.Attribute
{   
}

Die Konstruktoren der Attribut-Klasse sollten jene Parameter annehmen, die anschliessend bei der Verwendung des Attributes selbst benötigt werden. Zusätzlich sollten auch noch Properties definiert werden, um die Eigenschaften des Attributes auch namentlich setzen zu können.

Beispiel für die Klasse "DeveloperAttribute":


public class DeveloperAttribute : System.Attribute
{
    // ********************************************************
    // Private member variables
    // ********************************************************

    private string m_Name;
    private string m_Comment;

    // ********************************************************
    // Constructors
    // ********************************************************

    public DeveloperAttribute( string name, string comment )
    {
        m_Name = name;
        m_Comment = comment;
    }

    public DeveloperAttribute( string name )
        : this( name, "" )
    {
    }

    public DeveloperAttribute()
        : this( "", "" )
    {
         // Console.Out.WriteLine( "DeveloperAttribute.ctor(): Default constructor called" ); // Ja auch das funktioniert
    }

    // ********************************************************
    // Public properties
    // ********************************************************

    public string Name
    {
        get
        {
            return m_Name;
        }

        set
        {
            m_Name = value;
        }
    }

    public string Comment
    {
        get
        {
            return m_Comment;
        }

        set
        {
            m_Comment = value;
        }
    }
}

Wenn wir die Klasse auf diesen weisen definieren, können wir das Attribute in folgender Weise benutzen:

[Developer("Mein Name", "Mein Kommentar")]: Der Konstruktor welcher zwei Parameter entgegennimmt wird aufgerufen
[Developer("Mein Name")]: Der Konstruktor welcher einen Parameter entgegennimmt wird aufgerufen
[Developer(Name = "Mein Name", Comment="Mein Kommentar")]: Der Konstruktor welcher keinen Parameter entgegennimmt wird augerufen. Anschliessend werden die Werte über die öffentlichen Properties gesetzt.

Es ist zu beachten, dass die Attributklasse erst zu jenem Zeitpunkt instantiiert wird, bei dem mittels eines Mechanismusses (Reflection) auf das Attribut zugegriffen wird. Wenn wir also in unserer Applikation das DeveloperAttribute Attribut benutzen, so hat dies keinerlei auswirkung auf die Laufzeit der Applikation. Erst wenn wir das Attribut auslesen wird eine entsprechende Instanz desselben erstellt mit welcher wir anschliessend arbeiten.

Bei der Definition des Attributes können wir festlegen für welches Element dieses verwendet werden kann. Dies geschieht durch ein vom Framework zur Verfügung gestelltes Attribut mit dem namen "AttributeUsage", welchem wir als Parameter eine aufzählung übergeben welche die Verwendung einschränken. So könnten wir beispielsweise veranlassen dass unser selbst erstelltes Attribut nur auf Klassen und Methoden angewandt werden kann:


[AttributeUsage( AttributeTargets.Class | AttributeTargets.Method )]
public class DeveloperAttribute : System.Attribute

Folgende Werte sind in der Aufzählung "AttributeTargets" definiert:


public enum AttributeTargets
{
    All, Assembly, Class, Constructor,
    Delegate, Enum, Event, Field,
    Interface, Method, Module, Parameter,
    Property, ReturnValue, Struct
}

Das auslesen von Attributen mittels Reflection

Um nun etwas mit den Attributen anfangen zu können welche wir im Quelltext deklariert haben und die vom Compiler in den Metainformationen der Assembly gespeichert wurden, müssen wir auf den Reflection Mechanismus zurückgreifen der uns vom Framework zur verfügung gestellt wird.

Bei diesem handelt es sich um eine Methodik um nachträglich Informationen wie beispielsweise Typeinformationen, Variablennamen, Methodeninformationen aber auch Attribute aus Assemblys auslesen und weiterverarbeiten zu können. (Zum beispiel wäre es mittels Reflection auch möglich eine Assembly interne, private Klasse zu instantiieren und diese zu verwenden - dies aber nur am Rande; Wird in einem kommenden Artikel genauer behandelt).

Für unser Beispiel wollen wir eine kleine Applikation entwickeln, welche unser DeveloperAttribute aus einer bestehenden Assembly ausliest und die darin gespeicherten Informationen sichtbar macht. In unserem Fall sind dies der Name des Entwicklers und der entsprechende Kommentar für ein Element.


Assembly toReflect = Assembly.LoadFrom( args[0] );
Assembly attribAssembly = Assembly.LoadFrom( "DeveloperAttribute.dll" );
Type developerAttribute = attribAssembly.GetType( "DeveloperAttribute", true );
PropertyInfo devAttrName = developerAttribute.GetProperty( "Name" );
PropertyInfo devAttrComment = developerAttribute.GetProperty( "Comment" );
BindingFlags bindFlags = ( BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance );



foreach( Type definedType in toReflect.GetTypes() )
{
    object[] typeAttributes = definedType.GetCustomAttributes( developerAttribute, false );
    if( typeAttributes.Length > 0 )
    {
        Console.Out.WriteLine( "{0}", definedType.Name );
        foreach( object attribute in typeAttributes )
        {
            Console.Out.WriteLine( "    -> Entwickler : {0}", devAttrName.GetValue( attribute, null ) );
            Console.Out.WriteLine( "    -> Kommentar  : {0}", devAttrComment.GetValue( attribute, null ) );
        }
    }

    foreach( MemberInfo member in definedType.GetMembers( bindFlags ))
    {
        object[] memberAttributes = member.GetCustomAttributes( developerAttribute, false );
        if( memberAttributes.Length > 0 )
        {
            if( member.MemberType == MemberTypes.Method )
            {
                ParameterInfo[] parameters = null;
                ParameterInfo returnType = null;

                try
                {
                    parameters = member.ReflectedType.GetMethod( member.Name, bindFlags ).GetParameters();
                    returnType = member.ReflectedType.GetMethod( member.Name, bindFlags ).ReturnParameter;
                }
                catch( AmbiguousMatchException )
                {
                    Console.Out.WriteLine( "Overloaded Methods are currently not supported!" );
                    continue;
                }

                Console.Out.Write( "{0}.[{1} {2}", definedType.FullName, returnType.ParameterType, member.Name );
                if( parameters.Length > 0 ) Console.Out.Write( " (" );
                for( int paramIndex = 0; paramIndex < parameters.Length; paramIndex ++ )
                {
                    Console.Out.Write( "{0} {1}{2}", parameters[paramIndex].ParameterType, parameters[paramIndex].Name,
                                                     (( paramIndex >= parameters.Length-1 ) ? "" : ", " ));
                }

                if( parameters.Length > 0 )
                {
                    Console.Out.Write( ")]" );
                }
                else
                {
                    Console.Out.Write( "]" );
                }

                Console.Out.Write( System.Environment.NewLine );
            }
            else if( member.MemberType == MemberTypes.Field )
            {
                FieldInfo fieldType = member.ReflectedType.GetField( member.Name, bindFlags );
                Console.Out.WriteLine( "{0}.[{1} {2}]", definedType.FullName, fieldType.FieldType, member.Name );
            }
            else if( member.MemberType == MemberTypes.Property )
            {
                PropertyInfo propertyType = member.ReflectedType.GetProperty( member.Name, bindFlags );
                Console.Out.WriteLine( "{0}.[{1} {2}]", definedType.FullName, propertyType.PropertyType, member.Name );
            }
            else
            {
                Console.Out.WriteLine( "{0}.[{1}]", definedType.FullName, member.Name );
            }

            foreach( object attribute in memberAttributes )
            {
                Console.Out.WriteLine( "    -> Entwickler : {0}", devAttrName.GetValue( attribute, null ));
                Console.Out.WriteLine( "    -> Kommentar  : {0}", devAttrComment.GetValue( attribute, null ));
            }
        }
    }
}

Das obige Beispiel sieht vielleicht auf den ersten Blick etwas verwirrend aus, beschränkt sich aber im wesentlichen auf folgende Tätigkeiten:
*Laden einer Assembly welche wir der Applikation auf der Kommandozeile übergeben haben *Laden der Assembly welche das Attribut definiert welches wir auslesen wollen *Den Type unseres Attributes aus der Attribut-Assembly laden und anschliessend zwei PropertyInfo Klassen instantiieren welche zu diesem Type gehören (mittels dieser werden wir anschliessend die Werte der Properties auslesen; Wir benötigen diese welche wir die Attribute als Type "Object" zurückgeliefert bekommen und Late-Binding verwenden) *Überprüfen ob die Typen der zu inspezierenden Assembly schon selbst ein entsprechendes Attribut besitzen - wenn ja, dann auf der Konsole ausgeben *Für jeden Typen in der zu inspezierenden Assembly alle Member desselben durchforsten und nachsehen ob das derzeitige Element ein Attribut besitzt - wenn ja, dann auf der Konsole ausgeben

Viel spass beim ausprobieren.

49.485 Beiträge seit 2005
vor 17 Jahren
N
177 Beiträge seit 2006
vor 17 Jahren

Original von egrath
Serializable: Ein mit diesem Attribut gekennzeichnetes Element kann zur Laufzeit von Serializer persisitiert (und natürlich auch wiederhergestellt) werden.

Kleine Off-topic Anmerkung dazu:
Ich bin nicht viel weiter mit dem Lesen gekommen als bis hier. Das Problem mit solchen Beschreibungen, die auch Microsoft gerne verwendet, liegt darin, dass sie nichtssagend sind. Sie sind zunächst selbstereferenziell. Beispiel: "Instanziierung: Man erstellt eine Instanz, indem man ein Objekt instanziiert". Oft sind die Beschreibungen auch in sich geschlossene Kreisläufe. Also etwa: "Ein Objekt kann persistiert werden. Serialisierung ermöglicht auf einfache Weise Persistierung. Persistierung wird durch Serialisierung implementiert." Dabei werden auch gerne Begriffe benutzt (persistiert), mit denen man gerade als Anfänger erst mal nichts anfangen kann. Genauso gerne führt Microsoft auch neue Begriffe für altbekannte Dinge ein (Delegate = Callback). Und es fehlt schlicht an grundlegenden Sachen wie Definitionen oder zumindest umgangssprachliche Erklärungen.

Mein Posting ist zwar mehr eine Kritik an der MS "Dokumentation", aber der obige Schnipsel kopiert exakt dieses Schema. Der Rest des Artikels ist vermutlich ausgezeichnet geschrieben. Bei Bedarf kann mein Posting auch gelöscht werden, da es sich nicht wirklich mit dem Artikel beschäftigt.

egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren

Hallo nop,

Danke für deine Anregung - ich vergesse leider oft in meiner Schreiberei dass ich Dinge die ich teilweise als selbstverständlich ansehe in einem Artikel besser erläutern sollte.

Werde den Artikel dahingegehend überarbeiten.

Grüsse,

Egon