Kategorie: Softwareentwicklung

Enumerationen in C# 👍 👎

Während man in anderen Programmiersprachen (wie beispielsweise PHP) eine Sammlung von Konstanten bemühen muss, um zumindest so etwas ähnliches wie einen Aufzählungstypen zu definieren, bietet uns C# hierfür eine komfortable und zugleich typsichere Lösung mittels enum an.

Häufig finden derartige Aufzählungen im Rahmen von Optionen für Methodenaufrufe Verwendung; auch im .NET-Framework begegnet ihr diesen sehr häufig (z. B. bei Dateioperationen mit der Enumeration FileMode). Als Beispiel soll hier das Recht eines Benutzers dienen. Zuerst definieren wir dazu eine entsprechende Enumeration:
Enumeration "Permission" mit den Rechten "Lesen", "Schreiben" und "Löschen"
0102030405
public enum Permission {    Read,    Write,    Delete}
Als nächstes erstellen wir eine (sehr minimalistische) Benutzer-Klasse, welche eine zu unserer Rechte-Enumeration kompatible Eigenschaft besitzt:
Klasse "User" mit Eigenschaft für Benutzerrecht
010203040506
public class User {    public Permission UserPermission {        get;        set;    }}
Jetzt können wir auch schon eine Benutzer-Instanz erzeugen und ein Recht zuweisen, beispielsweise "Lesen":
Benutzer mit Leserecht erzeugen
010203
User du = new User() {    UserPermission = Permission.Read};
Das Prinzip entspricht soweit grundsätzlich also dem Vorgehen wie bei jedem anderen Datentypen (→ allgemeinere Informationen) auch. Insbesondere können wir der Eigenschaft UserPermission z. B. nicht versehentlich einen Wert einer Aufzählung "Farbe" oder "Geschlecht" zuweisen. Nun wäre es noch praktisch, wenn wir auch auf ein spezifisches Benutzerrecht prüfen könnten – also implementieren wir eine entsprechende Methode in der Benutzer-Klasse:
Methode zur Rechteprüfung in "User"
010203
public bool CheckPermission(Permission permission) {    return (this.UserPermission == permission);}
Die Verwendung gestaltet sich nun denkbar einfach (ich schreibe es der Vollständigkeit wegen dennoch auf Smiley: tongue_out):
Rechteprüfung
0102030405
if(du.CheckPermission(Permission.Write)) {    // Recht vorhanden} else {    // Recht NICHT vorhanden}
Soweit können wir glaube ich zufrieden sein. Einen kleinen Schönheitsfehler gibt es jedoch: Wenn wir voraussetzen, dass ein Benutzer mit reinen Schreibrechten noch nichts löschen darf, so müssten wir ihm zwei Rechte zuweisen, wenn er es doch können soll. Wir könnten also eine Rechteliste führen – oder wir bedienen uns der Bitarithmetik und des Flags-Attributs (→ weitere Details zur Verwendung) und bringen beide Rechte in nur einem Feld unter.

Dazu passen wir zuerst einmal unsere Aufzählung an, da wir zu diesem Zwecke Zweierpotenzen als Werte benötigen und geben auch gleich das Attribut mit an. Der späteren Bequemlichkeit wegen, erstellen wir auch noch einen weiteren Wert ReadWrite, welchen wir mit einem bitweisen ODER aus Read und Write definieren:
Enumeration "Permission" mit Attribut und eigenen Werten versehen, sowie Erweiterung um "ReadWrite"
01020304050607
[Flags]public enum Permission {    Read = 1,    Write = 2,    Delete = 4,    ReadWrite = (Read | Write)}
Darüber hinaus müssen wir unsere Prüfmethode geringfügig anpassen:
Methode zur Rechteprüfung in "User" erweitern
010203
public bool CheckPermission(Permission permission) {    return ((this.UserPermission & permission) == permission);}
Nun können wir wieder ein Objekt erzeugen und erhalten die erwarteten Ergebnisse:
Benutzer mit Rechten erzeugen und prüfen
01020304050607080910111213
  // Benutzer mit Lese- und Schreibrecht erzeugenUser du = new User() {    UserPermission = (Permission.Read | Permission.Write)      // – oder auch -    UserPermission = Permission.ReadWrite};
// Rechte prüfen (einige Beispiele)du.CheckPermission(Permission.Read); // truedu.CheckPermission(Permission.Write); // truedu.CheckPermission(Permission.Delete); // falsedu.CheckPermission(Permission.ReadWrite); // truedu.CheckPermission(Permission.Read | Permission.Delete); // false
Eine derartige Vorgehensweise findet auch innerhalb des .NET-Frameworks Verwendung, z. B. bei Dateioperationen mit der Enumeration FileAccess.

Eigenschaften in C# 👍 👎

C# besitzt eine – meiner Meinung nach – äußerst elegante Implementierung für Akzessoren. Während es beispielsweise in Java üblich ist, nach dem Muster getEigenschaft() (und setEigenschaft(…)) zu verfahren, kombiniert C# Felder und Zugriffsmethoden zu sog. Properties.

Dies stellt einerseits die Kapselung der internen Daten sicher, vermeidet darüber hinaus jedoch eine Sammlung dutzender Zugiffsmethoden neben den eigentlichen Operationen einer Klasse. Sollte man lediglich "Standardzugriff" benötigen, wird einem dies besonders einfach gemacht – doch dazu später noch etwas mehr.

Als kleines Beispiel dazu wollen wir eine Klasse Person entwickeln, welche den Vor- und Nachnamen einer Person aufnehmen kann:
Klasse "Person" mit Eigenschaften (manuelle Implementierung)
010203040506070809101112131415161718192021
public class Person {    private string firstName;    public string FirstName {        get {            return this.firstName;        }        set {            this.firstName = value;        }    }
private string lastName; public string LastName { get { return this.lastName; } set { this.lastName = value; } }}
Wie man sieht, erstellen wir dazu private Felder für die Daten und definieren dann die Eigenschaft (per Konvention mit einem Großbuchstaben beginnend) mit den entsprechenden Zugriffsmethoden (get und set). Der Parameter value wird implizit zur Verfügung gestellt und enthält den jeweils zugewiesenen Wert.

Der Zugriff auf diese Daten ist nun ganz einfach möglich:
Zugriff auf Eigenschaften der Klasse "Person"
0102030405060708091011
  // Zuweisung nach ObjekterzeugungPerson ich = new Person();
ich.FirstName = "Holger"; // Abfrage liefert "Holger"ich.LastName = "Stehle"; // Abfrage liefert "Stehle"
// Zuweisung per ObjektinitialisierungPerson ich = new Person() {
FirstName = "Holger", LastName = "Stehle"}; /* Abfrage-Resultate wie oben */
Innerhalb der Zugriffsmethoden kann beispielsweise Validierungslogik untergebracht werden. Das bloße Zuweisen und Abfragen ohne besondere zusätzliche Arbeit ist jedoch auch noch deutlich kompakter möglich – C# implementiert die Zugriffsmethoden dann automatisch. Sogar die interne Eigenschaft kann man sich sparen:
Klasse "Person" mit Eigenschaften (automatische Implementierung)
0102030405060708091011
public class Person {    public string FirstName {        get;        set;    }
public string LastName { get; set; }}
Das Verhalten ist nun wie in unserer vorherigen Klasse, die Verwendung entsprechend ebenfalls identisch. Eine feinere Abstimmung der Zugriffsebenen ist ebenfalls möglich. Weitere Details zur Verwendung finden sich im MSDN.

Ressourcen freigeben in C# 👍 👎

Dank verwaltetem Code und automatischer Speicherbereinigung muss man sich bei C# zumeist nicht explizit darum kümmern, angeforderte Ressourcen wieder freizugeben. An manchen Stellen kann dies jedoch notwendig sein: Dies ist beispielsweise der Fall, wenn nicht verwaltete oder externe Ressourcen (z. B. Dateien, Datenbanken) involviert sind.

Zur Freigabe von Ressourcen sollte die entsprechende Dispose()-Methode aufgerufen werden, welche Objekte zur Verfügung stellen, welche IDisposable implementieren. Dies kann beispielsweise wie folgt passieren:
Ressourcen explizit freigeben
0102030405060708091011
Class obj = new Class();
try { /** * Anwendungs-Code **/} finally { if(obj != null) { obj.Dispose(); }}
Es gibt jedoch noch eine elegantere Variante, und zwar mit Hilfe der using-Anweisung:
Ressourcen implizit freigeben (empfohlen)
0102030405
using(Class obj = new Class()) {    /**     * Anwendungs-Code    **/}
Durch dieses Vorgehen werden die Ressourcen automatisch nach Verlassen des Gültigkeitsbereichs freigegeben; dazu wird im Hintergrund folgender Code generiert:
Generierter Code zur Freigabe der Ressource
01020304050607080910111213
{    Class obj = new Class();
try { /** * Anwendungs-Code **/ } finally { if(obj != null) { ((IDisposable) obj).Dispose(); } }}
Zu beachten gilt, dass der using-Block – wie bereits durch die umschließenden geschweiften Klammern zu sehen – einen eigenen Gültigkeitsbereich darstellt, was demnach bei Variablendeklaration und -verwendung berücksichtigt werden muss.

Operatorüberladung in C# 👍 👎

C++-Entwickler werden sich freuen (oder auch nicht Smiley: winking) und PHP-Entwickler wohl größtenteils irritiert sein: C# unterstützt das Überladen von Operatoren für eigene Datentypen. Das ist nun natürlich nicht neu, aber gemeinhin eher selten eingesetzt. Das im Übrigen nicht ganz ohne Grund, aber dazu später mehr.

Was bedeutet das nun konkret? Nun, Operatoren dürften den meisten zumindest aus dem mathematischen Sprachgebrauch bekannt sein: "+" und "-" sind beispielsweise solche Operatoren. Und diese werden auch genau in diesem Sinne bei den meisten Programmiersprachen – so auch bei C# – verwendet, wenn es um das Rechnen mit numerischen Datentypen geht. Unter C# dient "+" darüber hinaus auch der Konkatenierung von Zeichenketten und außerdem dem Hinzufügen von Ereignis-Behandlungsroutinen (und "-" dem Entfernen).

Hier sehen wir schon, dass zumindest manche dieser Operatoren vom Kontext abhängige Bedeutungen besitzen können. Tatsächlich können wir ein solches Verhalten auch für eigene Klassen implementieren und werden dies nun exemplarisch auch angehen.

Insbesondere im mathematischen Bereich, wo Operatoren klar definierte und hinreichend bekannte Semantik besitzen, bieten sich eigene Operatoren an. Wir werden dazu eine einfache Klasse zur Intervallarithmetik – also dem Rechnen mit Intervallen – implementieren, welche uns die beiden binären Operatoren "+" und "-" für Objekte dieses Typs anbietet. Dazu entwickeln wir als erstes eine ganz normale Klasse, ergänzen diese jedoch um zwei spezielle öffentliche, statische Methoden für die Operationen:
Intervall-Klasse mit Operatorüberladung zur Addition und Subtraktion
0102030405060708091011121314151617181920212223242526
public class Interval {    public double X {        get;        set;    }
public double Y { get; set; }

public static Interval operator +(Interval a, Interval b) { return new Interval() { X = (a.X + b.X), Y = (a.Y + b.Y) }; }
public static Interval operator -(Interval a, Interval b) { return new Interval() { X = (a.X – b.Y), Y = (a.Y – b.X) }; }}
Wichtig bei den beiden speziellen Methoden, ist also die Kennzeichnung durch das Schlüsselwort operator, gefolgt vom entsprechenden Symbol.

Die Verwendung ist jetzt denkbar einfach und intuitiv:
Erzeugung zweier Intervalle und Addition dieser zu einem neuen Intervall
01020304050607080910111213
  // Intervalle erzeugenInterval a = new Interval() {    X = 1,    Y = 3};
Interval b = new Interval() { X = 2, Y = 4};
// Intervalle zu neuem Intervall addierenInterval c = (a + b);
Die Subtraktion verhält sich natürlich analog dazu, nur eben mit "-". Intuitiv sollte man beim Thema Operatorüberladung im Übrigen ernst nehmen und nicht nur aus persönlicher Motivation heraus betrachten. Auf jeden Fall sollte man widersprüchliche Definitionen zur allgemeinen Ansicht vermeiden (also: bitte aus "+" kein "-" machen und umgekehrt). Damit würde man nämlich den Vorteil der Übersichtlichkeit wieder völlig zunichtemachen. Insofern solltet ihr bei eigenen Implementierungen immer darauf achten, eine nachvollziehbare Semantik einzuhalten und den Entwickler darüber informieren. Ein sicherlich vorbildliches Beispiel sind die relationalen Operatoren der DateTime-Struktur.

Zum Schluss gilt noch zu berücksichtigen, dass nicht alle Operatoren überladen werden können und relationale Operatoren paarweise überladen werden müssen. Detailliertere Informationen dazu findet ihr selbstverständlich wie immer im MSDN.

Erweiterungsmethoden in C# 👍 👎

Die meisten haben sicherlich bereits von den sog. Erweiterungsmethoden unter C# gehört, meinem Eindruck nach haben jedoch deutlich weniger eine konkrete Vorstellung davon oder sie gar bereits selbst verwendet. Dies ist jedoch ganz einfach und kann durchaus nützlich sein, weshalb ich hier eine kleine Einführung geben möchte.

Ganz allgemein lässt sich sagen, dass Typen (sowohl eingebaute, als auch selbst definierte) mittels Erweiterungsmethoden um neue Operationen ergänzt werden können – im Ergebnis also ähnlich zur herkömmlichen Ableitung von einer Klasse, doch dazu später noch ein paar Worte mehr.

Wir möchten in diesem Beitrag den DateTime-Typen insofern erweitern, dass er die komfortable Konvertierung eines DateTime-Objekts zu einem UNIX-Zeitstempel ermöglicht, was insbesondere sinnvoll sein kann, wenn alte (z. B. PHP-basierte) Projekte mit entsprechenden Werten in einer Datenbank auf ASP.NET umgestellt werden sollen.

Um dieses Verhalten zu implementieren ist nicht mehr nötig, als in einer statischen Klasse eine öffentliche statische Methode der gewünschten Funktionalität zu implementieren, welche jedoch einen speziellen ersten Parameter erhält:
Erweiterungsmethode für "DateTime" implementieren
01020304050607
public static class ExtensionMethods {    public static int ToUnixTimestamp(this DateTime dateTime) {        DateTime unixBaseTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
return Convert.ToInt32((dateTime.ToUniversalTime() – unixBaseTime).TotalSeconds); }}
Bei dem bereits angekündigten "speziellen ersten Parameter" handelt es sich also um "this DateTime", wobei das Schlüsselwort this der Hinweis dafür ist, dass der nachfolgende Typ (in diesem Fall DateTime) erweitert werden soll.

Die Verwendung ist anschließend denkbar einfach, da sich derartige Operationen aus Sicht des Entwicklers im weiteren Verlauf wie Instanzmethoden des entsprechenden Objekts verhalten:
Erweiterungsmethode für "DateTime" verwenden
010203040506
  // Zeitstempel für "jetzt"int unixTimestamp = DateTime.Now.ToUnixTimestamp();
// Zeitstempel für beliebiges DateTime-ObjektDateTime birthdate = new DateTime(1988, 1, 29);int unixTimestamp = birthdate.ToUnixTimestamp();
Der erste Parameter (also der mit this) wird dabei übrigens nicht mit angegeben, da er implizit durch die Instanz gegeben ist. Es gilt außerdem noch zu beachten, dass ggf. der Namensraum eurer Erweiterungsmethoden eingebunden werden muss, sofern er von dem des "Einsatzortes" abweicht.

Nun gilt natürlich noch zu klären (s. o.), wie sich dieses Vorgehen zur "normalen" Ableitung verhält. Dies ist tatsächlich gar nicht immer absolut festzumachen, was man im konkreten Fall einsetzen sollte, aus Sicht eines objektorientierten Designs sollte üblicherweise jedoch Vererbung bevorzugt werden, zumal Erweiterungsmethoden nur auf öffentliche Mitglieder zugreifen können und bei Namenskonflikten ggf. das Nachsehen haben. Es gibt jedoch Fälle, wo dies nicht möglich ist: So ist beispielsweise der eingebaute Typ string als sealed markiert, so dass davon nicht geerbt werden kann. In einem solchen Fall kann die Ergänzung per Erweiterungsmethode sinnvoll sein. Auch in unserem Beispiel halte ich den Einsatz von Erweiterungsmethoden für legitim, schon weil ein neuer Typ (etwa "DateTimeWithUnixTimestampCapability"? Smiley: winking) nur begrenzt sinnvoll erscheint.

Möglicherweise benutzt ihr im Übrigen sogar ständig Erweiterungsmethoden, ohne es zu wissen. Microsoft macht davon nahezu exzessiven Gebrauch im Zusammenhang mit LINQ, um euch so komfortable Operationen wie Count(…) und Where(…) auf beliebigen Objekten anbieten zu können, die IEnumerable implementieren.

Projektverweise

Kategorien / Archiv  |  Übersicht RSS-Feed

Schlagworte

Suche