7.10 Attribute 

Ein Attribut ist ein Feature von .NET, das einer Komponente Zusatzinformationen bereitstellt oder ein bestimmtes Verhalten signalisiert oder vorgibt. Attribute gehören zu den Metadaten des Programms und werden zur Laufzeit von der CLR (Common Language Runtime) ausgewertet. Dieses Prinzip ist grundsätzlich nicht neu, denken Sie nur an die altbekannten INI-Dateien, die eine ähnliche Zielsetzung hatten. Mit einem Attribut lässt sich das Laufzeitverhalten praktisch aller .NET-Elemente beeinflussen: Assemblys, Klassen, Interfaces, Strukturen, Delegates, Enumerationen, Konstruktoren, Methoden, Eigenschaften, Parameter, Ereignisse, ja sogar die Rückgabewerte von Methoden.
Als Basisklasse aller Attribute dient die abstrakte Klasse Attribute aus dem Namespace System. Wenn Sie in die Online-Dokumentation zum Stichwort Attribute schauen, werden Sie feststellen, dass das .NET Framework sehr viele Attribute vordefiniert, die aus Attribute abgeleitet sind. Alle denkbaren Anforderungen werden damit sicherlich nicht abgedeckt, deshalb können Sie auch benutzerdefinierte Attribute entwickeln und dadurch die Flexibilität Ihrer Anwendung erhöhen.
7.10.1 Das »Flags«-Attribut 

Wir wollen uns die Wirkungsweise eines Attributs exemplarisch am Attribut FlagsAttribute ansehen, das vom .NET Framework bereitgestellt wird und mit Enumerationen verknüpft wird. Dazu nehmen wir zunächst an, dass eine Enumeration namens Keys die drei Zustandstasten ,
und
beschreibt.
public enum Keys { Shift = 1, Ctrl = 2, Alt = 4 }
Wird diese Enumeration um FlagsAttribute erweitert, geben wir an, dass die Aufzählung als Kombination von Bits aufgefasst werden kann. Das Attribut wird in C# in eckige Klammern gefasst.
[FlagsAttribute] public enum Keys { Shift = 1, Ctrl = 2, Alt = 4 }
Wir können nun eine Variable vom Typ Keys deklarieren und ihr einen Wert zuweisen, der den Zustand der beiden gleichzeitig gedrückten Tasten und
beschreibt. Beide verknüpfen wir mit dem |-Operator:
Keys tastenkombination = Keys.Ctrl | Keys.Shift;
Mit den bitweisen Operatoren kann nun natürlich auch geprüft werden, ob der Anwender eine bestimmte Taste oder gar Tastenkombination gedrückt hat:
Keys tastenkombination = Keys.Ctrl | Keys.Shift; if ((tastenkombination & Keys.Alt) == Keys.Alt) Console.WriteLine("Alt gedrückt"); else Console.WriteLine("Alt nicht gedrückt");
Hier wird natürlich die Ausgabe lauten, dass die -Taste nicht gedrückt ist. Eine ähnliche Enumeration, die dann allerdings jede Taste der Tastatur beschreibt, gibt es im Namespace System.Windows.Forms.
Wenn wir uns nun den Inhalt der Variablen tastenkombination mit
Console.WriteLine(tastenkombination.ToString());
ausgeben lassen, indem wir ToString an der Konsole aufrufen, erhalten wir:
Shift, Ctrl
Hätten wir FlagsAttribut nicht gesetzt, würde die Ausgabe 3 lauten. Sie müssen berücksichtigen, dass die Mitglieder solchermaßen definierter Enumerationen Zweierpotenzen sind (also 1, 2, 4, 8, 16, 32, 64 ...). Alternativ sind auch hexadezimale Zahlenwerte zulässig.
Attribute setzen
Attributsbezeichner enden immer mit dem Suffix Attribute. Wird ein Attribut mit einem Element verknüpft, darf auf das Suffix Attribute verzichtet werden. Bezogen auf unser Beispiel dürfen Sie also
[FlagsAttribute] public enum Keys { ... }
und
[Flags] public enum Keys { ... }
gleichwertig verwenden. Bemerkt die Laufzeit die Verknüpfung eines Attributs mit einem Element, sucht sie nach einer Klasse, die mit dem angegebenen Attributbezeichner übereinstimmt und gleichzeitig die Klasse Attribute ableitet, also beispielsweise nach einer Klasse namens Flags. Wird die Laufzeit nicht fündig, hängt sie automatisch das Suffix Attribute an den Bezeichner an und wiederholt die Suche.
Sie können auch mehrere Attribute gleichzeitig setzen. Beispielsweise könnten Sie mit dem ObsoleteAttribute das Element zusätzlich als veraltet kennzeichnen, z. B. so:
[FlagsAttribute] [Obsolete("Diese Enumeration ist veraltet."); public enum Keys { ... }
7.10.2 Anmerkungen zu den Attributen 

Das Attribut [Flags] erinnert in seiner Wirkungsweise eher an eine boolesche Variable, die gesetzt ist oder nicht. Weist eine Enumeration dieses Attribut auf, lassen sich die Elemente der Aufzählung bitweise miteinander kombinieren. Sehr ähnlich verhalten sich auch viele andere Attribute der .NET-Klassenbibliothek. Das sehr häufig verwendete Attribut SerializableAttribute kennzeichnet beispielsweise eine Klasse als binär serialisierbar.
Jedes .NET-Projekt weist neben den Quellcodedateien auch die Datei AssemblyInfo.cs auf, die Metadaten über die Assemblierung beschreibt. Dazu gehören Versionsinformationen, Titel und Copyright-Hinweise. Diese Zusatzinformationen werden ebenfalls über Attribute bereitgestellt und können direkt in AssemblyInfo.cs eingetragen werden. Die Daten werden zur Laufzeit ausgewertet, wie auch die aller anderen Attribute.
Attribute basieren auf Klassendefinitionen und können daher alle klassentypischen Elemente enthalten. Dazu gehören neben Konstruktoren auch Felder. Insbesondere diese beiden Elemente ermöglichen es, über ein Attribut dem attributierten Element Zusatzinformationen bereitzustellen. Wie das in der Praxis aussieht, wollen wir uns am Beispiel eines benutzerdefinierten Attributs verdeutlichen.
7.10.3 Benutzerdefinierte Attribute 

Obwohl das .NET Framework zahlreiche Attribute vordefiniert, können Sie auch für eigene Zwecke Attributklassen selbst schreiben. Allerdings müssen Sie für die Auswertung des Attributs zur Laufzeit auch selbst sorgen.
Drei Punkte müssen Sie beachten, um ein benutzerdefiniertes Attribut zu codieren:
- Der Definition eines benutzerdefinierten Attributs selbst geht immer die Definition des Attributs AttributeUsageAttribute voraus.
- Die Klasse wird aus Attribute abgeleitet.
- An den Klassenbezeichner sollte das Suffix Attribute angehängt werden.
Lassen Sie uns an dieser Stelle exemplarisch ein eigenes Attribut erstellen, dessen Aufgabe es ist, sowohl den Entwickler einer Klasse oder Methode als auch dessen Personalnummer anzugeben. Das folgende Beispiel zeigt die noch unvollständige Definition der Attributklasse:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] public class DeveloperAttribute : Attribute { ... }
Die Voranstellung des Attributs AttributeUsage vor die Klasse sorgt dafür, bereits hier elementare Eigenschaften der neuen Attributklasse festzulegen. In diesem Zusammenhang sind drei Parameter besonders interessant:
- AttributeTargets
- Inherited
- AllowMultiple
Während AttributeTargets angegeben werden muss, sind die beiden anderen optional.
AttributeTargets
Jedes Attribut kann sich nur auf bestimmte Codeelemente auswirken. Diese werden mit AttributeTargets bekannt gegeben. Im Beispiel oben wird mit AttributeTargets.All zum Ausdruck gebracht, dass wir das benutzerdefinierte Attribut auf alle Elemente anwenden können, aber das muss nicht immer so sein. Man kann den Einsatz eines Attributs ebenso gut nur auf Methoden oder Felder beschränken. Es steht dabei immer folgende Frage im Vordergrund: Was soll das Attribut letztendlich bewirken, welche Elemente sollen über das Attribut beeinflusst werden?
AttributeTargets ist in der .NET-Klassenbibliothek als Enumeration vordefiniert und enthält die in Tabelle 7.4 aufgeführten Entitäten.
Mitglieder | Beschreibung |
All |
Das Attribut gilt für jedes Element der Anwendung. |
Assembly |
Das Attribut gilt für die Assemblierung. |
Class |
Das Attribut gilt für die Klasse. |
Constructor |
Das Attribut gilt für den Konstruktor. |
Delegate |
Das Attribut gilt für das Delegate. |
Enum |
Das Attribut gilt für die Enumeration. |
Event |
Das Attribut gilt für das Ereignis. |
Field |
Das Attribut gilt für das Feld. |
Interface |
Das Attribut gilt für die Schnittstelle. |
Method |
Das Attribut gilt für die Methode. |
Module |
Das Attribut gilt für das Modul. |
Parameter |
Das Attribut gilt für den Parameter. |
Property |
Das Attribut gilt für die Property. |
ReturnValue |
Das Attribut gilt für den Rückgabewert. |
Struct |
Das Attribut gilt für die Struktur. |
Natürlich steht auch hinter AttributeUsageAttribute eine Klassendefinition, die im Namespace System zu finden ist. Im Grunde genommen unterscheidet sich eine Klasse, die ein Attribut beschreibt, nicht von einer herkömmlichen Klasse. Daher verwundert es nicht, dass die Klasse AttributeUsageAttribute einen Konstruktor definiert, der einen Parameter vom Typ AttributeTargets erwartet:
public AttributeUsageAttribute(AttributeTargets validon);
Der Parameter validon vom Typ AttributeTargets wird bitweise interpretiert. Jedes Bit beschreibt dabei ein Element, auf das das Attribut angewendet werden kann. Möchten Sie das Attribut mehreren verschiedenen Elementen zugänglich machen, müssen Sie mehrere AttributeTargets-Konstanten bitweise verknüpfen, z. B. so:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
In diesem Fall wird ein Attribut bereitgestellt, das sowohl Methoden als auch Eigenschaften als Informationsquelle dienen kann.
Inherited
Eine Klasse kann ihre Mitglieder einer abgeleiteten Klasse vererben. Einem Entwickler stellt sich natürlich die Frage, ob das Attribut in den Vererbungsprozess mit einbezogen wird oder ob es Gründe gibt, es davon auszuschließen. Einem benutzerdefinierten Attribut teilen wir dies durch den booleschen Parameter Inherited mit, den wir optional AttributeUsageAttribute übergeben können. Standardmäßig ist der Wert auf true festgelegt. Demnach vererbt sich ein gesetztes Attribut in einer Vererbungshierarchie weiter.
AllowMultiple
In wohl eher seltenen Fällen kann es erforderlich sein, ein Attribut demselben Element mehrfach zuzuweisen. Diese Situation wäre denkbar, wenn man über das Attribut einem Element mehrere Feldinformationen zukommen lassen möchte. Dann muss man die mehrfache Anwendung eines Attributs explizit gestatten. Zur Lösung geben Sie den Parameter
AllowMultiple = true
an. Verzichten Sie auf diese Angabe, kann ein Attribut per Definition mit einem bestimmten Element nur einmal verknüpft werden.
Konstruktoren eines Attributs
Sie können in Attributklassen öffentliche Felder und Eigenschaften definieren, deren Werte an den Benutzer des Attributs weitergeleitet werden. Initialisiert werden die Felder über Konstruktoren. Unser DeveloperAttribute soll mit Name und Identifier zwei Felder beschreiben. Das erstgenannte Feld wird beim Konstruktoraufruf initialisiert.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DeveloperAttribute : Attribute { public string Name; public int Identifier; public DeveloperAttribute(string name) { Zuname = name; } }
Der Konstruktor nimmt einen Parameter entgegen, nämlich den Wert für das Feld Name. Bevor Sie sich darüber Gedanken machen, wie man das Feld Identifier initialisiert, sollten Sie sich ansehen, wie das Attribut auf eine Klasse angewendet wird:
[DeveloperAttribute("Meier")]
public class Demo {
...
}
Mit dieser Definition wird der Konstruktor unter Übergabe einer Zeichenfolge aufgerufen. Das zweite Feld des Attributs (Identifier) wird mit keinem bestimmten Wert initialisiert, es enthält 0. Selbstverständlich könnten wir innerhalb der Attributdefinition für Identifier einen von 0 abweichenden Wert festlegen, aber dieser ist dann natürlich für jedes verknüpfte Element identisch.
Positionale und benannte Parameter
Um Identifier einen individuellen Wert zuzuweisen, können Sie DeveloperAttribute auch wie folgt mit der Klasse verknüpfen:
[DeveloperAttribute("Meier", Identifier = 8815)]
public class Demo {
...
}
Beachten Sie, dass wir jetzt zwei Argumente übergeben, obwohl der Konstruktor nur einen Parameter vorsieht. Dies ist ein besonderes Merkmal der Attribute, denn beim Initialisieren eines Attributs können Sie sowohl positionale als auch benannte Parameter verwenden.
- Positionale Parameter sind die Parameter für den Konstruktoraufruf und müssen immer angegeben werden, wenn das Attribut gesetzt wird. Benannte Parameter sind optionale Parameter. In unserem Beispiel ist Name ein positionaler Parameter, dem die Zeichenfolge »Meier« übergeben wird, während Identifier ein benannter Parameter ist.
- Benannte Parameter sind sehr flexibel. Einerseits können sie Standardwerte aufweisen, die grundsätzlich immer gültig sind, andererseits kann der Wert im Bedarfsfall individuell festgelegt werden.
Ob Sie ein Attributfeld positional oder benannt einsetzen wollen, ist eine Entscheidung, die sich an den spezifischen Anforderungen orientiert. Ob ein benannter Parameter neu festgelegt werden muss oder ob der Standardwert akzeptabel ist, hängt vom Einzelfall ab.
Die Möglichkeit, benannte Parameter vorzusehen, befreit Sie von der Verpflichtung, für jede denkbare Kombination von Feldern und Eigenschaften überladene Konstruktoren in der Attributdefinition vorsehen zu müssen. Andererseits wird Ihnen damit aber nicht die Alternative entzogen, dennoch den Konstruktor zu überladen. Da unterscheiden sich die herkömmlichen Klassendefinitionen nicht von denen der Attribute.
Wenn Sie ein Attribut mit einem Element verknüpfen und dabei positionale und benannte Parameter verwenden, müssen Sie eine wichtige Regel beachten: Zuerst werden die positionalen Parameter aufgeführt, danach die benannten. Die Reihenfolge der benannten Parameter ist beliebig, da der Compiler aufgrund der Parameternamen die angegebenen Werte richtig zuordnen kann. Benannte Parameter können alle öffentlich deklarierten Felder oder Eigenschaften sein – vorausgesetzt, sie sind weder statisch noch konstant definiert.
7.10.4 Attribute auswerten 

Vermutlich werden Sie sich jetzt fragen, was ein Attribut bewirkt, welchen Sinn es also hat, ein Attribut zu setzen. Im Grunde genommen ist die Antwort ganz einfach. Viele Methoden im .NET Framework verlangen zur Ausführung ihrer Operation, dass das betroffene Element mit einem bestimmten Attribut versehen ist. Beispielsweise können Sie eine Klasse nur dann binär serialisieren, wenn der Klasse das Attribut Serializable angeheftet ist. Ansonsten ist eine binäre Serialisierung nicht möglich. Sehr ähnlich agiert auch das Flags-Attribut. Nur wenn es gesetzt ist, lassen sich die beiden Operanden bitweise kombinieren. Ein Attribut hat demnach einen steuernden Einfluss auf das Laufzeitverhalten eines Elements und versorgt es möglicherweise mit essenziellen Informationen.
Operationen, die auf die Existenz eines Attributs angewiesen sind, müssen zuerst feststellen, ob das erforderliche Attribut gesetzt ist oder nicht. Im folgenden Beispielprogramm soll dies für das Beispiel unseres eben entwickelten DeveloperAttributes gezeigt werden. Beachten Sie hier bitte, dass der Namespace System.Reflection bekannt gegeben werden muss.
// --------------------------------------------------------- // Beispiel: ...\Kapitel 7\AttributeDemo // --------------------------------------------------------- using System.Reflection; [Developer("Meier")] class Demo { [Developer("Fischer", Identifier=455)] public void DoSomething() { } public void DoMore() { } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DeveloperAttribute : Attribute { public string Name; public int Identifier; public DeveloperAttribute(string name) { Name = name; } } class Program { static void Main(string[] args) { Type tDemo = typeof(Demo); Type tAttr = typeof(DeveloperAttribute); MethodInfo mInfo1 = tDemo.GetMethod("DoSomething"); MethodInfo mInfo2 = tDemo.GetMethod("DoMore"); // Prüfen, ob das Attribut bei der Klasse 'Demo' gesetzt ist DeveloperAttribute attr = (DeveloperAttribute)Attribute.GetCustomAttribute(tDemo, tAttr); if (attr != null) { Console.WriteLine("Name: {0}", attr.Name); Console.WriteLine("Identifier: {0}", attr.Identifier); } else Console.WriteLine("Attribut nicht gesetzt"); // Prüfen, ob das Attribut bei der Methode 'DoSomething' gesetzt ist attr = (DeveloperAttribute)Attribute.GetCustomAttribute(mInfo1, tAttr); if (attr != null) { Console.WriteLine("Name: {0}", attr.Name); Console.WriteLine("Identifier: {0}", attr.Identifier); } // Prüfen, ob das Attribut bei der Methode 'DoMore' gesetzt ist bool isDefinied = Attribute.IsDefined(mInfo2, tAttr); if (isDefinied) Console.WriteLine("DoMore hat das Attribut."); else Console.WriteLine("DoMore hat das Attribut nicht."); Console.ReadLine(); } }
Das benutzerdefinierte Attribut DeveloperAttribut ist identisch mit demjenigen, das wir bereits vorher in diesem Abschnitt behandelt haben. Es kann mit einer Klasse oder einer Methode verknüpft werden. Die Klasse Demo, die dieses Attribut aufweist, enthält mit DoSomething und DoMore zwei Methoden, von denen nur die erstgenannte mit dem Attribut verknüpft ist.
Um die Frage zu beantworten, ob ein bestimmtes Element mit dem DeveloperAttribute verknüpft ist oder nicht, greifen wir auf die Möglichkeiten einer Technik zurück, die als Reflection bezeichnet wird. Die Reflection gestattet es, die Metadaten einer .NET-Assembly und der darin enthaltenen Datentypen zu untersuchen und auszuwerten. Zur Abfrage von Attributen stellt die Reflection die Klasse Attribute mit der statischen Methode GetCustomAttribute bereit. Da wir sowohl die Klasse als auch die Methoden untersuchen wollen, müssen wir auf zwei verschiedene Überladungen zurückgreifen. Für die Klasse ist es die folgende:
public static Attribute GetCustomAttribute(Module, Type);
Um eine Methode zu untersuchen, ist es die folgende Überladung:
public static Attribute GetCustomAttribute (MemberInfo, Type);
Im ersten Argument geben wir den Typ des zu untersuchenden Elements an, im zweiten Parameter den Typ des Attributs. Um den Typ mithilfe von Code zu beschreiben, wird von der Reflection die Klasse Type bereitgestellt. Diese beschreibt den Datentyp und kann auf zweierlei Art und Weise erzeugt werden:
- unter Verwendung des Operators typeof, dem der Typbezeichner übergeben wird
(z. B. typeof(Demo))
- durch Aufruf der Methode GetType() auf einer Objektreferenz
(z. B. myObject.GetType())
Um die Attribute einer Klasse auszuwerten, übergeben Sie der Methode GetCustomAttribute nur den Type der Klasse und den Type des gesuchten Attributs. Zur Auswertung einer Methode ist ein MemberInfo-Objekt erforderlich. MemberInfo ist eine abstrakte Klasse im Namespace System.Reflection. Wir erhalten die Metadaten der zu untersuchenden Methode, wenn wir die Methode GetMethod des Type-Objekts unter Angabe des Methodenbezeichners aufrufen. Der Typ der Rückgabe ist MethodInfo, eine von MemberInfo abgeleitete Klasse.
Der Typ der Rückgabe der beiden Überladungen von GetCustomAttribute ist Attribute. Dabei handelt es sich entweder um die Referenz auf das gefundene Attribut oder um null, falls das Attribut nicht mit dem im ersten Parameter angeführten Element verknüpft ist. Daher erfolgt zuerst eine Konvertierung in das Zielattribut und anschließend eine Überprüfung, ob der Rückgabewert null ist.
DeveloperAttribute attr = (DeveloperAttribute)Attribute.GetCustomAttribute(tDemo, tAttr); if (attr != null) { Console.WriteLine("Name: {0}", attr.Name); Console.WriteLine("Identifier: {0}", attr.Identifier; } else Console.WriteLine("Attribut nicht gesetzt");
Da wir bei der Implementierung von Main wissen, dass nur unser benutzerdefiniertes Attribut DeveloperAttribute gesetzt ist (oder auch nicht), genügt uns diese Untersuchung. Ein Element kann natürlich auch mit mehreren Attributen verknüpft sein. Im Code müssten wir dann die Elemente auf alle gesetzten Attribute abfragen.
7.10.5 Festlegen der Assembly-Eigenschaften 

Weiter oben habe ich kurz die Datei AssemblyInfo.cs erwähnt. Ganz allgemein dient diese Datei dazu, Zusatzinformationen zu der aktuellen Assemblierung bereitzustellen, beispielsweise eine Beschreibung, Versionsinformationen, Firmenname, Produktname und mehr. Diese werden im Windows Explorer in den Dateieigenschaften angezeigt. Da die Informationen die Assemblierung als Ganzes betreffen, müssen die Deklarationen außerhalb einer Klasse stehen und dürfen auch nur einmal gesetzt werden.
[assembly: AssemblyTitle("AssemblyTitle")] [assembly: AssemblyDescription("AssemblyDescription")] [assembly: AssemblyConfiguration("AssemblyConfiguration")] [assembly: AssemblyCompany("Tollsoft")] [assembly: AssemblyProduct("AssemblyProduct")] [assembly: AssemblyCopyright("Copyright ©Tollsoft 2008")] [assembly: AssemblyTrademark("AssemblyTrademark")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] [assembly: Guid("948efa6b-af3a-4ba2-8835-b54b058015d4")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")]
Sie können die gewünschten Assembly-Informationen in der Datei AssemblyInfo.cs eintragen, Sie können aber auch die Einträge im Eigenschaftsdialog des Projekts vornehmen. Dazu öffnen Sie das Eigenschaftsfenster des Projekts und wählen die Lasche Anwendung. Auf dieser Registerkarte sehen Sie die Schaltfläche Assembly-Information…, über die der Dialog aus Abbildung 7.4 geöffnet wird.
Abbildung 7.4 Eintragen der Assembly-Informationen in Visual Studio 2010