Exportieren (0) Drucken
Alle erweitern

C# Tipps - Oft benötigte Schnittstellen und Schemata

Veröffentlicht: 12. Apr 2006
Von Allen Jones

Auszug aus dem Buch "Microsoft Visual C#.NET - Programmier-Rezepte - Hunderte von Lösungen und Codebeispielen aus der Praxis".

Das nächste Mal, wenn Sie auf ein Problem in Ihrem C#-Code stoßen, das schwierig zu lösen scheint, werfen Sie doch einen Blick in dieses Buch. Hier finden Sie Lösungen und Best Practices zum Schreiben effizienten Codes in C#.

´Microsoft Visual C#.NET Programmierrezepte´ ist Ihre Quelle für hunderte von C#- und .NET Framework-Programmier-Szenarien und -Aufgaben, die mit Hilfe eines konsistenten Problem/Lösungs-Ansatzes erläutert und gelöst werden.

Der logische Aufbau des Buchs erlaubt Ihnen, schnell die Themen zu finden, die Sie interessieren und damit praktische Beispiele, Code-Schnipsel, Best Practices und undokumentierte Vorgehensweisen, die Ihnen helfen, Ihre Arbeit schnell und effizient zu erledigen.

Sämtliche Codebeispiele sind von der Microsoft Press-Website downloadbar.

Microsoft Press


Diesen Artikel können Sie dank freundlicher Unterstützung von Microsoft Press auf MSDN Online lesen. Microsoft Press ist ein Partner von MSDN Online.


Zur Partnerübersichtsseite von MSDN Online

Die Rezepte in diesem Kapitel zeigen Ihnen, wie Sie Schemata implementieren, die Sie während der Entwicklung von Microsoft .NET Framework-Anwendungen oft benötigen. Einige dieser Schemata werden mithilfe von Schnittstellen formalisiert, die in der .NET Framework-Klassenbibliothek definiert sind. Ande-re sind weniger streng definiert, verlangen aber von Ihnen, ein bestimmtes Design für Ihre Typen zu verwenden und diese auf eine bestimmte Art und Weise zu implementieren.

Auf dieser Seite

 Einen serialisierbaren Typ implementieren
 Einen klonbaren Typ implementieren
 Einen Typ implementieren, der mit anderen Typen verglichen werden kann
 Einen Typ implementieren, dessen Inhalte sequenziell verarbeitet werden können

Einen serialisierbaren Typ implementieren

Aufgabe
Sie möchten einen benutzerdefinierten Typ implementieren, der serialisierbar sein und Ihnen die folgenden Möglichkeiten bieten soll:

  • Das dauerhafte Speichern der Instanzen dieses Typs (zum Beispiel in einer Datei oder einer Datenbank).

  • Das Übermitteln von Instanzen dieses Typs über das Netzwerk.

  • Die »Wertübergabe« der Instanzen dieses Typs über die Anwendungsdomänengrenzen hinweg.

Lösung
Um einfache Typen zu serialisieren, weisen Sie der Typdeklaration das Attribut System.Seria¬lizableAttribute zu. Wenn Sie mit komplexen Typen arbeiten oder den Inhalt und die Struktur der serialisierten Daten kon-trollieren möchten, implementieren Sie die Schnittstelle System.Runtime.Serialization.ISerializable.

Beschreibung

Objekt können mithilfe der Formatter-Klassen serialisiert und deserialisiert werden. Diese Klassen sind ein Bestandteil der .NET Framework-Klassenbibliothek. Typen sind jedoch nicht standardmäßig serialisierbar. Um einen benutzerdefinierten Typ zu implementieren, der serialisierbar sein soll, müssen Sie Ihrer Typdeklaration das Attribut SerializableAttribute hinzufügen. Wenn alle Datenfelder in Ihrem Typ serialisierbare Typen sind, sind weitere Schritte nicht notwendig. Wenn Sie eine benut-zerdefinierte Klasse implementieren, die von einer Basisklasse abgeleitet ist, muss die Basisklasse ebenfalls serialisierbar sein.

Jede Formatter-Klasse enthält die Logik, die zur Serialisierung von Typen mit dem Attribut Seriali-zableAttribute notwendig ist. Diese Logik sorgt dafür, dass alle öffentlichen, geschützten und privaten Fel-der korrekt serialisiert werden. Der folgende Code-Auszug zeigt die Typ- und Felddeklarationen einer seria-lisierbaren Klasse namens Employee (Angestellte).

        using System;

        [Serializable]
        public class Employee {

           private string name;
           private int age;
           private string address;
           ...
        }
      

HINWEIS: Klassen, die von einem serialisierbaren Typ abgeleitet sind, erben nicht das Attribut Seriali-zableAttribute. Um abgeleitete Typen serialisieren zu können, müssen Sie diese explizit als serialisierbar deklarieren, indem Sie ihnen das Attribut SerializableAttribute zuweisen.

Sie können bestimmte Felder von der Serialisierung ausschließen, indem Sie ihnen das Attribut System.NonSerializedAttribute zuweisen. Sie sollten in der Regel die folgenden Felder nicht serialisieren:

  • Felder, die nicht serialisierbare Datentypen enthalten.

  • Felder mit Werten, die ungültig sein könnten, wenn das Objekt deserialisiert wird. Dazu zählen beispielsweise Datenbankverbindungen, Speicheradressen, Thread-IDs und nicht verwaltete Ressourcen-Handles.

  • Felder, die sensitive oder geheime Informationen enthalten, zum Beispiel Kennwörter, Verschlüsselungsschlüssel und die nicht öffentlichen Details über Personen und Unternehmen.

  • Felder mit Daten, die anhand anderer Quellen einfach neu erstellt oder abgerufen werden können – dies gilt besonders dann, wenn die Datenmenge sehr groß ist.

Wenn Sie Felder von der Serialisierung ausschließen, müssen Sie bei der Implementierung Ihres Typs darauf achten, dass einige Daten nicht mehr vorhanden sind, sobald das Objekt deserialisiert wird. Sie können die fehlenden Datenfelder leider nicht in einem Instanzenkonstruktor erstellen oder abrufen, da Formatierer während der Deserialisierung von Objekten keine Konstruktoren aufrufen. Die gebräuchlichste Lösung dieses Problems besteht darin, die verzögerte Initialisierung zu implementieren, bei der Ihr Typ erst dann die Daten erstellt oder abruft, wenn diese zum ersten Mal benötigt werden.

Der folgende Code zeigt eine modifizierte Version der Klasse Employee. Dort wurde dem Feld address das Attribut NonSerializedAttribute zugewiesenen, sodass ein Formatierer den Wert dieses vertraulichen Feldes nicht serialisiert. Die Klasse Employee implementiert öffentliche Eigenschaften, um den Zugriff auf die privaten Datenmember zu ermöglichen. Eine solche öffentliche Eigenschaft eignet sich auch zur Implementierung der verzögerten Initialisierung des Feldes address.

        using System;

        [Serializable]
        public class Employee {

            private string name;
            private int age;

            [NonSerialized]
            private string address;

            // Einfacher Employee-Konstruktor
            public Employee(string name, int age, string address) {

                this.name = name;
                this.age = age;
                this.address = address;
            }

            // Öffentliche Eigenschaft für den Zugriff auf den Namen des Angestellten
            public string Name {
                get { return name; }
                set { name = value; }
            }

            // Öffentliche Eigenschaft für den Zugriff auf das Alter des Angestellten
            public int Age {
                get { return age; }
                set { age = value; }
            }

            // Öffentliche Eigenschaft für den Zugriff auf die Adresse des Angestellten.
            // Die verzögerte Initialisierung verwenden, um die Adresse einzurichten, da
            // ein deserialisiertes Objekt keinen Adresswert hat.
            public string Address {
                get {
                    if (address == null) {
                        // Die Adresse von einem dauerhaften Speicher laden.
                        ;<$VE>
                    }
                    return address;
                }

                set {
                    address = value;
                }
            }
        }
      

Für die meisten benutzerdefinierten Typen, die Sie erstellen werden, genügen die Attribute SerializableAttribute und NonSerializedAttribute. Wenn Sie sich mehr Kontrollmöglichkeiten für die Serialisierung wünschen, können Sie die Schnittstelle ISerializable implementieren. Die Formatter-Klassen benutzen verschiedene Verfahren zur Serialisierung und Deserialisierung der Typinstanzen, die ISerializable implementieren. Um ISerializable korrekt zu implementieren, müssen Sie

  • deklarieren, dass Ihr Typ ISerializable implementiert.

  • Ihrer Typdeklaration wie bereits beschrieben das Attribut SerializableAttribute zuweisen. Benutzen Sie nicht NonSerializedAttribute, da dies keine Auswirkungen haben würde.

  • die Methode ISerializable.GetObjectData implementieren (die während der Serialisierung verwendet wird), die die folgenden Argumenttypen erwartet:

    • System.Runtime.Serialization.SerializationInfo

    • System.Runtime.Serialization.StreamingContext

  • einen nicht öffentlichen Konstruktor implementieren (der während der Deserialisierung verwendet wird), der dieselben Argumente wie die Methode GetObjectData erwartet. Denken Sie daran den Konstruktor als geschützt (protected) zu kennzeichnen, wenn Sie Klassen von Ihrer serialisierbaren Klasse ableiten möchten.

  • darauf achten, dass die GetObjectData-Methode und der Deserialisierungskonstruktor Ihres Typs die entsprechende Methode und den Konstruktor der übergeordneten Klasse aufrufen müssen, wenn Sie eine serialisierbare Klasse aus einer Basisklasse erstellen, die ebenfalls ISerializable implementiert.

Während der Serialisierung ruft der Formatter die Methode GetObjectData auf und übergibt ihr SerializationInfo- und StreamingContext-Referenzen als Argumente. Ihr Typ muss das SerializationInfo-Objekt mit den Daten füllen, die Sie serialisieren möchten. Die Klasse SerializationInfo stellt die Methode AddValue zur Verfügung, mit der Sie jedes beliebige Datenelement hinzufügen können. Wenn Sie AddValue aufrufen, müssen Sie einen Namen für das Datenelement angeben – Sie benutzen diesen Namen während der Deserialisierung, um das jeweilige Datenelement abzurufen. Es gibt sechzehn Überladungen der Methode AddValue, die es Ihnen ermöglichen, dem SerializationInfo-Objekt verschiedene Datentypen hinzuzufügen.

Das StreamingContext-Objekt stellt Informationen über den Zweck und das Ziel der serialisierten Daten zur Verfügung. Sie können mithilfe dieses Objekts bestimmen, welche Daten serialisiert werden sollen. Sie könnten beispielsweise bedenkenlos geheime Daten serialisieren, wenn diese für eine andere Anwendungsdomäne im selben Prozess bestimmt wären und nicht in eine Datei geschrieben würden.

Wenn ein Formatter einer Instanz Ihres Typs deserialisiert, ruft er den Deserialisierungskonstruktor auf und übergibt ihm SerializationInfo- und StreamingContext-Referenzen als Argumente. Ihr Typ muss die serialisierten Daten mit einer der SerializationInfo.Get*-Methoden aus dem SerializationInfo-Objekt extrahieren. Beispiele für diese Methoden sind GetString, GetInt32 oder GetBoolean. Während der Deserialisierung stellt das StreamingContext-Objekt Informationen über die Quelle der serialisierten Daten zur Verfügung. Dies ermöglicht es Ihnen, die Logik zu spiegeln, die Sie für die Serialisierung implementiert haben.

HINWEIS: Während der Standardserialisierung greifen die Formatter nicht auf die Fähigkeiten des StreamingContext-Objekts zurück, um Informationen über die Quelle, das Ziel und den Zweck der serialisierten Daten bereitzustellen. Wenn Sie jedoch eine angepasste Serialisierung durchführen möchten, kann Ihr Code das StreamingContext-Objekt des jeweiligen Formatter-Objekts konfigurieren, bevor die Serialisierung und Deserialisierung initiiert wird. Die Dokumentation zum .NET Framework-SDK enthält weitere Informationen zur Klasse StreamingContext.

Das folgende Beispiel zeigt eine modifizierte Version der Klasse Employee, die die Schnittstelle ISerializable implementiert. In dieser Version verzichtet die Employee-Klasse auf die Serialisierung des address-Feldes, wenn das vorhandene StreamingContext-Objekt angibt, dass das Ziel der serialisierten Daten eine Datei ist. Sie finden den vollständigen Code dieses Beispiels im Beispielcode dieses Kapitels und dort in der Datei SerializableExample.cs. Die Datei enthält ebenfalls eine Main-Methode, die die Serialisierung und Deserialisierung eines Employee-Objekts demonstriert.

        using System;
        using System.Runtime.Serialization;

        [Serializable]
        public class Employee : ISerializable {

            private string name;
            private int age;
            private string address;

            // Einfacher Employee-Konstruktor
            public Employee(string name, int age, string address) {

                this.name = name;
                this.age = age;
                this.address = address;
            }

            // Konstruktor, der es einem Formatter ermöglicht, ein Employee-Objekt
            // zu deserialisieren. Sie sollten einen privaten oder zumindest einen geschützten
            // Konstruktor deklarieren, um sicherzustellen, dass er nicht unnötigerweise aufgerufen wird.
            private Employee(SerializationInfo info, StreamingContext context) {
                    // Den Namen und das Alter des Angestellten extrahieren. Diese Daten werden unabhängig
                // vom Wert von StreamingContext immer in den serialisierten Daten präsent sein.
                name = info.GetString("Name");
                age = info.GetInt32("Age");
                    // Versuchen, die Adresse des Angestellten zu extrahieren, und den entsprechenden
                // Fehler abfangen, wenn die Adresse nicht verfügbar ist.
                try {
                    address = info.GetString("Address");
                } catch (SerializationException) {
                    address = null;
                }
            }
            // Eigenschaften Name, Age und Address sind nicht aufgeführt.
        ...
            // Die von der Schnittstelle ISerializable deklarierte Methode GetObjectData
            // stellt den Mechanismus zur Verfügung, mit dem ein Formatter die Objektdaten
            // abruft, die serialisiert werden sollen.
            public void GetObjectData(SerializationInfo inf, StreamingContext con){

                // Den Namen und das Alter des Angestellten immer deserialisieren.
                inf.AddValue("Name", name);
                inf.AddValue("Age", age);

                // Die Adresse des Angestellten nicht serialisieren, wenn StreamingContext
                // anzeigt, dass die serialisierten Daten in eine Datei geschrieben werden
                if ((con.State & StreamingContextStates.File) == 0) {

                    inf.AddValue("Address", address);
                }
            }
        }
      

Einen klonbaren Typ implementieren

Aufgabe
Sie möchten einen benutzerdefinierten Typ erstellen, der einen einfachen Mechanismus zur Verfügung stellt, mit dem Programmierer Kopien der Typinstanzen anfertigen können.

Lösung
Implementieren Sie die Schnittstelle System.ICloneable.

Beschreibung

Wenn Sie einem Werttyp einen anderen Werttyp zuweisen, erstellen Sie eine Kopie des Wertes. Es gibt keine Verbindung zwischen den beiden Werten – die Änderung des einen Wertes wirkt sich nicht auf den anderen aus. Wenn Sie jedoch einem Verweistyp einen anderen Verweistyp zuweisen (mit Ausnahme von Strings, die von der Laufzeit besonders behandelt werden), erstellen Sie keine neue Kopie des Verweistyps. Beide Verweistypen verweisen stattdessen auf dasselbe Objekt, und Änderungen am Objektwert werden von beiden Verweisen reflektiert. Um eine echte Kopie eines Verweistyps zu erstellen, müssen Sie das Objekt klonen, auf das verwiesen wird.

Die Schnittstelle ICloneable identifiziert einen Typ als klonbar und deklariert die Methode Clone, mit der Sie den Klon eines Objekts abrufen können. Die Methode Clone erwartet keine Argumente und gibt unabhängig vom Implementierungstyp ein System.Object zurück. Dies bedeutet, dass Sie den Klon in den korrekten Typ konvertieren müssen, wenn Sie ein Objekt klonen.

Wie Sie in einem benutzerdefinierten Typ die Clone-Methode implementieren, ist von den Datenmembern abhängig, die innerhalb dieses Typs deklariert sind. Wenn der benutzerdefinierte Typ nur Werttypen (int, byte usw.) und System.String-Datenmember enthält, können Sie die Clone-Methode implementieren, indem Sie ein neues Objekt instanziieren und dessen Datenmember auf dieselben Werte wie die des aktuellen Objekts setzen. Die Klasse Object (von der alle Typen abgeleitet sind) enthält die geschützte (protected) Methode MemberwiseClone, die diesen Prozess automatisiert. Nachfolgend ist ein Beispiel aufgeführt, das eine einfache Klasse namens Employee zeigt, die nur String-Member enthält. Die Clone-Methode verwendet somit die geerbte Methode MemberwiseClone, um einen Klon zu erstellen.

        using System;

        public class Employee : ICloneable {

            public string Name;
            public string Title;

            // Einfacher Employee-Konstruktor
            public Employee(string name, string title) {

                Name = name;
                Title = title;
            }

            // Einen Klon mit der Methode Object.MemberwiseClone erstellen, da die
            // die Klasse Employee nur String-Verweise enthält
            public object Clone() {

                return MemberwiseClone();
            }
        }
      

Wenn Ihr benutzerdefinierter Typ Datenmember enthält, die Verweistypen sind, müssen Sie entscheiden, ob Ihre Clone-Methode eine flache oder eine tiefe Kopie erstellt. Eine flache Kopie bedeutet, dass alle Datenmember im Klon, die Verweistypen sind, auf dasselbe Objekt wie die entsprechenden Verweistypen-Datenmember im ursprünglichen Objekt verweisen. Um eine tiefe Kopie zu erhalten, müssen Sie Klone vom gesamten Objektgraph erstellen, sodass die Datenmember des Klons, die Verweistypen sind, auf physisch unabhängige Kopien (Klone) des Objekts verweisen, dass vom ursprünglichen Objekt referenziert wird.

Eine flache Kopie ist einfach zu implementieren. Dies geschieht mit der bereits beschriebenen Methode MemberwiseClone. Eine tiefe Kopie ist das, was Programmierer häufig erwarten, wenn Sie ein Objekt zum ersten Mal klonen – doch die Ergebnisse entsprechen nur selten den Erwartungen. Dies gilt besonders für die Auflistungsklassen im Namespace System.Collections, die in ihren Clone-Methoden flache Kopien implementieren. Obwohl es oft sinnvoll wäre, wenn diese Auflistungen eine tiefe Kopie implementierten, gibt es zwei wichtige Gründe dafür, dass Typen (besonders generische Auflistungsklassen) dies nicht tun:

  • Einen Klon von einem großen Objektgraphen zu erstellen, erfordert sehr viel Rechen- und Speicherleistung.

  • Generische Auflistungen können umfangreiche Objektgraphen enthalten, die aus jedem beliebigen Objekttypen bestehen können. Es ist nicht möglich, eine tiefe Kopie zu erstellen, die einer solchen Bandbreite von Möglichkeiten gerecht wird, da einige Objekte in der Auflistung oft nicht klonbar sind, während andere Objekte Zirkelverweise enthalten können, sodass der Klonvorgang in einer Endlosschleife gefangen wäre.

Für eine stark typisierte Auflistung, in der die enthaltenen Elemente bekannt und kontrollierbar sind, kann eine tiefe Kopie ein sehr nützliches Feature sein. System.Xml.XmlNode implementiert beispielsweise in seiner Clone-Methode eine tiefe Kopie. Dies ermöglicht es Ihnen, echte Kopien ganzer XML-Objekthierarchien mit nur einer Anweisung zu generieren.

TIPP: Wenn Sie ein Objekt klonen möchten, das ICloneable nicht implementiert, aber serialisierbar ist, können Sie oft das Objekt serialisieren und anschließend deserialisieren, um zum selben Ergebnis wie beim Klonen zu gelangen. Denken Sie jedoch daran, dass die Serialisierung möglicherweise nicht alle Datenmember serialisiert (wie in Rezept 16.1 beschrieben). Wenn Sie einem benutzerdefinierten serialisierbaren Typ erstellen, können Sie ebenfalls die Serialisierung nutzen, um mit Ihrer Implementierung der Methode ICloneable.Clone eine tiefe Kopie zu generieren. Um ein serialisierbares Objekt zu klonen, benutzen Sie die Klasse System.Runtime.Serialization.Formatters.Binary.BinaryFormatter, mit der Sie das Objekt in ein System.IO.MemoryStream-Objekt serialisieren und später wieder deserialisieren.

Die nachfolgend aufgeführte Klasse Team enthält eine Implementierung der Methode Clone, die eine tiefe Kopie erstellt. Die Klasse enthält außerdem eine aus Employee-Objekten bestehende Auflistung, die ein Team repräsentiert. Wenn Sie die Clone-Methode eines Team-Objekts aufrufen, erzeugt die Methode einen Klon von jedem enthaltenen Employee-Objekt und fügt ihn dem geklonten Team-Objekt hinzu. Die Team-Klasse stellt einen privaten Konstruktor zur Verfügung, um den Code in der Clone-Methode zu vereinfachen – der Einsatz von Konstruktoren ist ein gebräuchliches Verfahren zur Vereinfachung des Klonens. Die Datei CloneableExample.cs im Beispielcode dieses Kapitels enthält die Klassen Team und Employee. Sie stellt außerdem eine Main-Methode zur Verfügung, die Ihnen zeigt, wie Sie eine tiefe Kopie anfertigen.

        using System;
        using System.Collections;

        public class Team : ICloneable {

            public ArrayList TeamMembers = new ArrayList();

            public Team() {
            }

            // Privater Konstruktor, der von der Clone-Methode aufgerufen wird,
            // um ein neues Team-Objekt zu erstellen und dessen ArrayList mit
            // geklonten Employee-Objekten aus einer bereitgestellten ArrayList
            // zu füllen.
            private Team(ArrayList members) {
   
                foreach (Employee e in members) {

                    TeamMembers.Add(e.Clone());
                }
            }

            // Dem Team ein Employee-Objekt hinzufügen
            public void AddMember(Employee member) {

                TeamMembers.Add(member);
            }

            public object Clone() {

                // Eine tiefe Kopie des Teams erstellen, indem der private Team-
                // Konstruktor aufgerufen und die ArrayList übergeben wird, die
                // die Team-Mitglieder enthält.
                return new Team(this.TeamMembers);

                // Der folgende Befehl würde eine flache Kopie des Teams erstellen.
                // return MemberwiseClone();
            }
        }
      

Einen Typ implementieren, der mit anderen Typen verglichen werden kann

Aufgabe
Sie möchten einen Mechanismus zur Verfügung stellen, der es Ihnen ermöglicht, benutzerdefinierte Typen miteinander zu vergleichen, sodass Sie Auflistungen sortieren können, die Instanzen dieses Typs enthalten.

Lösung
Um den Standardvergleichsmechanismus für Typen zu verwenden, implementieren Sie die Schnittstelle System.IComparable. Wenn Sie mehrere Eigenschaften eines Typs mit denen eines anderen Typs vergleichen möchten, erstellen Sie separate Typen, die die Schnittstelle System.Collections.IComparer implementieren.

Beschreibung

Wenn Sie Ihren Typ nach nur einem Kriterium sortieren möchten, zum Beispiel aufsteigen anhand einer ID oder alphabetisch anhand des Nachnamens, sollten Sie die Schnittstelle IComparable implementieren. IComparable definiert eine einzelne Methode namens CompareTo, die nachfolgend aufgeführt ist.

        int CompareTo(object obj);
      

Das Objekt (obj), das der Methode übergeben wird, muss vom selben Typ wie das aufgerufene Objekt sein, da CompareTo andernfalls die Ausnahme System.ArgumentException auslöst. Der von CompareTo zurückgegebene Wert wird wie folgt aufgeschlüsselt:

  • Wenn das aktuelle Objekt kleiner als obj ist, wird ein Wert zurückgegeben, der kleiner als 1 ist (z.B. –1).

  • Wenn das aktuelle Objekt denselben Wert wie obj hat, wird 0 zurückgegeben.

  • Wenn das aktuelle Objekt größer als obj ist, wird ein Wert zurückgegeben, der größer als 0 ist (z.B. 1).

Die Art des Vergleichs ist von den Typen abhängig, die die Schnittstelle IComparable implementieren. Würden Sie beispielsweise Personen anhand des Nachnamens sortieren, müssten Sie einen Zeichenfolgenvergleich durchführen. Wollten Sie die Personen nach den Geburtsdaten sortieren, würden Sie System.DateTime-Objekte vergleichen.

Um verschiedene Sortierreihenfolgen für bestimmte Typen zu unterstützen, müssen Sie separate Hilfstypen implementieren, die wiederum die Schnittstelle IComparer mit der folgenden Compare-Methode implementieren.

        int Compare(object x, object y);
      

Diese Hilfstypen müssen die Logik kapseln, die notwendig ist, um zwei Objekte miteinander zu vergleichen, und einen Wert zurückgeben, der sich wie folgt berechnet:

  • Wenn x kleiner als y ist, muss ein Wert zurückgegeben werden, der kleiner als 0 ist (z.B. –1).

  • Wenn x denselben Wert wie y hat, muss 0 zurückgegeben werden.

  • Wenn x größer als y ist, muss ein Wert zurückgegeben werden, der größer als 0 ist (z.B. 1).

Die nachfolgend aufgeführte Klasse Newspaper (Zeitung) demonstriert die Implementierung der Schnittstellen IComparable und IComparer. Die Methode Newspaper.CompareTo vergleicht die name-Felder zweier Newspaper-Objekte miteinander. Die Groß-/Kleinschreibung wird dabei nicht berücksichtigt. Eine private verschachtelte Klasse namens AscendingCirculationComparer implementiert IComparer und vergleicht die circulation-Felder (verkaufte Auflage) zweier Newspaper-Objekte miteinander. Ein AscendingCirculationComparer-Objekt wird über die statische Eigenschaft Newspaper.CirculationSorter abgerufen.

        using System;
        using System.Collections;

        public class Newspaper : IComparable {

            private string name;
            private int circulation;

            private class AscendingCirculationComparer : IComparer {

                int IComparer.Compare(object x, object y) {

                    // Behanldungslogik für null-Verweise, wie von der Schnittstelle
                    // IComparer vorgeschrieben. Null gilt als kleinster Wert.
                    if (x == null && y == null) return 0;
                    else if (x == null) return -1;
                    else if (y == null) return 1;

                    // x und y verweisen auf dasselbe Objekt
                    if (x == y) return 0;

                    // Überprüfen, ob x und y Newspaper-Instanzen sind
                    Newspaper newspaperX = x as Newspaper;
                    if (newspaperX == null) {

                        throw new ArgumentException("Ungültiger Objekttyp", "x");
                    }

                    Newspaper newspaperY = y as Newspaper;
                    if (newspaperY == null) {

                        throw new ArgumentException("Ungültiger Objekttyp", "y");
                    }

                    // Die verkauften Auflagen (circulation) miteinander vergleichen. IComparer schreibt vor, dass:
                    //      ein Wert kleiner als 0 zurückgegeben werden muss, wenn x < y
                    //      Null zurückgegeben werden muss, wenn x = y
                    //      ein Wert größer als 0 zurückgegeben werden muss, wenn x > y
                    // Diese Logik wird einfach mithilfe der Integer-Arithmetik implementiert.
                    return newspaperX.circulation - newspaperY.circulation;
                }
            }

            public Newspaper(string name, int circulation) {

                this.name = name;
                this.circulation = circulation;
            }

            // Eine schreibgeschützte Eigenschaft deklarieren, die eine Instanz von
            // AscendingCirculationComparer zurückgibt.
            public static IComparer CirculationSorter{
                get { return new AscendingCirculationComparer(); }
            }

            public override string ToString() {

                return string.Format("{0}: Circulation = {1}", name, circulation);
            }

            // Die Methode CompareTo vergleicht zwei Newspaper-Objekte, indem sie einen von der
            // Groß-/Kleinschreibung unabhänigen Vergleich der Zeitungsnamen durchführt.
            public int CompareTo(object obj) {

                // IComparable schreibt vor, dass ein Objekt immer größer als Null ist.
                if (obj == null) return 1;

                // 0 zurückgeben, wenn das andere Objekt ein Verweis auf dieses Objekt ist.
                if (obj == this) return 0;

                // Versuchen, dass andere Objekt in eine Newspaper-Instanz zu konvertieren.
                Newspaper other = obj as Newspaper;

                // Wenn "other" null ist, muss es nicht in eine Newspaper-Instanz konvertiert werden.
                // IComparable schreibt vor, dass CompareTo in dieser Situation die Ausnahme
                // System.ArgumentException auslösen muss.
                if (other == null) {

                    throw new ArgumentException("Ungültiger Objekttyp", "obj");

                } else {

                    // Den Rückgabewert berechnen, indem ein von der Groß-/Kleinschreibung unabhängiger
                    // Vergleich der Zeitungsnamen durchgeführt wird.

                    // Da der Zeitungsname eine Zeichenfolge ist, besteht die einfachste
                    // Vorgehensweise darin, die Vergleichsmöglichkeiten der Klasse String
                    // zu nutzen, die während des Zeichenfolgenvergleichs auch die kulturellen
                    // Einstellungen berücksichtigt.
                    return string.Compare(this.name, other.name, true);
                }
            }
        }
      

Die hier aufgeführte Main-Methode zeigt die Vergleichs- und Sortierungsmöglichkeiten, die Ihnen zur Verfügung stehen, wenn Sie die Schnittstellen IComparable und IComparer implementieren. Die Methode erstellt eine System.Collections.ArrayList-Auflistung, die fünf Newspaper-Objekte enthält. Main sortiert die ArrayList anschließend zweimal. Dies geschieht mit der Methode ArrayList.Sort. Der erste Sortiervorgang nutzt den Newspaper-Standardvergleichsmechanismus der Methode IComparable.CompareTo. Das zweite Sortierverfahren verwendet ein AscendingCirculationComparer-Objekt und dessen Implementierung der Methode IComparer.Compare, um den Vergleich durchzuführen.

        public static void Main() {

            ArrayList newspapers = new ArrayList();

            newspapers.Add(new Newspaper("The Echo", 125780));
            newspapers.Add(new Newspaper("The Times", 55230));
            newspapers.Add(new Newspaper("The Gazette", 235950));
            newspapers.Add(new Newspaper("The Sun", 88760));
            newspapers.Add(new Newspaper("The Herald", 5670));

            Console.WriteLine("Nicht sortierte Zeitungsliste:");
            foreach (Newspaper n in newspapers) {

                Console.WriteLine(n);
            }

            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("Nach dem Namen sortierte Zeitungsliste (Standardreihenfolge):");
            newspapers.Sort();
            foreach (Newspaper n in newspapers) {

                Console.WriteLine(n);
            }

            Console.WriteLine(Environment.NewLine);
            Console.WriteLine("Nach der Auflage sortierte Zeitungsliste:");
            newspapers.Sort(Newspaper.CirculationSorter);
            foreach (Newspaper n in newspapers) {

                Console.WriteLine(n);
            }
        }
      

Die Ausführung der Main-Methode führt zu der folgenden Ausgabe.

        Nicht sortierte Zeitungsliste:
        The Echo: Circulation = 125780
        The Times: Circulation = 55230
        The Gazette: Circulation = 235950
        The Sun: Circulation = 88760
        The Herald: Circulation = 5670

        Nach dem Namen sortierte Zeitungsliste (Standardreihenfolge):
        The Echo: Circulation = 125780
        The Gazette: Circulation = 235950
        The Herald: Circulation = 5670
        The Sun: Circulation = 88760
        The Times: Circulation = 55230

        Nach der Auflage sortierte Zeitungsliste:
        The Herald: Circulation = 5670
        The Times: Circulation = 55230
        The Sun: Circulation = 88760
        The Echo: Circulation = 125780
        The Gazette: Circulation = 235950
      

Einen Typ implementieren, dessen Inhalte sequenziell verarbeitet werden können

Aufgabe
Sie möchten einen Auflistungstyp erstellen, dessen Inhalte Sie mit einer foreach-Anweisung sequenziell verarbeiten können.

Lösung
Implementieren Sie in Ihrem Auflistungstyp die Schnittstelle System.IEnumerable. Die GetEnumerator-Methode der Schnittstelle IEnumerable gibt einen Enumerator zurück – ein Objekt, dass die Schnittstelle System.IEnumerator implementiert. Die Schnittstelle IEnumerator definiert die Methoden, die von der foreach-Anweisung verwendet werden, um die Auflistung
sequenziell zu verarbeiten.

Beschreibung

Ein numerischer Index ermöglicht es Ihnen, die Elemente einer Auflistung mit einer for-Schleife zu durchlaufen. Diese Technik ist jedoch für nicht lineare Datenstrukturen, wie zum Beispiel Baumstrukturen und mehrdimensionale Auflistungen, nicht unbedingt geeignet. Die foreach-Anweisung bietet einen einfachen und syntaktisch eleganten Mechanismus an, um eine Auflistung mit Objekten unabhängig davon zu durchlaufen, welche interne Struktur diese Objekte aufweisen.

Um die foreach-Semantik zu unterstützen, muss das Objekt, das die Objektauflistung enthält, die Schnittstelle System.IEnumerable implementieren. Die Schnittstelle IEnumerable deklariert eine einzelne Methode namens GetEnumerator, die keine Argumente erwartet und ein Objekt zurückgibt, das System.IEnumerator wie folgt implementiert.

        IEnumerator GetEnumerator();
      

Die von GetEnumerator zurückgegebene IEnumerator-Instanz ist das Objekt, das die sequenzielle Verarbeitung der Datenelemente der Auflistung unterstützt. Die Schnittstelle IEnumerator stellt einen schreibgeschützten Cursor zur Verfügung, der nur vorwärts bewegt werden kann, um auf die Member der zu Grunde liegenden Auflistung zuzugreifen. Tabelle 1 beschreibt die Member der Schnittstelle IEnumerator.

Member

Beschreibung

Current

Eine Eigenschaft, die das aktuelle Datenelement zurückgibt. Wenn der Enumerator erstellt wird, verweist Current auf eine Position vor dem ersten Datenelement. Dies bedeutet, dass Sie MoveNext vor Current aufrufen müssen. Wenn Sie Current aufrufen, während der Enumerator vor dem ersten oder hinter dem letzten Element der Datenauflistung positioniert ist, löst Current die Ausnahme System.InvalidOperationException aus.

MoveNext

Eine Methode, die den Enumerator zum nächsten Datenelement der Auflistung bewegt. Die Methode gibt true zurück, wenn weitere Elemente vorhanden sind, zu denen der Cursor bewegt werden kann. Andernfalls wird der Wert false zurückgegeben. Wenn sich die zu Grunde liegende Datenquelle während der Lebensdauer des Enumerators ändert, löst MoveNext die Ausnahme InvalidOperationException aus.

Reset

Eine Methode, die den Enumerator vor das erste Element der Datenauflistung bewegt. Wenn sich die zu Grunde liegende Datenquelle während der Lebensdauer des Enumerators ändert, löst Reset die Ausnahme InvalidOperationException aus.

Tabelle 1: Member der Schnittstelle IEnumerator

Die Klassen TeamMember, Team und TeamMemberEnumerator zeigen, wie die Schnittstellen IEnumerable und IEnumerator implementiert werden. Die nachfolgend aufgeführte Klasse TeamMember repräsentiert ein Mitglied eines Teams.

        // Die TeamMember-Klasse repräsentiert ein einzelnes Teammitglied.
        public class TeamMember {

            public string Name;
            public string Title;

            // Einfacher TeamMember-Konstruktor.
            public TeamMember(string name, string title) {

                Name = name;
                Title = title;
            }

            // Eine Zeichenfolgendarstellung des TeamMember-Objekts zurückgeben.
            public override string ToString() {

                return string.Format("{0} ({1})", Name, Title);
            }
        }
      

Die Klasse Team, die ein aus mehreren Personen bestehendes Team repräsentiert, ist eine aus TeamMember-Objekten bestehende Auflistung. Team implementiert die Schnittstelle IEnumerable und deklariert eine separate Klasse namens TeamMemberEnumerator, um die sequenzielle Verarbeitung zu ermöglichen. Auflistungsklassen implementieren häufig direkt sowohl die Schnittstelle IEnumerable als auch IEnumerator. Der Einsatz einer separaten Enumeratorklasse bietet jedoch die einfachste Möglichkeit, um mehrere Enumeratoren zuzulassen – und damit auch mehrere Threads. Diese Vorgehensweise ermöglicht eine »parallele« sequenzielle Verarbeitung eines Teams.

Team verwendet Delegaten und Ereignismember, um das Observer-Schema zu implementieren und alle TeamMemberEnumerator-Objekte zu benachrichtigen, wenn sich die Teams ändern, die diesen Objekten zu Grunde liegen. Die Klasse TeamMemberEnumerator ist eine private verschachtelte Klasse, sodass Sie Instanzen davon nur über die Methode Team.GetEnumerator erzeugen können. Der Code der Klassen Team und TeamMemberEnumerator ist nachfolgend aufgeführt.

        // Die Team-Klasse repräsentiert eine Auflistung von TeamMember-Objekten. Sie implementiert
        // die Schnittstelle IEnumerable, um die sequenzielle Verarbeitung von TeamMember-Objekten
        // zu ermöglichen.
        public class Team : IEnumerable {

            // TeamMemberEnumerator ist eine private verschachtelte Klasse, die die Funktionalität
            // zur Verfügung stellt, die zur sequenziellen Verarbeitung der TeamMember-Objekte in
            // einer Team-Auflistung notwendig ist. TeamMemberEnumerator kann wie eine verschachtelte
            // Klasse auf die privaten Member der Team-Klasse zugreifen.
            private class TeamMemberEnumerator : IEnumerator {

                // Das Team, das von diesem Objekt aufgelistet wird.
                private Team sourceTeam;

                // Boolescher Wert, der anzeigt, ob sich das zu Grunde liegende Team geändert hat,
                // was die Enumeration ungültig werden ließe.
                private bool teamInvalid = false;

                // Integer, der das aktuelle TeamMember-Objekt identifiziert. Stellt den Index
                // des TeamMember-Objekts in der zu Grunde liegenden ArrayList zur Verfügung, der
                // von der Team-Auflistung verwendet wird. Dieser Index wird mit -1 initialisiert,
                // was dem Index vor dem ersten Element entspricht.
                private int currentMember = -1;

                // Der Konstruktor nimmt einen Verweis auf das Team entgegen, das die Quelle der
                // enumerierten Daten ist.
                internal TeamMemberEnumerator(Team team) {

                    this.sourceTeam = team;

                    // Bei sourceTeam registrieren, um Änderungsbenachrichtigungen zu erhalten
                    sourceTeam.TeamChange +=
                    new TeamChangedEventHandler(this.TeamChange);
                }

                // Die Eigenschaft IEnumerator.Current implementieren.
                public object Current {
                    get {

                        // Wenn TeamMemberEnumerator vor dem ersten oder hinter dem letzten
                        // Element positioniert wird, eine Ausnahme auslösen.
                        if (currentMember == -1 ||
                            currentMember > (sourceTeam.teamMembers.Count-1)) {

                            throw new InvalidOperationException();
                        }

                        //Andernfalls das aktuelle TeamMember-Objekt zurückgeben
                        return sourceTeam.teamMembers[currentMember];
                    }
                }

                // Die Methode IEnumerator.MoveNext implementieren.
                public bool MoveNext() {

                   // Wenn das zu Grunde liegende Team ungültig ist, Ausnahme auslösen
                   if (teamInvalid) {

                       throw new InvalidOperationException("Team wurde modifiziert");
                   }

                   // Andernfalls zum nächsten TeamMember-Objekt wechseln
                   currentMember++;

                   // false zurückgeben, wenn der Cursor hinter das letzte TeamMember-Objekt
                   // bewegt wurde
                   if (currentMember > (sourceTeam.teamMembers.Count-1)) {
                       return false;
                   } else {
                       return true;
                   }
                }

                // Die Methode IEnumerator.Reset implementieren.
                // Diese Methode setzt die Position von TeamMemberEnumerator
                // auf den Anfang der TeamMembers-Auflistung zurück.
                public void Reset() {

                    // Wenn das zu Grunde liegende Team ungültig ist, Ausnahme auslösen
                    if (teamInvalid) {

                        throw new InvalidOperationException("Team wurde modifiziert");
                    }

                    // Den aktuellen currentMember-Zeiger auf den Index setzen, der
                    // die Position vor dem ersten Element repräsentiert.
                    currentMember = -1;
                }

                // Ein Ereignis-Handler, der auf die Nachricht reagiert, die mitteilt,
                // dass sich die zu Grunde liegende Team-Auflistung geändert hat.
                internal void TeamChange(Team t, EventArgs e) {

                    // Signalisieren, dass das zu Grunde liegende Team nun ungültig ist
                    teamInvalid = true;
                }
            }

            // Ein  Delegat der die Signatur festlegt, die alle Ereignis-Handler-Methoden
            // implementieren müssen, die auf Teamänderungen reagieren.
            public delegate void TeamChangedEventHandler(Team t, EventArgs e);

            // Eine  ArrayList für die TeamMember-Objekte
            private ArrayList teamMembers;

            // Das Ereignis, das TeamMemberEnumerators darüber benachrichtigt, dass
            // das Team geändert wurde.
            public event TeamChangedEventHandler TeamChange;

            // Team-Konstruktor
            public Team() {

                teamMembers = new ArrayList();
            }

            // Die Methode IEnumerable.GetEnumerator implementieren.
            public IEnumerator GetEnumerator() {
                return new TeamMemberEnumerator(this);
            }
            // Dem Team ein TeamMember-Objekt hinzufügen
            public void AddMember(TeamMember member) {

                teamMembers.Add(member);

                // Die Listener darüber informieren, dass sich die Liste geändert hat
                if (TeamChange != null) {

                    TeamChange(this, null);
                }
            }
        }
      

Wenn Ihre Auflistungsklasse verschiedene Arten von Daten enthält, die Sie gesondert sequenziell verarbeiten möchten, genügt es nicht, in der Auflistungsklasse die Schnittstelle IEnumerable zu implementieren. In diesem Fall müssten Sie einige Eigenschaften implementieren, die verschiedene IEnumerator-Instanzen zurückgeben würden. Würde die Klasse Team beispielsweise sowohl die Teammitglieder als auch die Hardware des Teams repräsentieren, könnten Sie beispielsweise die folgenden Eigenschaften implementieren.

        // Eigenschaft zur Auflistung von Teammitgliedern.
        public IEnumerator Members {
            get {
                return new TeamMemberEnumerator(this);
            }
        }

        // Eigenschaft zur Auflistung von Teamcomputern.
        public IEnumerator Computers {
            get {
                return new TeamComputerEnumerator(this);
            }
        }
      

Um diese Enumeratoren zu benutzen, könnten Sie den folgenden Code verwenden:

        Team team = new Team();
        ...
        foreach(TeamMember in team.Members) {
            // Verarbeitung der Teammitglieder…
        }

        foreach(TeamComputer in team.Computers) {
            // Verarbeitung der Teamcomputer…
        }
      

HINWEIS: Die foreach-Anweisung unterstützt auch Typen, die ein Schema implementieren, dass dem der Schnittstellen IEnumerable und IEnumerator entspricht. Dies gilt auch dann, wenn der jeweilige Typ die Schnittstellen nicht implementiert. Ihr Code ist jedoch übersichtlicher und verständlicher, wenn Sie die Schnittstelle IEnumerable implementieren. Die C#-Sprachspezifikationen enthalten weitere Informationen zur foreach-Anweisung.

Genaueres finden Sie unter http://msdn2.microsoft.com/en-us/netframework/aa569283.aspx


Anzeigen:
© 2015 Microsoft