4.8 Attribute
Klassen und Klassenmitglieder können in ihrer Funktionalität durch Attribute erweitert werden. Damit verändern Sie nicht die Klasse selbst, sondern geben anderen Klassen ergänzende Informationen. Die Klasse selbst hat in der Regel kein Interesse an Attributen. Daher werden Attribute eingeführt, wenn einer anderen Klasse die Typdefinition dieser Klasse nicht ausreicht, um mit ihr zusammenzuarbeiten. Zum Beispiel gibt es Klassen im .NET, die Objekte speichern und dazu die objektgebundenen Variablen der Reihe nach durchlaufen. Soll dabei nicht alles gesichert werden, müssen die (nicht) zu sichernden Elemente gekennzeichnet werden. Zum Beispiel ist es bei der Sicherung eines Computerbenutzers sinnvoll, dessen Namen zu sichern, aber das Kennwort des Zugangs bei der Speicherung zu überspringen (sonst kann jeder es lesen). In einem solchen Fall würden Sie das Kennwort mittels eines Attributs als nicht zu sichern kennzeichnen. Ein weiteres Beispiel ist das Attribut DllAttribut zur Deklaration externer Funktionen (siehe Abschnitt 3.4.3, »Externe Funktionen«).
Attribute werden vor Modifikatoren wie Public in spitzen Klammern deklariert. In der folgenden Syntax sind optionale Teile in eckige Klammern gesetzt, Alternativen durch | getrennt, und kursive Namen müssen Sie Ihren Bedürfnissen anpassen. Mehrere Attribute können entweder als Einzelattribute hintereinander geschrieben oder kommagetrennt in dieselben spitzen Klammern gesetzt werden.
<[Assembly:|Module:] Name [([Positionsargumente [, Eigenschaftswerte]])] [, weitere Attribute]> |
Durch diese Syntax wird ein Attribut lediglich deklariert. An einer anderen Stelle muss eine Klasse mit dem Namen des Attributs definiert werden, die von System.Attribute abgeleitet ist. Ob sich daraufhin das Verhalten der Klasse oder Methode ändert, obliegt der Methode, die das Attribut auswertet (wie es einige Methoden des .NET Frameworks machen). Bei der Deklaration sind einige Dinge zu beachten:
- Attribute sind optional, ihre Reihenfolge ist beliebig.
- Wenn der Compiler nach dem Anhängen von Attribute an den Namen ein Attribut nicht findet, wird die Suche ohne das Suffix Attribute wiederholt.
- Attribute müssen zur Compilezeit vollständig festgelegt sein, insbesondere sind alle Werte Konstanten.
- Die runden Klammern parameterloser Attribute sind optional.
- Typparameter können nicht in Attributargumenten verwendet werden (konkrete generische Typen sind erlaubt).
- Durch Assembly: oder Module: gekennzeichnete Attribute müssen nach Option- und Imports-Anweisungen und vor allen anderen Deklarationen stehen.
Die Positionsargumente werden an den Konstruktor der Attributklasse weitergereicht. Die Werte von öffentlichen Instanzvariablen oder -eigenschaften werden in beliebiger Reihenfolge, aber nach den Positionsargumenten durch
Eigenschaft := Wert |
festgelegt. Die folgenden Unterabschnitte werden den Gebrauch von Attributen und deren Definition deutlich machen. Dies ist nur eine sehr kleine Auswahl. Ich habe 443 Attribute im .NET Framework gezählt; einen kleinen Ausschnitt zeigt Tabelle 4.1. Ein Beispiel für ein Attribut haben Sie schon in Abschnitt 3.4.3, »Externe Funktionen«, kennengelernt.
Attribut | Beschreibung |
AttributeUsageAttribute |
Anwendungsbereich eines benutzerdefinierten Attributs |
CLSCompliantAttribute |
Markierung als standardkonform |
ContextStaticAttribute |
Werte statischer Felder, nach Kontext getrennt |
FlagsAttribute |
Verwendung einer Enumeration als Bitfeld |
LoaderOptimizationAttribute |
Hinweis zur Optimierung für den Klassenlader |
MTAThreadAttribute |
Multithreading für COM-Interaktion |
NonSerializedAttribute |
Serialisierung eines Feldes unterdrücken |
ObsoleteAttribute |
Hinweis auf veraltete Programmelemente |
ParamArrayAttribute |
Das zu ParamArray korrespondierende Attribut |
SerializableAttribute |
Datentyp als serialisierbar markieren |
STAThreadAttribute |
Einzelthread für COM-Interaktion |
ThreadStaticAttribute |
Werte statischer Felder, nach Thread getrennt |
4.8.1 Beispiel: Bedingte Kompilierung
Oft wird das Flags-Attribut für Enumerationen zur Einführung verwendet. Da es nach meinen Tests optional zu sein scheint, das Attribut explizit anzugeben, zeige ich ein Beispiel für bedingte Kompilierung. Das Attribut Conditional steuert, ob ein Methodenaufruf in die ausführbare Datei übernommen wird. Dazu wird geprüft, ob die als Zeichenkette übergebene Compilerkonstante definiert ist. Dies können Sie entweder über die Projekteinstellungen machen, oder Sie platzieren eine #Const-Anweisung im Quelltext. Das folgende Beispiel definiert eine Ausgabemethode, die nur dann ausgeführt werden soll, wenn die Konstante An definiert ist. In der Methode Test() steht je ein Aufruf der Methode vor und nach der Definition der Konstanten. Die Zahl 0 und False als Wert für die Konstante sind gleichwertig.
'...\Datentypen\Attribute\Bedingung.vb |
Option Strict On Namespace Datentypen Module Bedingung <Conditional("An")> Sub Ausgabe(ByVal text As String) Console.WriteLine(text) End Sub Sub Test() Ausgabe("Maier") #Const An = True Ausgabe("Schulze") Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe des Programms zeigt, dass nur der zweite Aufruf erfolgt:
Schulze
4.8.2 Codeerzeugung: DesignerGenerated
Einige Werkzeuge in Visual Studio, wie zum Beispiel der grafische Designer für Windows-Anwendungen, markieren eine Klasse mit dem Attribut DesignerGenerated. Dies hat unter anderem den Effekt, dass der Standardkonstruktor implizit die Methode InitializeComponent aufruft. Dies zeigt zum einen, dass auch Klassen mit Attributen versehen werden können, und zum anderen, dass auch ausführbarer Code implizit durch Attribute erzeugt werden kann. Das nächste Codefragment zeigt eine Klasse Rechner, die mit dem Attribut DesignerGenerated markiert ist und keinen expliziten Konstruktor enthält.
'...\Datentypen\Attribute\Implizit.vb |
Option Strict On Namespace Datentypen <Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> Class Rechner Sub InitializeComponent() Console.WriteLine("In InitializeComponent") End Sub End Class Module Implizit Sub Test() Dim r As New Rechner() Console.ReadLine() End Sub End Module End Namespace
Wie die Ausgabe zeigt, enthält der vom Compiler automatisch generierte parameterlose Standardkonstruktor einen Aufruf von InitializeComponent(), der nirgendwo im Quelltext steht:
In InitializeComponent
4.8.3 Standardinstanzen: MyGroupCollectionAttribute
Manchmal ist es syntaktisch notwendig, statt einer Klasse ein Objekt vom Typ der Klasse zu verwenden. Dann ist es praktisch, einen Mechanismus zur Hand zu haben, der einem das benötigte Objekt verschafft. Das Attribut MyGroupCollection kennzeichnet eine Klasse als Fabrik für solche Standardinstanzen. Das erste Argument des Attributs spezifiziert eine oder mehrere durch Komma getrennte Klassen, für deren abgeleitete Klassen jeweils ein Objekt erstellt wird, auf das über eine gleichnamige Eigenschaft der Fabrik zugegriffen werden kann. Die Erstellung übernimmt die im zweiten Argument angegebene Methode, während das dritte Argument auf die Methode verweist, die aufgerufen wird, wenn ein Standardobjekt zerstört wird. Das vierte Argument würde einen Ausdruck spezifizieren, den der Compiler gegebenenfalls anstelle des Klassennamens verwenden darf, wenn dies im Compiler implementiert wäre. Der Mechanismus wird von Klassen zur Windows- und Webprogrammierung genutzt.
Das folgende Beispiel zeigt eine solche Erzeugungsklasse namens Fabrik und eine kleine Klassenhierarchie mit der Basisklasse Licht. Die Methode Test() nutzt die Standardinstanzen der abgeleiteten Klassen, ohne dass diese im Quelltext erzeugt werden müssen – der Compiler fügt den nötigen Code automatisch hinzu. Dieser Automatismus greift auch für jede neu definierte Klasse, die von Licht abgeleitet ist. So können Sie die Erzeugung einer Standardinstanz nicht aus Versehen vergessen – eine Standardinstanz existiert garantiert.
'...\Datentypen\Attribute\Standard.vb |
Option Strict On Namespace Datentypen <Microsoft.VisualBasic.MyGroupCollection( _ "Attribute.Datentypen.Licht,", "Neu", "Aus", "Pseudonym" _ )> Class Fabrik Private Shared Function Neu(Of T As {New, Licht})(ByVal obj As T) As T If obj Is Nothing Then Return New T() Else Return obj End Function Private Shared Sub Aus(Of T As Licht)(ByRef aktion As T) Console.WriteLine("Licht {0} ausmachen", aktion.was) aktion = Nothing End Sub End Class Class Licht : Public was As String = "hell" : End Class Class Kerze : Inherits Licht : Public was As String = "Kerze" : End Class Class Lampe : Inherits Licht : Public was As String = "Lampe" : End Class Module Standard Sub Test() Dim fab As New Fabrik() Console.WriteLine("Standardkerze: {0}", fab.Kerze.was) Console.WriteLine("Standardlampe: {0}", fab.Lampe.was) fab.Kerze = Nothing Console.ReadLine() End Sub End Module End Namespace
Die ersten beiden Ausgaben reflektieren die automatisch generierten Instanzen der abgeleiteten Klassen. Für die Basisklasse, hier Licht, wird kein Standardobjekt erzeugt. Wie die letzte Ausgabe zeigt, wird bei Vernichtung der Standardinstanz automatisch die Methode Aus() in Fabrik aufgerufen. Da diese eine Basisklassenreferenz vom Typ Licht verwendet und die Variable was nicht polymorph ist, wird die Bezeichnung der Basisklasse Licht ausgegeben.
Standardkerze: Kerze Standardlampe: Lampe Licht hell ausmachen
4.8.4 Klassenerweiterungen: Extension
Mit Attributen lässt sich auch eine Klasse erweitern, ohne die Klasse selbst ändern zu müssen. Ich halte diese Art der Definition von Methoden zwar für komfortabel, aber sie macht die Klassendefinition sehr viel unübersichtlicher. Es reicht nicht mehr, die Beschreibung einer Klasse zu konsultieren, sondern es müssen alle Erweiterungen nachgeschlagen werden, um den vollen Funktionsumfang zu ermitteln. Sind diese nicht sauber bei der Klasse dokumentiert, endet das Ganze schnell im Chaos. Bei entsprechender Disziplin sind die Möglichkeiten aber enorm. Tabelle 4.2 zeigt Klassen im .NET Framework, die Erweiterungsmethoden bereitstellen. Wenn Sie bei einer Klasse mehr als die dokumentierte Funktionalität vorfinden, lohnt es sich, diese in einer der gelisteten Klassen zu suchen.
Klasse |
Microsoft.VisualStudio.Tools.Applications.MarshalExtensions |
System.Data.Linq.SqlClient.SqlNodeTypeOperators |
System.Linq.Enumerable |
System.Linq.Expressions.ReadOnlyCollectionExtensions |
System.Linq.Queryable |
System.Web.Query.Dynamic.DynamicQueryable |
System.Xml.Linq.Extensions |
System.Xml.XPath.Extensions |
Beispiel
Als einfaches Beispiel dient uns der implizite Zugriff auf die Klasse Enumerable. Sie definiert für alle Klassen, die die Schnittstelle IEnumerable implementieren, eine ganze Reihe von Erweiterungsmethoden. Wir greifen uns hier die Methode ElementAt() heraus. In der Dokumentation zu Arrays werden Sie feststellen, dass die Methode für Arrays nicht definiert ist, aber Array die Schnittstelle IEnumerable implementiert. Um sicherzugehen, verwendet das Beispiel den Reflection-Mechanismus von .NET, um mit GetMethod() nach der Methode im Array und in Enumerable zu suchen. Bevor wir einen Zugriff mit ElementAt() in der letzten Ausgabe wagen, prüfen wir noch, ob das Array die Schnittstelle implementiert. Sollten Sie lieber darauf verzichten wollen, können Sie die Information auch anhand der Dokumentation des .NET Frameworks ermitteln.
'...\Datentypen\Attribute\Enumerable.vb |
Option Strict On Namespace Datentypen Module Enumerable Sub Test() Dim werte As Integer() = {2, 8, –2, 12} Console.WriteLine("Array hat Methode ElementAt: {0}", _ werte.GetType().GetMethod("ElementAt") IsNot Nothing) Console.WriteLine("Enumerable hat Methode ElementAt: {0}", _ GetType(System.Linq.Enumerable).GetMethod("ElementAt") IsNot Nothing) Console.WriteLine("Array hat Typ IEnumerable: {0}", _ werte.GetType().GetInterfaces().Contains(GetType(IEnumerable))) Console.WriteLine("Position 2: {0}", werte.ElementAt(2)) Console.ReadLine() End Sub End Module End Namespace
Die ersten beiden Ausgaben zeigen, dass die Methode ElementAt() nicht im Array enthalten ist, wohl aber in Enumerable. In der dritten Ausgabe sehen wir, dass das Array vom Typ IEnumerable ist. Für so einen Typ definiert Enumerable die Erweiterungsmethode ElementAt(), so dass das Element in der vierten Ausgabe gezeigt werden kann.
Array hat Methode ElementAt: False Enumerable hat Methode ElementAt: True Array hat Typ IEnumerable: True Position 2: –2
Hinweis |
IntelliSense erkennt die Methode als Erweiterung und markiert sie speziell in der Auswahlliste, in Texten mit <Extension>. |
Prinzip
Um die Idee der Erweiterungsmethoden zu erfassen, ist es am leichtesten, selbst eine Klasse mit einer Erweiterungsmethode zu definieren. Sie muss
- in einem Module stehen (Klassen sind nicht erlaubt, auch nicht, wenn die Methode mit Shared an die Klasse gebunden wird),
- durch das Attribut Extension markiert werden,
- für die nutzende Klasse ausreichende Sichtbarkeit haben (zum Beispiel Public),
- eine normale Methode sein und kein Operator,
- als ersten Parameter ein Objekt der zu erweiternden Klasse haben, das nicht optional sein darf und
- von einem Objekt und nicht einer Klasse angesprochen werden.
Darüber hinaus darf sie
- generische Typen enthalten, die beim Aufruf aufgelöst werden,
- Schnittstellen erweitern (ohne Einfluss auf Implements-Klauseln),
- das erweiterte Objekt als ByRef-Parameter verwenden,
- in AddressOf für ein Delegate verwendet werden und
- implizit aufgerufen werden, zum Beispiel durch eine For Each–Schleife.
Hinweis |
Object als Typ des ersten Parameters und eine frühe Bindung mit Option Strict On widersprechen sich. |
Um die Methode zu verwenden, rufen Sie sie über eine Objektreferenz des richtigen Typs auf, wobei Sie den ersten Parameter weglassen. Wenn die Methode nur einen Parameter hat, rufen Sie sie also ohne Parameter auf. Ist der weggelassene Parameter ein mit ByVal deklarierter Werttyp, wird er wie üblich als Kopie an die Methode übergeben. Das folgende Beispiel zeigt eine Klasse Staat, die durch das Module Geldquellen um die Methode Steuern() erweitert wird.
'...\Datentypen\Attribute\Erweiterung.vb |
Option Strict On Namespace Datentypen Module Geldquellen <System.Runtime.CompilerServices.Extension()> _ Friend Sub Steuer(ByVal s As Staat, ByVal wert As Double) Console.WriteLine("Steuer {0}", wert) If s IsNot Nothing Then s.budget += wert End Sub End Module Class Staat Friend budget As Double = 100 Shared Sub Test() Dim d As New Staat() Console.WriteLine("Budget vor Steuer: {0}", d.budget) d.Steuer(10) Console.WriteLine("Budget nach Steuer: {0}", d.budget) Console.ReadLine() End Sub End Module End Namespace
Die neue Methode wird korrekt angesprochen:
Budget vor Steuer: 100 Steuer 10 Budget nach Steuer: 110
Hinweis |
Sie müssen selbst auf Nullreferenzen in Erweiterungsmethoden aufpassen. |
Priorität
Die Reihenfolge der Auswertung sorgt dafür, dass Erweiterungsmethoden nachrangig angesprochen werden (ist ein Schritt erfolgreich, fällt der Rest weg):
- passende Methode in der Klasse selbst oder innerhalb der Klassenhierarchie aufrufen
- implizite Typumwandlungen
- passende Methode in der Klasse selbst oder innerhalb der Klassenhierarchie aufrufen
- Erweiterungsmethode aufrufen
Das nächste Beispiel zeigt diese Reihenfolge anhand der Anrede einer Person. Dazu wird eine Klasse Herr ohne Anrede definiert und eine davon abgeleitete Klasse Doktor mit Anrede. Wenn nichts Konkretes in der Klasse festgelegt ist, sollen zwei Methoden im Module Anreden greifen, die zwei Arten der Namensspezifikation unterstützen. Die Methode Anrede wird über ein Objekt des Typs Doktor mit einer Zeichenkette und einem Feld von Zeichen aufgerufen. Schließlich erfolgt ein Aufruf mit einer Referenz vom Typ Herr.
'...\Datentypen\Attribute\Reihenfolge.vb |
Option Strict On Namespace Datentypen Module Anreden <System.Runtime.CompilerServices.Extension()> _ Friend Sub Anrede(ByVal s As Herr, ByVal name As String) Console.WriteLine("Hallo " & name) End Sub <System.Runtime.CompilerServices.Extension()> _ Friend Sub Anrede(ByVal s As Herr, ByVal name As Char()) Console.WriteLine("Guten Tag " & name) End Sub End Module Class Herr : End Class Class Doktor : Inherits Herr Sub Anrede(ByVal name As String) Console.WriteLine("Herr Dr. " & name) End Sub End Class Module Reihenfolge Sub Test() Dim dr As New Doktor() Dim hr As Herr = dr dr.Anrede("Schmidt") dr.Anrede(New Char() {"S"c, "c"c, "h"c, "m"c, "i"c, "d"c, "t"c}) hr.Anrede("Schmidt") Console.ReadLine() End Sub End Module End Namespace
Die ersten beiden Ausgaben verwenden die in der Klasse definierte Methode. Dies zeigt, dass die implizite Typumwandlung noch vor der zweiten Erweiterungsmethode angewendet wird, die vom Typ her besser passt. Nur im dritten Fall hat die Klasse Herr keine passende Anrede, und es wird die Erweiterungsmethode verwendet.
Herr Dr. Schmidt Herr Dr. Schmidt Hallo Schmidt
Generisches: Map und Fold
In Bezug auf generische Typen stellen Erweiterungsmethoden nichts Besonderes dar. Das folgende Beispiel ist etwas schwerer verdaulich, dafür ist diese neue Idee extrem flexibel einsetzbar. Das Beispiel definiert Erweiterungsmethoden für Datensammlungen und verwendet, um es allgemein zu halten, statt eines konkreten Typs die generische Schnittstelle IEnumerable(Of TArg). Sie stellt sicher, dass wir eine For Each-Schleife verwenden können. Auf die Schnittstelle (und die verwendeten Listen) gehen wir im nächsten Kapitel ein. Hier reicht es zu wissen, dass Arrays diese Schnittstelle implementieren. Die Definition der Erweiterungsmethoden ist relativ komplex, aber der Gebrauch sehr einfach. Wenn Sie die Funktionalität nur verwenden wollen, reicht es, die Erklärung in den nächsten beiden Absätzen zu überfliegen.
Die erste Methode, Map(), wendet eine im zweiten Argument gegebene Funktion auf jedes Element der im ersten Argument (implizit) übergebenen Liste an. Dazu wird zuerst der Fall einer leeren Liste behandelt. Ist die Liste nicht leer, wird die Funktion auf das erste Listenelement angewendet und einer neuen Liste mit Add() hinzugefügt. Der Rest der Liste wird rekursiv hinzugefügt, indem Map() durch Skip(1) mit dem Rest der Eingabeliste aufgerufen und mit AddRange() der neuen Liste hinzugefügt wird (auf Listen gehen wir im nächsten Kapitel näher ein). Zum Beispiel ist das Ergebnis des Aufrufs Map({a, b, c}, f) die Liste {f(a), f(b), f(c)}.
Die zweite Methode, Fold(), startet mit einem Wert, der im dritten Argument übergeben wird. Danach wird dieser Wert mit dem ersten Element der im ersten Argument übergebenen Liste mittels der im zweiten Argument gegebenen Funktion »kombiniert«. Wie die Kombination aussieht, ist mehr oder weniger beliebig. Dieses neue Ergebnis wird dann mit dem dritten Listenelement verknüpft und so weiter. Zum Beispiel liefert der Aufruf Fold({a, b, c}, f, s) das Ergebnis f(f(f(s, a), b), c).
In der Methode Test() können Sie sehen, dass die Verwendung der Funktionen recht einfach ist. Zuerst wird eine beliebige Liste definiert, hier eine Liste ganzer Zahlen. In der ersten Ausgabe wird Map() auf dieser Liste mit einer Funktion aufgerufen, die die Listenelemente quadriert. Auf dem Ergebnis – das wieder eine Liste ist – wird Fold() aufgerufen, dessen Funktion die Listenelemente zu einer Zeichenkette verbindet. Die zweite Ausgabe verwendet Map() zum Verdoppeln der Listenelemente, während Fold() diese dann summiert. Schließlich werden die Initialen eines Namens bestimmt. Dazu wird ein Feld von Zeichenketten definiert sowie für Map() eine Funktion zur Umwandlung jedes Namensteils in Großbuchstaben und für Fold() eine Funktion zum Anfügen des ersten Buchstabens eines Namensteils.
Indem Sie die an Map() und Fold() übergebenen Funktionen ändern, können Sie also das Verhalten beliebig verändern, ohne den Quelltext von Map() oder Fold() antasten zu müssen.
'...\Datentypen\Attribute\Generisch.vb |
Option Strict On Namespace Datentypen Module Listenoperationen <System.Runtime.CompilerServices.Extension()> _ Function Map(Of TArg, TErg)( _ ByVal l As IEnumerable(Of TArg), _ ByVal f As Func(Of TArg, TErg) _ ) As IEnumerable(Of TErg) If l Is Nothing OrElse l.Count() = 0 Then Return New List(Of TErg)() Dim erg As New List(Of TErg)() erg.Add(f(l.First())) erg.AddRange(Map(l.Skip(1), f)) Return erg End Function <System.Runtime.CompilerServices.Extension()> _ Public Function Fold(Of TArg, TErg)( _ ByVal l As IEnumerable(Of TArg), _ ByVal f As Func(Of TErg, TArg, TErg), _ ByVal erg As TErg _ ) As TErg For Each i As TArg In l : erg = f(erg, i) : Next Return erg End Function End Module Module Generisch Sub Test() Dim z As Integer() = {2, 8, 5, 12} Console.WriteLine( _ z.Map(Function(x) x ^ 2).Fold(Function(s, n) s & " " & n, "")) Console.WriteLine( _ z.Map(Function(x) 2 * x).Fold(Function(s, n) s + n, 0)) Dim name As String() = {"herbert", "maier"} Dim gross As Func(Of String, String) = Function(x) x.ToUpper() Dim initial As Func(Of String, String, String) = _ Function(s, n) s & n.First() Console.WriteLine(name.Map(gross).Fold(initial, "")) Console.ReadLine() End Sub End Module End Namespace
Die Verschiedenartigkeit der Ausgabe unterstreicht noch einmal die Flexibilität des Konzepts:
4 64 25 144 54 HM
4.8.5 Attribute abfragen
Die Deklaration eines benutzerdefinierten Attributs hat keine Konsequenzen, außer dass sich die Metadaten eines Datentyps vergrößern. Damit Sie ein selbst definiertes Attribut zum Leben erwecken können, müssen Sie zur Laufzeit an geeigneter Stelle abfragen, ob und wie ein Attribut gesetzt wurde. Dies ist auch der Weg, den die Implementation der Klassen des .NET Frameworks beschreiten muss. Wir greifen im folgenden Beispiel auf vorhandene Attribute zu und befassen uns mit der Definition eines eigenen Attributs und dessen Auswertung im nächsten Abschnitt. Die Schleife gibt alle Attribute für die Assembly, den Datentyp Abfrage und die Methode Test() mittels der Methode GetCustomAttributes() aus. An die Assembly und die Methode kommen wir über Methoden im Namensraum System.Reflection. Danach geben wir den Wert des Modulattributs mit einer anderen Klasse aus, um einige der sonst notwendigen Typumwandlungen zu vermeiden.
'...\Datentypen\Attribute\Abfrage.vb |
Option Strict On Imports System.Reflection Namespace Datentypen <DebuggerDisplay("Modul Abfrage")> Module Abfrage <DebuggerHidden()> Sub Test() Dim typ As Type = GetType(Abfrage) Dim ass As Assembly = typ.Assembly Dim meth As MethodInfo = typ.GetMethod("Test") Dim typen As ICustomAttributeProvider() = {ass, typ, meth} For Each ca As ICustomAttributeProvider In _ New ICustomAttributeProvider() {ass, typ, meth} For Each attr As System.Attribute In ca.GetCustomAttributes(True) Console.WriteLine(attr) Next Console.WriteLine() Next Console.WriteLine(CustomAttributeData.GetCustomAttributes(typ)(1). _ ConstructorArguments(0).Value) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass einige Attribute für die gesamte Assembly sowie für Typen automatisch definiert werden. Die Methode zeigt nur das explizit definierte Attribut.
System.Reflection.AssemblyProductAttribute ... System.Runtime.InteropServices.GuidAttribute Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute System.Diagnostics.DebuggerDisplayAttribute System.Diagnostics.DebuggerHiddenAttribute Modul Abfrage
4.8.6 Benutzerdefinierte Attribute
Sie können selbstverständlich auch eigene Attributklassen definieren. Dabei ist Folgendes zu beachten:
- Der Name sollte auf Attribute enden.
- Die Attributklasse muss von System.Attribute abgeleitet sein.
- Die Attributklasse darf nicht mit MustInherit als abstrakt gekennzeichnet sein, aber die Vererbung sollte meistens mit NotInheritable unterbunden werden.
- Die Attributklasse darf kein generischer Typ sein oder Teil eines generischen Typs sein.
- Die Attributklasse sollte mit dem Attribut AttributUsage versehen sein.
- Der Konstruktor muss ausreichend sichtbar sein (Public oder Friend).
- Konstruktorparameter sind die Positionsargumente des Attributs und dürfen nicht mit ByRef deklariert werden.
- Benannte Parameter können Felder oder parameterlose Eigenschaften sein (wenn der Bezeichner ein Schlüsselwort ist, muss er in der Klassendefinition wie üblich in eckige Klammern gesetzt werden).
- Benannte Parameter müssen mit Public als öffentlich gekennzeichnet sein und dürfen weder mit ReadOnly schreibgeschützt noch mit Shared klassengebunden sein.
- Benannte Parameter dürfen nur Zahlen mit Ausnahme von Decimal sein (Enumerationen sind also zulässig) oder den Typ Boolean, Char, String, Object oder Type haben (siehe Abschnitt 2.5.4, »Einfache Datentypen«); außerdem sind eindimensionale Arrays dieser Arten erlaubt.
Das Attribut AttributUsage legt das neue Attribut genauer fest. Es hat drei Parameter, die in Tabelle 4.3 aufgelistet sind.
Parameter | Typ | Beschreibung | Art |
AllowMultiple |
Boolean |
Die mehrfache Anwendung des Attributs, ggf. mit verschiedenen Parametern, sichert es mehrfach in den Metadaten. |
optional benannt |
Inherited |
Boolean |
Das Attribut wird an Kindklassen weitergegeben. |
optional benannt |
ValidOn |
AttributeTargets |
Erlaubte Elemente für das Attribut, zum Beispiel Methode, Klasse |
Pflicht ReadOnly |
ValidOn ist zwingend und legt fest, auf welche Elemente das benutzerdefinierte Attribut angewendet werden darf (siehe Tabelle 4.4). Ein Module wird durch die Kompilierung einer einzelnen Datei erzeugt, eine Assembly fasst als Bibliothek oder ausführbare Datei diese zusammen. Der Datentyp Module wird vom Wert Class erfasst. Sollen verschiedene Stellen erlaubt sein, werden die Werte mit dem Operator Or verknüpft.
Auf was darf das Attribut wirken? | Werte |
Alles |
All |
Dateien |
Assembly, Module |
Datentyp |
Class, Struct, Enum, Interface, Delegate |
Klassenelement |
Constructor, Method, Property, Field, Event |
Parameter |
Parameter, GenericParameter |
Rückgabewert |
ReturnValue |
Das nächste Beispiel nutzt Attribute, um die Preisgabe von Informationen zu steuern. Dazu wird eine Klasse Arbeiter mit den Feldern Name und Gehalt definiert, die eine Methode Info() enthält, die abhängig von den Attributen der aufrufenden Methode das Gehalt ausgibt oder nicht. Die Methode, die Info() aufruft, wird über ein Objekt der Klasse StackTrace ermittelt, und dessen Attribute werden in einer For Each-Schleife durchlaufen. Wenn das Feld Art des Attributs gleich "Berater" ist, wird neben dem Namen auch das Gehalt ausgegeben. Das Attribut Funktion ist von Attribute abgeleitet und wird durch das Attribut AttributeUsage auf Methoden beschränkt. Es definiert einen erforderlichen Parameter namens Art im Konstruktor und einen optionalen im Feld Name. Danach werden zwei Klassen mit Methoden definiert, die die Methode Arbeiter.Info() aufrufen. Nur die Methode Klient() der Klasse Steuerberater hat dabei ein Funktion-Attribut, das die Ausgabe des Gehalts erlaubt. In der Methode Test() werden schließlich die drei Methoden der beiden Klassen aufgerufen.
'...\Datentypen\Attribute\Benutzer.vb |
Option Strict On Imports System.Reflection Namespace Datentypen Class Arbeiter Private Name As String Private Gehalt As Double Sub New(ByVal name As String, ByVal gehalt As Double) Me.Name = name : Me.Gehalt = gehalt End Sub Sub Info() Dim frager As MethodBase = (New StackTrace()).GetFrame(1).GetMethod() Dim meth As Object() = frager.GetCustomAttributes(True) Dim cls As Object() = frager.DeclaringType.GetCustomAttributes(True) For Each att As Object In meth.Concat(cls) If att.GetType() IsNot GetType(FunktionAttribute) Then Continue For Dim fun As FunktionAttribute = CType(att, FunktionAttribute) If fun.Art = "Berater" Then Console.WriteLine("{0}: {1} verdient {2}", fun.Name, Name, Gehalt) Else Console.WriteLine("{0}: Arbeiter heißt {1}", fun.Name, Name) End If Return Next Console.WriteLine("Arbeiter heißt {0}", Name) End Sub End Class <AttributeUsage(AttributeTargets. Method Or AttributeTargets.Class)> _ Class FunktionAttribute : Inherits System.Attribute Sub New(ByVal art As String) Me.Art = art End Sub Public ReadOnly Art As String Public Name As String 'keine Property, um das Beispiel kurz zu halten End Class Class Nachbar <Funktion("Bekannter", Name:="Danton")> Sub Frage(ByVal an As Arbeiter) Console.Write("Nachbar ") : an.Info() End Sub End Class <Funktion("Berater", Name:="Gouge")> Class Freund Sub Frage(ByVal an As Arbeiter) Console.Write("Freund ") : an.Info() End Sub End Class Class Steuerberater <Funktion("Berater", Name:="Necker")> Sub Klient(ByVal an As Arbeiter) Console.Write("Steuerberater ") : an.Info() End Sub Sub Fremder(ByVal an As Arbeiter) Console.Write("Steuerberater: ") : an.Info() End Sub End Class Module Benutzer Sub Test() Dim nb As New Nachbar(), fr As New Freund(), sb As New Steuerberater() Dim ar As New Arbeiter("Peroux", 19750) nb.Frage(ar) fr.Frage(ar) sb.Klient(ar) sb.Fremder(ar) Console.ReadLine() End Sub End Module End Namespace
Nur der Steuerberater und der Freund haben die richtigen Werte im Attribut Funktion, um das Gehalt auszugeben:
Nachbar Danton: Arbeiter heißt Peroux Freund Gouge: Peroux verdient 19750 Steuerberater Necker: Peroux verdient 19750 Steuerberater: Arbeiter heißt Peroux
Mehrfachanwendung
Als Beispiel für Mehrfachanwendung definieren wir ein Attribut namens DateiAttribute zur Speicherung von Informationen über die Quelltextdatei. Um das Beispiel klein zu halten, befinden sich die Attributdefinition und -verwendung in derselben Datei. Die Spezifikation der Information sollte nur auf Dateiebene erfolgen, und daher legen wir als Ziel Module fest. Die Mehrfachanwendung erlauben wir durch AllowMultiple:=True. Bei seiner Verwendung in der dritten und vierten Zeile, die bei datei- und anwendungsweiten Attributen immer vor den Klassendefinitionen erfolgen muss, geht dem Attribut Module: voraus, um es an die Datei zu binden.
'...\Datentypen\Attribute\Quelle.vb |
Option Strict On Imports System.Reflection <Module: Attribute.Datentypen.Datei("Quelle.vb"), _ Module: Attribute.Datentypen.Datei("Stephan Leibbrandt")> Namespace Datentypen <AttributeUsage(AttributeTargets.Module, AllowMultiple:=True)> _ Public Class DateiAttribute : Inherits System.Attribute Sub New(ByVal info As String) Me.Info = info End Sub Public ReadOnly Info As String End Class Module Quelle Sub Test() Dim datei As [Module] = GetType(Quelle).Module For Each att As System.Attribute In datei.GetCustomAttributes(True) If att.GetType() Is GetType(DateiAttribute) Then Console.Write(CType(att, DateiAttribute).Info & " : ") End If Next Console.ReadLine() End Sub End Module End Namespace
Die For Each-Schleife in der Methode Test() erfasst alle Dateiattribute. Sie sollten sich nicht auf die Reihenfolge verlassen.
Stephan Leibbrandt : Quelle.vb :
Vererbung
Attribute können Klassenhierarchien bilden wie ganz normale Klassen. Bei der Verwendung bestimmt der Parameter Inherited des Attributs AttributeUsage, ob eine Kindklasse das deklarierte Attribut übernimmt. Die Weitergabe ist unabhängig davon, ob das Attribut mehrfach angegeben werden darf. Wenn ein Attribut nur einfach angegeben werden darf, überschreibt eine Spezifikation in der Kindklasse die in der Elternklasse vollständig. Schnittstellen, Eigenschaften und Ereignisse geben auch mit Inherited:=True Attribute nicht weiter. Die Methoden innerhalb von Eigenschaften und Ereignissen wiederum verhalten sich ganz normal. Das folgende Codefragment testet diese Aussage.
'...\Datentypen\Attribute\Vererbung.vb |
Option Strict On Imports System.Reflection Namespace Datentypen <AttributeUsage(AttributeTargets.All, Inherited:=True)> _ Class Farbe : Inherits System.Attribute : End Class <Farbe()> Interface Graphik : End Interface Interface Zeichnung : Inherits Graphik : End Interface <Farbe()> Class Ellipse : Implements Zeichnung <Farbe()> Overridable ReadOnly Property Art() As String <Farbe()> Get Return ("Ellipse") End Get End Property End Class Class Kreis : Inherits Ellipse Overrides ReadOnly Property Art() As String Get Return ("Kreis") End Get End Property End Class Module Vererbung Sub Ausgabe(ByVal c As ICustomAttributeProvider, ByVal s As String) For Each a As Object In c.GetCustomAttributes(True) If a.GetType() Is GetType(Farbe) Then Console.Write(s & " farbig ") Next End Sub Sub Test() Ausgabe(GetType(Zeichnung), "Schnittstelle") Ausgabe(GetType(Kreis), "Klasse") Ausgabe(GetType(Kreis).GetProperty("Art"), "Eigenschaft") Ausgabe(GetType(Kreis).GetProperty("Art").GetGetMethod(), "Get") Console.ReadLine() End Sub End Module End Namespace
Wie erwartet geben Eigenschaften und Schnittstellen keine Attribute weiter:
Klasse farbig Get farbig
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.