Sind Aufzählungstypen noch zeitgemäß?
Einleitung
Viele populäre Programmiersprachen unterstützen Aufzählungstypen zur Gruppierung mehrerer Konstanten eines bestimmten ganzzahligen Datentyps. Die Vorteile gegenüber herkömmlichen Lösungen wie nur namentlich als zusammengehörig erkennbaren Konstanten sowie die Abstraktion von den eigentlichen Konstantenwerten durch Vergabe bedeutungsvoller Namen sind offensichtlich. In Anbetracht der sonstigen Entwicklung von Programmiersprachen wie dem zunehmenden Einzug von Objektorientierung muß erneut untersucht werden, ob Aufzählungstypen in der präsenten Form eine ideale Lösung darstellen und ob nicht alternative Möglichkeiten existieren, welche über das Potential verfügen, allfällige Beschränkungen von Aufzählungstypen aufzuheben.
Aufzählungstypen und Flags
Im Unterschied zur Programmiersprache Java, die bis zur Version 5.0 (2004) keine Aufzählungstypen unterstützte, haben diese in Visual Basic seit Version 5.0 (1997) Tradition. Bei Aufzählungstypen handelt es sich um Typen, die eine feste Anzahl benannter Konstanten gruppieren. Neben der Gruppierung von der Bedeutung nach zusammengehöriger Konstanten war sicher auch die dadurch verbesserte Unterstützung der Codierung über IntelliSense in Form einer Auswahlliste möglicher Konstanten für die Einführung von Aufzählungstypen in Visual Basic ausschlaggebend. Dies ist bei ungruppierten Konstanten nicht möglich, da der Bezug zwischen formalem Parametertyp und den möglichen Konstanten nicht durch die Entwicklungsumgebung hergestellt werden kann. In Umgebungen, die Aufzählungstypen nicht unterstützen, werden häufig Präfixe den Namen der Konstanten vorangestellt, um deren Zusammengehörigkeit Ausdruck verleihen, etwa WS_BORDER
, WS_CAPTION
, WS_CHILD
etc. in der Win32-Programmierschnittstelle.
In verschiedenen Programmiersprachen unterscheiden sich Aufzählungstypen in der gebotenen Typsicherheit. So fassen manche Programmiersprachen Konstanten einer Aufzählung lediglich als Pseudonyme bestimmter numerischer Werte auf, sodaß es möglich ist, Werte unterschiedlicher Aufzählungen und numerischer Datentypen miteinander zu kombinieren oder einander zuzuweisen und im Quellcode zu mischen. In anderen Programmiersprachen wie etwa Visual Basic .NET mit aktiviertem Option Strict
sind keine impliziten Typumwandlungen zwischen Werten verschiedener Aufzählungstypen und ganzzahliger Datentypen vorgesehen. Stattdessen sind zu diesem Zweck explizite Typumwandlungen im Quellcode erforderlich.
Aufzählungstypen besitzen in .NET einen ganzzahligen Basistyp, der den Typ der einzelnen Konstanten der Aufzählung vorgibt. Während die Anzahl an Konstanten, die in der Aufzählung definiert werden können, keiner praktisch relevanten Begrenzung unterliegt, wird die Anzahl der in Form vom Flags miteinander in einem Wert des Aufzählungstyps kombinierbaren Konstanten durch die Bitbreite des Basistyps beschränkt. Unter Flags versteht man benannte boolesche Variablen. Der kompakteren Handhabung und leichteren Persistierbarkeit bei zugleich geringen Kosten für Verarbeitung und Speicherung wegen können mehrere Flags in einem Wert eines Aufzählungstyps zusammengefaßt gespeichert werden. Die gesetzten Bits geben Aufschluß über die gesetzten Flags. In einer Aufzählung des Datentyps Integer
, einem 32-Bit-Ganzzahldatentyp, können folglich bis zu 32 benannte Flags definiert werden.
In der Bitrepräsentation jeder der Konstanten einer Aufzählung von Flags ist genau eines der 32 Bits gesetzt. Wenngleich die Begrenzung auf 32 Flags in den meisten Fällen ebenfalls nicht von praktischer Bedeutung ist, kann es doch vorkommen, daß eine Aufzählung im Laufe der Zeit durch Erweiterung auf über 32 Flags anwachsen müßte. Folgendes Listing zeigt die Deklaration eines Aufzählungstyps in Visual Basic .NET. Das Attribut Flags
zeigt an, daß mehrere Konstanten der Aufzählung in einem Wert kombiniert gespeichert werden. Die Werte der einzelnen Konstanten entsprechen Zweierpotenzen.
Neben der Anzeige von Auswahllisten in IntelliSense zur Erleichterung der Codierung können Programmiersprachen auch das Verzweigen anhand eines Aufzählungswertes erlauben. In Visual Basic .NET steht zu diesem Zweck die Select Case
-Anweisung zur Verfügung. C# bietet mit switch
ebenfalls eine Verzweigungsanweisung. Im Gegensatz zu Select Case
muß es sich bei den Verzweigungsausdrücken von switch
jedoch um konstante Ausdrücke handeln, weshalb das Verzweigen zwar bei mittels Enum
definierten Aufzählungstypen funktioniert, nicht aber mit Aufzählungen wie Color
, Pens
und Brushes
, deren Elemente keine konstanten Werte besitzen.
Beschränkungen von Aufzählungstypen
Bei der Untersuchung der Grenzen von Aufzählungstypen ist es notwendig, zwischen jenen zu unterscheiden, deren Konstanten nicht bitweise verknüpft werden können und denjenigen, bei denen dies vorgesehen ist, um mehrere Optionen in einem Wert des Aufzählungstyps zu speichern. Zusammenfassend können die folgenden drei Kritikpunkte an Aufzählungstypen in der Form, wie sie in Visual Basic .NET und C# verfügbar sind, festgestellt werden:
Der Basistyp von Aufzählungstypen (Schlüsselwort
Enum
) muß ein ganzzahliger numerischer Datentyp sein. Es besteht keine Möglichkeit, Instanzen von Verweistypen als Werte von Elementen eines Aufzählungstyps zu spezifizieren, um zusätzliche Daten an die Elemente zu binden.-
Die Bitrepräsentation der einzelnen Optionen sollte ein für den Entwickler in den meisten Fällen ein transparentes Implementierungsdetail darstellen. Setzen und Auswerten gesetzter Flags in einem Wert eines Aufzählungstyps erfordern jedoch Wissen über Bitoperationen und die interne Implementierung von Aufzählungstypen.
Die dabei zum Einsatz kommenden bitweisen Operatoren sind semantisch unpassend. So wird der Operator
Or
benutzt, um zwei FlagsLetter.A
undLetter.B
zu kombinieren, wobei eigentlich einAnd
(„Optionen A und B sind gesetzt“) von der Bedeutung her treffender wäre. Auch das Auswerten gesetzter Flags mittels des OperatoresAnd
(AIsSelected = ((SelectedLetters And Letter.A) = Letter.A)
) ist unintuitiv und irreführend. Wird ein Aufzählungstyp zur Benennung mehrerer kombinierbarer Flags verwendet, so ist die Anzahl an möglichen Optionen durch die Bitbreite des Basistyps der Aufzählung beschränkt. Dies kann zu Problemen bei der Erweiterung des Aufzählungstyps um zusätzliche Elemente führen.
Aufzählungstypen für Elemente eines beliebigen Datentyps
In einigen Szenarien ist es sinnvoll, eine Aufzählung von benannten Instanzen eines Datentyps bereitzustellen, die in IntelliSense-Auswahllisten bei Benutzung des Datentyps im Quellcode angezeigt wird. Dies wird in .NET über gemeinsame Eigenschaften oder Felder einer Klasse bewerkstelligt. Der Klasse, die normalerweise nicht instanzierbar ist, kommt dabei lediglich die Funktion des Gruppierens der benannten Instanzen zu. In der Klassenbibliothek von .NET ist dies etwa bei den Datentypen Color
, Pen
(Pens
), Brush
(Brushes
) und einigen anderen der Fall, wobei im Falle des Datentyps Color
die eigenen gemeinsamen Eigenschaften in der Auswahlliste angezeigt werden.
Aufzählungstypen mit Elementen eines beliebigen Datentyps können in .NET über als NotInheritable
markierte Klassen, deren Instanzierung durch Definition eines privaten Konstruktors unterbunden wird, implementiert werden. Die Elemente der Aufzählung können entweder als gemeinsame Eigenschaften oder Felder definiert werden. Im nachstehenden Listing wird der Quellcode eines Aufzählungstyps mit Elementen eines bestimmten Datentyps wiedergegeben.
Mittels des Attributs cref
des undokumentierten completionlist
-Elementes kann im XML-Kommentar eines Datentyps der Datentyp festgelegt werden, dessen gemeinsame Mitglieder in der IntelliSense-Auswahlliste angezeigt werden sollen. Die Entwicklungsumgebung Visual Studio 2005 bietet eine Auswahl an möglichen Werten über IntelliSense an, wenn einer Variablen oder Eigenschaft eines bestimmten Datentyps ein Wert zugewiesen wird oder ein Wert in einem formalen Parameter dieses Datentyps übergeben werden soll. Leider funktioniert die automatische Anzeige der Auswahlliste nicht bei als ParamArray
markierten Parametern des Datentyps. Folgende Abbildung zeigt die von Visual Studio 2005 angebotene Auswahl der gemeinsamen Mitglieder der Klasse WindowParts
bei einer Zuweisung an eine Variable des Datentyps WindowPart
.
Das completionlist
-Element wird in Visual Studio 2005 anscheinend nur in Visual Basic .NET herangezogen, um entsprechende Auswahllisten anzubieten. In den Programmiersprachen C# und C++/CLI kann zwar das XML-Element angegeben werden, die Entwicklungsumgebung macht jedoch keinen Gebrauch davon. Wenngleich die Verwendung undokumentierter Elemente immer kritisch betrachtet werden muß, dürfte der Einsatz von completionlist
unproblematisch sein. Insbesondere beim Bereitstellen wiederverwendbarer Bibliotheken bietet es sich an, completionlist
zu spezifizieren, um ein zu den Klassen der Klassenbibliothek von .NET konsistentes Verhalten zu erzielen.
Die von completionlist
gebotene Möglichkeit, bei einem Datentyp jenen Datentyp anzugeben, dessen gemeinsame Mitglieder in IntelliSense angezeigt werden, erweist sich zwar als praktisch, ist jedoch unzureichend. Im Falle des Datentyps Brush
werden in der Auswahlliste nur jene Pinsel angezeigt, die von der Klasse Brushes
bereitgestellt werden, obwohl auch andere Klassen wie etwa SystemBrushes
eine Auswahl benannter Pinsel anbieten. Bei der Kompilierung des Datentyps Brush
, im Zuge derer auch die XML-Dokumentationsdatei erstellt wird, kann der Compiler nicht feststellen, welche Typen für die Auswahlliste relevante Eigenschaften enthalten. So könnte in einem Projekt eine eigene Auflistung spezieller Pinsel definiert werden, die häufig zum Einsatz gelangen.
Aus Sicht der Erweiterbarkeit und Flexibilität wäre es daher wünschenswert, die Typzuordnung in umgekehrte Richtung vornehmen zu können. Anstatt des completionlist
-Elementes, das einem Datentyp maximal einen Datentyp zum Befüllen der Auswahlliste zuordnen kann, könnte ein direkt an den die Aufzählung enthaltenden Datentypen anzugebendes XML-Element treten. Allerdings ist die Implementierung eines Datentyps zur Aufzählung von Elementen eines Datentyps oder dessen Untertypen recht aufwendig, zumal Klassen, selbst in Form der in C# verfügbaren statischen Klassen, von der Semantik her kein geeignetes Mittel zur Gruppierung zusammengehöriger benannter Instanzen darstellen. Problematisch ist jedoch auch das Einführen eines neuen syntaktischen Konstrukts zur semantisch korrekteren Auszeichnung derartiger Auszählungstypen für Elemente eines beliebigen Datentyps, da einige Klassen, darunter Color
, bereits selbst eine Auswahl benannter Elemente bereitstellen.
Des Weiteren besteht eine Doppelgleisigkeit zwischen IntelliSense-Auswahlliste und der Auswahl an möglichen Eigenschaftswerten im Eigenschaftenfenster von Visual Studio. Während der Inhalt ersterer, wie zuvor dargelegt, über das completionlist
-Element festgelegt wird, bezieht letztere ihren Inhalt über eine von der Klasse TypeConverter
abgeleiteten Klasse. Hierbei besteht zwar nicht mehr die Einschränkung auf einen einzigen Aufzählungstyp, jedoch ist der Implementierungsaufwand immens, falls der Typumwandler Elemente mehrerer Aufzählungstypen anbieten können soll. Der Typumwandler kann über das Attribut TypeConverter
einem oder mehreren Datentypen zugeordnet werden. Im folgenden Listing ist die Implementierung eines Typumwandlers für den zuvor definierten Aufzählungstyp zu sehen.
Die oben angegebene Klasse WindowPartConverter
stellt nur die gemeinsamen Eigenschaften des Datentyps WindowPart
bereit. Die Implementierung der Funktion GetStandardValues
kann auch dahingehend erweitert werden, die gemeinsamen Mitglieder anderer Datentypen zurückzugeben. Folgendes Listing zeigt die Implementierung einer weiteren Aufzählungsklasse ListViewWindowParts
und einer um Unterstützung für Elemente dieser Klasse erweiterte Implementierung von GetStandardValues
. Besitzt ein Objekt nun eine Eigenschaft des Typs WindowPart
, werden bei dessen Anzeige in einem PropertyGrid-Steuerelement, etwa im Eigenschaftenfenster von Visual Studio, sowohl die im Datentyp WindowParts
als auch in ListViewWindowParts
definierten Elemente zur Auswahl angeboten.
Aufzählungen von Flags eines beliebigen Datentyps
Zur Modellierung von Aufzählungen mehrerer Flags eines beliebigen Datentyps bietet sich die Verwendung von Mengen an. An Stelle des Setzens oder Prüfens einzelner Bits in einem Wert treten Mengenoperationen. Damit werden die Einschränkungen der Anzahl an möglichen Flags durch die Bitbreite des Basistyps des Aufzählungstyps und die Beschränkung auf ganzzahlige Werte umgangen. Auf eine Implementierung eines geeigneten Mengentyps wird hier verzichtet. Im Einfachsten Fall kann hierzu eine generische Liste (Klasse List(Of T)
) herangezogen werden. Ideal wäre eine direkte Unterstützung typsicherer Mengentypen durch das .NET Framework und die .NET-Programmiersprachen in Form entsprechender Klassen und Operatoren, die einen intuitiven Umgang erlauben.
Betrachten wir das Beispiel einer Personenverwaltung, bei der Personen (Klasse Person
) neben einem Namen (Eigenschaft Name
) bestimmte Rollen (Eigenschaft Roles
) einnehmen können. Die Menge möglicher Rollen soll dabei beliebig erweiterbar sein, einerseits durch Instanzierung von Rollen, andererseits durch Ableiten vom Rollenbasistyp. Eine Person kann keine, eine oder mehrere Rollen besitzen. Welche Rollen eine Person bekleidet, kann sich während der Lebenszeit eines Person
-Objekts ändern.
Zur Darstellung der Rollen einer Person sind mehrere Ansätze denkbar. Kann eine Person nur genau eine Rolle einnehmen, können verschiedene Rollen durch Ableiten von der Klasse Person
realisiert werden. Das Ändern der von einer Person bekleideten Rolle kann bei dieser Lösung nachträglich nur mit großem Aufwand geändert werden. Anstatt verschiedene Rollen durch Untertypen zu realisieren könnte man die Klasse Person
um eine Eigenschaft Roles
erweitern, deren Typ ein herkömmlicher Enum
-Aufzählungstyp ist. Mehrere Rollen können binär zusammengefaßt werden. Nachteilig an dieser Lösung ist, daß die Menge der verfügbaren Rollen schwer erweitert werden kann. Außerdem besteht keine Möglichkeit, die Rollen mit Eigenschaften und ggf. Methoden auszustatten.
Eine dritte Möglichkeit bietet eine an das Rollen-Entwurfsmuster angelehnte Implementierung, bei der Rollen als Instanzen einer Klasse Role
oder ihrer Untertypen repräsentiert werden. Der Typ der Eigenschaft Roles
ist dann im einfachsten Fall eine Liste des Typs List(Of Role)
oder idealerweise eine dynamisch veränderbare Menge [Set](Of Role)
. Bei dieser Lösung bestehen die Probleme der fehlenden oder schwierigen Erweiterbarkeit von Rollenzahl und -typ und der Änderung der von einer Person bekleideten Rollen zur Laufzeit nicht mehr.
Der einfacheren Benutzbarkeit sollen in der Basisklassenbibliothek einige allgemeine Rollen definiert werden. Dies geschieht durch Ableiten der Klassen Passenger
und Agent
von der Klasse Role
. Diese Standardrollen werden im Aufzählungstyp Roles
ähnlich dem zuvor gezeigten Aufzählungstyp WindowParts
implementiert. Benutzer der Bibliothek können durch Ableiten von Role
weitere Rollen definieren. Auch das Gruppieren dieser benutzerdefinierten Rollen in eigenen Aufzählungen ist denkbar. Allerdings kann über das completionlist
-Element zur Zeit nur ein Aufzählungstyp mit einem Datentyp verknüpft werden. Die Eigenschaften der Rollenaufzählungen können, je nach Erfordernis, Verweise auf bestehende Rolleninstanzen oder auf neu angelegte Instanzen zurückgeben.
Aufgrund des Ersetzbarkeitsprinzips kann einer Eigenschaft des Datentyps Role
ein Verweis auf eine Instanz des Datentyps Role
bzw. eines davon abgeleiteten Datentyps zugewiesen werden. Allerdings ist hierbei die Anzahl an möglichen Elementen nicht fest, wohl aber jene an bei der Kompilierung bekannten gemeinsamen öffentlichen Mitglieder des Typs oder eines seiner Untertypen. In der IntelliSense-Auswahlliste bei Zuweisung einer Variablen des Datentyps Role
sollten demnach die gemeinsamen Mitglieder aller bekannten Datentypen angeboten werden, deren Datentyp Role
oder einer der davon abgeleiteten Datentypen ist. Besitzt die Eigenschaft, der ein Wert zugewiesen werden soll, einen spezifischeren Datentyp als Role
, so wird eine entsprechende Auswahl der gemeinsamen Mitglieder angezeigt.
Der Wertebereich von Aufzählungen
Bei der Implementierung von Aufzählungstypen mit erweiterter Funktionalität ist von Bedeutung, ob ein Aufzählungstyp eine feste Menge von Elementen besitzt oder über eine mittels verschiedener Mechanismen erweiterbare Elementmenge verfügt. Weiterhin ist zu unterscheiden, auf welchem Wege eine Erweiterung der Aufzählung möglich ist. Durch Ableiten vom Elementtyp können speziellere Elementtypen erstellt werden, die aufgrund des Ersetzbarkeitsprinzips anstelle der generischeren Elementtypen benutzt werden können. Andererseits können neue Aufzählungen dieser Typen erstellt werden, welche die Liste von zur Kompilierungszeit bekannter benannter Instanzen erweitert. IntelliSense soll anschließend die Möglichkeit der Auswahl aus allen benannten Instanzen bieten oder ggf. auch das Anlegen eigener Instanzen zur Laufzeit unterstützen.
Folgendes Listing zeigt die Implementierung einer generischen Klasse ArithmeticOperation
zur Darstellung eines arithmetischen Operators. Von dieser Klasse können konkrete Operatoren abgeleitet und die in der Basisklasse definierte Methode Eval
entsprechend der repräsentierten Operation überschrieben werden. Die Klasse ArithmeticOperation
selbst bietet einige vordefinierte Operatoren in Form einer mit gemeinsamen Eigenschaften implementierten Aufzählung an. Auch hier ist eine Erweiterung sowohl durch Vererbung als auch durch Bereitstellen zusätzlicher Klassen für arithmetische Operationen denkbar. Operatoren können anschließend einer im allgemeinen Typ ArithmethicOperation
typisierten Variablen oder Eigenschaft zugewiesen und polymorph genutzt werden, indem die Methode Eval
aufgerufen wird.
Schlußwort
Mittels der von .NET und den .NET-Programmiersprachen angebotenen technischen und syntaktischen Möglichkeiten ist eine semantisch saubere Implementierung von Aufzählungen mit Elementen eines beliebigen Datentyps nicht möglich. Ist zudem noch die von Flags bekannte Mengensemantik gefragt, müssen zusätzliche Klassen implementiert oder Listen benutzt werden. Eine saubere architekturelle Trennung zwischen Attributen und XML-Kommentaren ist ebenfalls nicht vorhanden. Während die Anzeige von Klassen und deren Mitgliedern in Werkzeugkasten, Codeeditor und Eigenschaftenfenster der Entwicklungsumgebung über Attribute gesteuert wird, wird der Inhalt der IntelliSense-Auswahlliste über ein XML-Kommentarelement gesteuert. Das Nichtvorhandensein von dessen Dokumentation läßt vermuten, daß man sich der Unzulänglichkeit der momentanen Lösung bewußt ist.
Weiterführende Informationen
- Jon Skeet [MVP]: Enhanced enums in C#, Blogeintrag vom 5. Januar 2006.
- Ralf Westphal [MVP]: A C# Set class based on enums, Blogeintrag vom 7. Januar 2006.
- Enhancements in JDK 5 – Enums