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]])] |
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.