10.8 Attribute
Ein Attribut ist ein Feature von .NET, das einer Komponente deklarativ Zusatzinformationen bereitstellt oder einfach nur alleine durch seine Anwesenheit bestimmte Operationen ermöglicht oder gar steuert. Attribute gehören zu den Metadaten eines Programms und können zur Laufzeit ausgewertet werden.
Abbildung 10.1 Die Metadaten der Klasse »GeometricObject«
Anmerkung
Metadaten sind ein Pflichtbestandteil jeder .NET-Anwendung. Metadaten beschreiben sprachneutral die gesamte Struktur einer Assembly. Dazu gehören alle enthaltenen Typen, Methoden, Eigenschaften, Ereignisse, Konstruktoren, implementierte Interfaces usw. Zudem gehören zu den Metadaten auch weiter gehende Informationen, beispielsweise die Parameterlisten der Methoden einschließlich der Typangabe, die Sichtbarkeit der einzelnen Komponenten, Basisklassenangaben und viele andere Details. Der Vorteil ist, dass die Metadaten mit Hilfe der Reflection-API gelesen und ausgewertet werden können. Dazu muss noch
nicht einmal eine Klasse geladen, geschweige denn ein Objekt erstellt werden. Die IntelliSense-Liste greift zum Beispiel auch die Metadaten ab, woher sollte sie auch sonst die Informationen beziehen?
Sie können sich die Metadaten natürlich selber ansehen. Dazu stellt Visual Studio das Tool IL-Disassembler (ildasm.exe) zur Verfügung. Wenn Sie das Tool starten, müssen Sie nur die entsprechende EXE-Datei angeben, um die Metadaten zu sehen. Abbildung 10.1 zeigt exemplarisch das Kompilat der Datei GeometricObjects.exe des Beispielprogramms GeometricObjectsSolution_11.
Mit einem Attribut lässt sich das Laufzeitverhalten praktisch aller .NET-Komponenten beeinflussen: Assemblys, Klassen, Interfaces, Strukturen, Delegates, Enumerationen, Konstruktoren, Methoden, Eigenschaften, Parameter, Ereignisse, ja sogar die Rückgabewerte von Methoden.
Ein ganz typisches Attribut ist das SerializableAttribute. Es kann zum Beispiel mit einer Klasse, Enumeration, Struktur oder einem Delegaten verknüpft werden, z. B.:
[SerializableAttribute]
public class Circle { [...] }
Und wozu dient dieses Attribut? Die Antwort ist ganz einfach: Es legt allein mit seiner Anwesenheit fest, dass der Typ, hier Circle, binär serialisiert werden kann. Ist das Attribut nicht vorhanden, geht es nicht. Somit kommt diesem Attribut, wie vielen anderen auch, einzig und allein die Bedeutung einer booleschen Variablen zu, die true oder false gesetzt ist. Der Clou an der Sache ist, dass dazu nicht erst eine Klasse geladen oder gar ein Objekt erstellt werden muss. Stattdessen werden zur Laufzeit nur die Metadaten per Reflection ausgewertet, und die Information steht bereit, da Attribute in den Metadaten zu finden sind.
Basisklasse aller Attribute ist die abstrakte Klasse System.Attribute. Schaut man in die Online-Dokumentation zu Attribute, wird man feststellen, dass das .NET Framework sehr viele Attribute vordefiniert. 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.
10.8.1 Das »Flags«-Attribut
Wir wollen uns die Wirkungsweise der Attribute exemplarisch am Attribut FlagsAttribute ansehen, das zum .NET Framework gehört und ausschließlich mit Enumerationen verknüpft werden kann. Mit dem Attribut lässt sich angeben, dass die Enumeration auch als Kombination von Bits, also ein Bit-Feld, aufgefasst werden kann.
Das wollen wir uns an einem Beispiel ansehen. Nehmen wir an, wir hätten eine benutzerdefinierte Enumeration namens Keys bereitgestellt, die drei Zustandstasten , und beschreibt.
public enum Keys {
Shift = 1,
Ctrl = 2,
Alt = 4
}
Listing 10.37 Benutzerdefinierte Enumeration »Keys«
Die Enumerationsmitglieder sollen nun befähigt werden, als Bit-Feld interpretiert zu werden. Dazu wird das Attribut in eckige Klammern gefasst und vor der Definition der Enumeration angegeben.
[FlagsAttribute]
public enum Keys {
Shift = 1,
Ctrl = 2,
Alt = 4
}
Listing 10.38 Benutzerdefinierte Enumeration »Keys« mit dem »FlagsAttribute«
Nun können wir eine Variable vom Typ Keys deklarieren und ihr einen Wert zuweisen, der den Zustand der beiden gleichzeitig gedrückten Tasten und beschreibt. Beide Member verknüpfen wir mit dem |-Operator:
Keys tastenkombination = Keys.Ctrl | Keys.Shift;
Mit den bitweisen Operatoren kann nun 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");
Listing 10.39 Prüfen, ob eine bestimmte Tastenkombination gedrückt ist
Hier wird natürlich die Ausgabe lauten, dass die -Taste nicht gedrückt ist, da die Variable tastenkombination die beiden Tasten und beschreibt.
Eine ähnliche Enumeration, die dann allerdings jede Taste der Tastatur beschreibt, gibt es übrigens auch im Namespace System.Windows.Forms.
Lassen wir uns nun den Inhalt der Variablen tastenkombination mit
Console.WriteLine(tastenkombination.ToString());
ausgeben, erhalten wir:
Shift, Ctrl
Hätten wir FlagsAttribute 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
Attributbezeichner 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 seine 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.:
[FlagsAttribute]
[Obsolete("Diese Enumeration ist veraltet.");
public enum Keys {
[...]
}
Listing 10.40 Verknüpfung von »Keys« mit den Attributen »Obsolete« und »Flags«
Anmerkung
Innerhalb einer mit Flags verknüpften Enumeration können Sie zur Zuweisung auch die bereits in der Enumeration angegebenen Konstanten verwenden. Stellen Sie sich beispiels-
weise vor, in unserer Keys-Enumeration soll zusätzlich der Member All hinzugefügt werden, der alle anderen Enumerationsmember beschreibt. Sie können das wie folgt umsetzen:
[FlagsAttribute]
public enum Keys {
Shift = 1,
Ctrl = 2,
Alt = 4
All = Shift | Ctrl | Alt
}
10.8.2 Benutzerdefinierte 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.
Obwohl das .NET Framework zahlreiche Attribute vordefiniert, können Sie auch für eigene Zwecke Attributklassen selber schreiben. Allerdings müssen Sie für die Auswertung des Attributs zur Laufzeit dann auch selber sorgen.
Drei Punkte müssen Sie beachten, um ein benutzerdefiniertes Attribut zu programmieren:
- Der Definition eines benutzerdefinierten Attributs selbst geht immer die Definition des Attributs AttributeUsageAttribute voraus.
- Die Klasse wird aus Attribute abgeleitet.
- Dem 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
{
[...]
}
Listing 10.41 Grundstruktur des benutzerdefinierten Attributs »DeveloperAttribute«
Die Voranstellung des Attributs AttributeUsage vor der Klasse legt elementare Eigenschaften der neuen Attributklasse fest. In diesem Zusammenhang sind drei Parameter besonders interessant:
Während AttributeTargets angegeben werden muss, sind die beiden anderen Angaben optional.
AttributeTargets
Jedes Attribut kann sich nur auf bestimmte Codeelemente auswirken. Diese werden mit AttributeTargets bekannt gegeben. Das Attribut Flags kann beispielsweise mit Klassen, Enumerationen, Strukturen und Delegaten verknüpft werden. 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 als Enumeration vordefiniert und weist seinerseits selbst das FlagsAttribute auf, um mehrere Zielelemente angeben zu können. In Tabelle 10.2 finden Sie alle möglichen Elemente, die generell mit Attributen verknüpft werden können.
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 den 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. |
Im folgenden Listing sehen Sie den Teilausschnitt unseres benutzerdefinierten Attributs. Das Attribut kann entweder Klassen oder Methoden angeheftet werden.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DeveloperAttribute : Attribute { [...] }
Listing 10.42 Die Zielmember des Attributs angeben
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.
Felder und Konstruktoren eines Attributs
Sie können in Attributklassen öffentliche Felder und Eigenschaften definieren, deren Daten an den Benutzer des Attributs weitergeleitet werden. Initialisiert werden die Felder über Konstruktoren.
Unser DeveloperAttribute soll nun um die beiden Felder Name und Identifier ergänzt werden. Das Feld Name wird beim Konstruktoraufruf initialisiert.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DeveloperAttribute : Attribute {
public string Name { get; set; }
public int Identifier { get; set; }
public DeveloperAttribute(string name) {
Name = name;
}
}
Listing 10.43 Vollständiges benutzerdefiniertes Attribut
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, sehen Sie sich an, wie das Attribut auf eine Klasse angewendet wird:
[DeveloperAttribute("Meier")]
public class Demo {
[...]
}
Listing 10.44 Verwenden des benutzerdefinierten Attributs
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.
Positionale und benannte Parameter
Um Identifier einen individuellen Wert zuzuweisen, lässt sich DeveloperAttribute auch wie folgt mit der Klasse verknüpfen:
[DeveloperAttribute("Meier", Identifier = 8815)]
public class Demo {
[...]
}
Listing 10.45 Verwendung positionaler und benannter Parameter
Beachten Sie, dass wir jetzt zwei Argumente übergeben, obwohl der Konstruktor nur einen Parameter definiert. 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.
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.
Verknüpfen Sie ein Attribut mit einem Element und verwenden dabei positionale und benannte Parameter, 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.
10.8.3 Attribute auswerten
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 DeveloperAttribute gezeigt werden. Beachten Sie hier bitte, dass der Namespace System.Reflection bekannt gegeben werden muss.
// Beispiel: ..\Kapitel 10\AttributeSample
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 { get; set; }
public int Identifier { get; set; }
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();
}
}
Listing 10.46 Beispielprogramm zur Auswertung eines Attributs
Das benutzerdefinierte Attribut DeveloperAttribute ist identisch mit demjenigen, das wir bereits vorher in diesem Abschnitt behandelt haben. Es erlaubt, mit einer Klasse oder einer Methode verknüpft zu 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.
Zur Beantwortung der Frage, 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:
Um eine Methode zu untersuchen, ist es die folgende Überladung:
Im ersten Argument geben wir den Typ des zu untersuchenden Elements an, im zweiten Parameter den Typ des Attributs. Zur Beschreibung des Typs mittels Code 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))
- unter Aufruf der Methode GetType() auf eine Objektreferenz (z. B. myObject.GetType())
Um die Attribute einer Klasse auszuwerten, übergeben Sie der Methode GetCustomAttribute nur den Typ 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-Objects 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 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");
Listing 10.47 Auswertung, ob ein Attribut gesetzt ist
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.
10.8.4 Festlegen der Assembly-Eigenschaften in »Assembly-Info.cs«
Jedes .NET-Projekt weist neben den Quellcodedateien auch die Datei AssemblyInfo.cs auf, die Metadaten über die Assemblierung beschreibt. 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")
Listing 10.48 Die Datei »AssemblyInfo.cs«
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 in Abbildung 10.2 gezeigte Dialog geöffnet wird.
Abbildung 10.2 Eintragen der Assembly-Informationen in Visual Studio 2012
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.