Kapitel 29 System.Array und die Auflistungsklassen
In diesem Kapitel erhalten Sie einen Überblick
darüber, welche Klassen verfügbar sind. Anschließend
werden die einzelnen Klassen besprochen, und es wird anhand von Beispielen
aufgezeigt, welche Schnittstellen und Funktionen für bestimmte
Features benötigt werden.
29.1 Sortieren und Suchen
 
Die Auflistungsklassen der Frameworks unterstützen einige nützliche
Funktionen für die Sortierung und binäre Suchläufe. Die
Array-Klasse bietet die gleiche Funktionalität, jedoch über
statische Funktionen, nicht durch Mitgliedsfunktionen.
Das Sortieren eines Arrays aus integer-Werten
kann sehr einfach sein:
using System;
class Test
{
public static void Main()
{
int[] arr = {5, 1, 10, 33, 100, 4};
Array.Sort(arr);
foreach (int v in arr)
Console.WriteLine("Element: {0}", v);
}
}
Dieser Code erzeugt die folgende Ausgabe:
1
4
5
10
33
100
Dies ist sehr bequem für die integrierten Typen,
funktioniert jedoch nicht für Klassen oder Strukturen, da die Routine
die richtige Sortierreihenfolge nicht kennt.
29.1.1 Implementieren von IComparable
 
Die Frameworks halten für Klassen oder Strukturen
einige sehr gute Methoden zur Sortierung der Klassen- oder Strukturinstanzen
bereit. Bei der einfachsten Methode implementiert das Objekt die IComparable-Schnittstelle:
using System;
public class Employee: IComparable
{
public Employee(string name, int id)
{
this.name = name;
this.id = id;
}
int IComparable.CompareTo(object obj)
{
Employee emp2 = (Employee) obj;
if (this.id > emp2.id)
return(1);
if (this.id < emp2.id)
return(-1);
else
return(0);
}
public override string ToString()
{
return(String.Format("{0}:{1}", name, id));
}
string name;
int id;
}
class Test
{
public static void Main()
{
Employee[] arr = new Employee[4];
arr[0] = new Employee("George", 1);
arr[1] = new Employee("Fred", 2);
arr[2] = new Employee("Tom", 4);
arr[3] = new Employee("Bob", 3);
Array.Sort(arr);
foreach (Employee emp in arr)
Console.WriteLine("Employee: {0}", emp);
}
}
Dieses Programm erzeugt die folgende Ausgabe:
Employee: George:1
Employee: Fred:2
Employee: Bob:3
Employee: Tom:4
Diese Implementierung ermöglicht lediglich
eine Sortierung; die Klasse könnte so definiert werden, dass eine
auf Mitarbeiter-ID oder Name basierende Sortierung vorgenommen wird,
dem Benutzer kann jedoch die Auswahl der Sortierreihenfolge nicht ermöglicht
werden.
29.1.2 Verwenden von IComparer
 
Die Designer der Frameworks haben die Fähigkeit
zur Definition mehrerer Sortierreihenfolgen bereitgestellt. Jede Sortierreihenfolge wird durch
die IComparer-Schnittstelle ausgedrückt, die geeignete
Schnittstelle wird der Sortier- oder Suchfunktion übergeben.
Die IComparer-Schnittstelle kann für
Employee jedoch nicht implementiert werden, da jede Klasse
eine Schnittstelle nur einmal implementieren kann. Es könnte also
nur eine Sortierreihenfolge bereitgestellt werden1 . Es ist für jede Sortierung eine separate Klasse erforderlich,
die IComparer implementiert. Die Klasse ist sehr einfach,
da nur die Compare()-Funktion implementiert wird:
using System;
using System.Collections;
class Employee
{
public string name;
}
class SortByNameClass: IComparer
{
public int Compare(object obj1, object obj2)
{
Employee emp1 = (Employee) obj1;
Employee emp2 = (Employee) obj2;
return(String.Compare(emp1.name, emp2.name));
}
}
Das Compare()-Mitglied trägt zwei
Objekte als Parameter. Da die Klasse nur zur Mitarbeitersortierung eingesetzt
werden sollte, werden die object-Parameter per Typumwandlung
zu Employee. Anschließend wird die in string
enthaltene Compare()-Funktion für den Vergleich verwendet.
Die Employee-Klasse wird demnach folgendermaßen
überarbeitet. Die Klassen für die Sortierungen werden innerhalb
der Employee-Klasse verschachtelt:
using System;
using System.Collections;
public class Employee: IComparable
{
public Employee(string name, int id)
{
this.name = name;
this.id = id;
}
int IComparable.CompareTo(object obj)
{
Employee emp2 = (Employee) obj;
if (this.id > emp2.id)
return(1);
if (this.id < emp2.id)
return(-1);
else
return(0);
}
public override string ToString()
{
return(name + ":" + id);
}
public class SortByNameClass: IComparer
{
public int Compare(object obj1, object obj2)
{
Employee emp1 = (Employee) obj1;
Employee emp2 = (Employee) obj2;
return(String.Compare(emp1.name, emp2.name));
}
}
public class SortByIdClass: IComparer
{
public int Compare(object obj1, object obj2)
{
Employee emp1 = (Employee) obj1;
Employee emp2 = (Employee) obj2;
return(((IComparable) emp1).CompareTo(obj2));
}
}
string name;
int id;
}
class Test
{
public static void Main()
{
Employee[] arr = new Employee[4];
arr[0] = new Employee("George", 1);
arr[1] = new Employee("Fred", 2);
arr[2] = new Employee("Tom", 4);
arr[3] = new Employee("Bob", 3);
Array.Sort(arr, (IComparer) new Employee.SortByNameClass());
// Mitarbeiter sind jetzt nach Name sortiert
foreach (Employee emp in arr)
Console.WriteLine("Employee: {0}", emp);
Array.Sort(arr, (IComparer) new Employee.SortByIdClass());
// Mitarbeiter sind jetzt nach ID sortiert
foreach (Employee emp in arr)
Console.WriteLine("Employee: {0}", emp);
ArrayList arrList = new ArrayList();
arrList.Add(arr[0]);
arrList.Add(arr[1]);
arrList.Add(arr[2]);
arrList.Add(arr[3]);
arrList.Sort((IComparer) new Employee.SortByNameClass());
foreach (Employee emp in arrList)
Console.WriteLine("Employee: {0}", emp);
arrList.Sort(); // Standard = nach ID
foreach (Employee emp in arrList)
Console.WriteLine("Employee: {0}", emp);
}
}
Der Benutzer kann nun die Sortierung angeben und
bei Bedarf zwischen den unterschiedlichen Sortierreihenfolgen wechseln.
Dieses Beispiel zeigt, wie die gleichen Funktionen bei Verwendung der
ArrayList-Klasse arbeiten, auch wenn Sort()
eine Mitgliedsfunktion und keine statische Funktion ist.
29.1.3 IComparer als Eigenschaft
 
Das Sortieren mit der Employee-Klasse
ist weiterhin ein wenig merkwürdig, da der Benutzer eine Instanz
der geeigneten Sortierungsklasse erstellen und in IComparer
umwandeln muss. Dem Benutzer kann diese Aufgabe durch den Einsatz statischer
Eigenschaften abgenommen werden:
using System;
using System.Collections;
public class Employee: IComparable
{
public Employee(string name, int id)
{
this.name = name;
this.id = id;
}
int IComparable.CompareTo(object obj)
{
Employee emp2 = (Employee) obj;
if (this.id > emp2.id)
return(1);
if (this.id < emp2.id)
return(-1);
else
return(0);
}
public static IComparer SortByName
{
get
{
return((IComparer) new SortByNameClass());
}
}
public static IComparer SortById
{
get
{
return((IComparer) new SortByIdClass());
}
}
public override string ToString()
{
return(name + ":" + id);
}
class SortByNameClass: IComparer
{
public int Compare(object obj1, object obj2)
{
Employee emp1 = (Employee) obj1;
Employee emp2 = (Employee) obj2;
return(String.Compare(emp1.name, emp2.name));
}
}
class SortByIdClass: IComparer
{
public int Compare(object obj1, object obj2)
{
Employee emp1 = (Employee) obj1;
Employee emp2 = (Employee) obj2;
return(((IComparable) emp1).CompareTo(obj2));
}
}
string name;
int id;
}
class Test
{
public static void Main()
{
Employee[] arr = new Employee[4];
arr[0] = new Employee("George", 1);
arr[1] = new Employee("Fred", 2);
arr[2] = new Employee("Tom", 4);
arr[3] = new Employee("Bob", 3);
Array.Sort(arr, Employee.SortByName);
// Mitarbeiter sind jetzt nach Name sortiert
foreach (Employee emp in arr)
Console.WriteLine("Employee: {0}", emp);
Array.Sort(arr, Employee.SortById);
// Mitarbeiter sind jetzt nach ID sortiert
foreach (Employee emp in arr)
Console.WriteLine("Employee: {0}", emp);
ArrayList arrList = new ArrayList();
arrList.Add(arr[0]);
arrList.Add(arr[1]);
arrList.Add(arr[2]);
arrList.Add(arr[3]);
arrList.Sort(Employee.SortByName);
foreach (Employee emp in arrList)
Console.WriteLine("Employee: {0}", emp);
arrList.Sort(); // Standard = nach ID
foreach (Employee emp in arrList)
Console.WriteLine("Employee: {0}", emp);
}
}
Die statischen Eigenschaften SortByName
und SortById erstellen eine Instanz der geeigneten Sortierungsklasse,
wandeln sie in IComparer um und geben sie an den Benutzer
zurück. Dies vereinfacht das Benutzermodell erheblich; die Eigenschaften
SortByName und SortById geben IComparer
zurück, es ist also offensichtlich, dass diese zur Sortierung eingesetzt
werden können. Alles, was der Benutzer dann noch tun muss, ist
die geeignete Sortierungseigenschaft für den IComparer-Parameter
anzugeben.
29.1.4 Überladen relationaler Operatoren
 
Weist eine Klasse eine Reihenfolge auf, die in IComparable
ausgedrückt wird, kann es sinnvoll sein, die weiteren relationalen
Operatoren zu überladen. Wie = und !=
müssen weitere Operatoren paarweise deklariert werden, <
und > stellen ein Paar, >= und <=
das andere Paar dar:
using System;
public class Employee: IComparable
{
public Employee(string name, int id)
{
this.name = name;
this.id = id;
}
int IComparable.CompareTo(object obj)
{
Employee emp2 = (Employee) obj;
if (this.id > emp2.id)
return(1);
if (this.id < emp2.id)
return(-1);
else
return(0);
}
public static bool operator <(
Employee emp1,
Employee emp2)
{
IComparable icomp = (IComparable) emp1;
return(icomp.CompareTo (emp2) < 0);
}
public static bool operator >(
Employee emp1,
Employee emp2)
{
IComparable icomp = (IComparable) emp1;
return(icomp.CompareTo (emp2) > 0);
}
public static bool operator <=(
Employee emp1,
Employee emp2)
{
IComparable icomp = (IComparable) emp1;
return(icomp.CompareTo (emp2) <= 0);
}
public static bool operator >=(
Employee emp1,
Employee emp2)
{
IComparable icomp = (IComparable) emp1;
return(icomp.CompareTo (emp2) >= 0);
}
public override string ToString()
{
return(name + ":" + id);
}
string name;
int id;
}
class Test
{
public static void Main()
{
Employee george = new Employee("George", 1);
Employee fred = new Employee("Fred", 2);
Employee tom = new Employee("Tom", 4);
Employee bob = new Employee("Bob", 3);
Console.WriteLine("George < Fred: {0}", george < fred);
Console.WriteLine("Tom >= Bob: {0}", tom >= bob);
}
}
Dieser Code erzeugt die folgende Ausgabe:
George < Fred: false
Tom >= Bob: true
29.1.5 Erweiterte Verwendung von Hashes
 
In einigen Situationen kann es wünschenswert
sein, mehr als einen Hashcode für ein spezifisches Objekt zu definieren.
Diese Methode kann beispielsweise eingesetzt werden, um basierend auf
Mitarbeiter-ID oder Mitarbeitername nach einem Mitarbeiter zu suchen.
Sie erreichen dies durch Implementierung der IHashCodeProvider-Schnittstelle,
wodurch eine alternative Hashfunktion bereitgestellt wird. Des Weiteren
ist eine passende Implementierung von IComparer erforderlich.
Diese neuen Implementierungen werden an die Erstellungsroutine der Hashtable
übergeben:
using System;
using System.Collections;
public class Employee: IComparable
{
public Employee(string name, int id)
{
this.name = name;
this.id = id;
}
int IComparable.CompareTo(object obj)
{
Employee emp2 = (Employee) obj;
if (this.id > emp2.id)
return(1);
if (this.id < emp2.id)
return(-1);
else
return(0);
}
public override int GetHashCode()
{
return(id);
}
public static IComparer SortByName
{
get
{
return((IComparer) new SortByNameClass());
}
}
public static IComparer SortById
{
get
{
return((IComparer) new SortByIdClass());
}
}
public static IHashCodeProvider HashByName
{
get
{
return((IHashCodeProvider) new HashByNameClass());
}
}
public override string ToString()
{
return(name + ":" + id);
}
class SortByNameClass: IComparer
{
public int Compare(object obj1, object obj2)
{
Employee emp1 = (Employee) obj1;
Employee emp2 = (Employee) obj2;
return(String.Compare(emp1.name, emp2.name));
}
}
class SortByIdClass: IComparer
{
public int Compare(object obj1, object obj2)
{
Employee emp1 = (Employee) obj1;
Employee emp2 = (Employee) obj2;
return(((IComparable) emp1).CompareTo(obj2));
}
}
class HashByNameClass: IHashCodeProvider
{
public int GetHashCode(object obj)
{
Employee emp = (Employee) obj;
return(emp.name.GetHashCode());
}
}
string name;
int id;
}
class Test
{
public static void Main()
{
Employee herb = new Employee("Herb", 555);
Employee george = new Employee("George", 123);
Employee frank = new Employee("Frank", 111);
Hashtable employees =
new Hashtable(Employee.HashByName, Employee.SortByName);
employees.Add(herb, "414 Evergreen Terrace");
employees.Add(george, "2335 Elm Street");
employees.Add(frank, "18 Pine Bluff Road");
Employee herbClone = new Employee("Herb", 000);
string address =(string) employees[herbClone];
Console.WriteLine("{0} lives at {1}", herbClone, address);
}
}
Diese Methode sollte nur sparsam eingesetzt werden.
Es ist häufig einfacher, einen Wert offen zu legen (beispielsweise
den Mitarbeiternamen als Eigenschaft) und diesen als Hashschlüssel
zu verwenden.
29.2 ICloneable
 
Die Funktion object.MemberWiseClone()
kann zum Erstellen von Klonen eines Objekts eingesetzt werden. Die Standardimplementierung dieser Funktion erzeugt eine »flache«
Objektkopie (shallow copy); die Objektfelder werden exakt kopiert, nicht
dupliziert. Sehen Sie sich folgendes Beispiel an:
using System;
class ContainedValue
{
public ContainedValue(int count)
{
this.count = count;
}
public int count;
}
class MyObject
{
public MyObject(int count)
{
this.contained = new ContainedValue(count);
}
public MyObject Clone()
{
return((MyObject) MemberwiseClone());
}
public ContainedValue contained;
}
class Test
{
public static void Main()
{
MyObject my = new MyObject(33);
MyObject myClone = my.Clone();
Console.WriteLine( "Values: {0} {1}",
my.contained.count,
myClone.contained.count);
myClone.contained.count = 15;
Console.WriteLine( "Values: {0} {1}",
my.contained.count,
myClone.contained.count);
}
}
Dieser Code erzeugt die folgende Ausgabe:
Values: 33 33
Values: 15 15
Da es sich bei der von MemberWiseClone()
erzeugten Kopie um eine flache Kopie handelt, ist der in contained
enthaltene Wert bei beiden Objekten gleich, und das Ändern eines
Wertes innerhalb des ContainedValue-Objekts wirkt sich
auf beide Instanzen von MyObject aus.
Wir benötigen eine »tiefe« Kopie
(deep copy), bei der für die neue Instanz von MyObject
eine neue Instanz von ContainedValue erzeugt wird. Dies
wird durch Implementieren der ICloneable-Schnittstelle
erreicht:
using System;
class ContainedValue
{
public ContainedValue(int count)
{
this.count = count;
}
public int count;
}
class MyObject: ICloneable
{
public MyObject(int count)
{
this.contained = new ContainedValue(count);
}
public object Clone()
{
Console.WriteLine("Clone");
return(new MyObject(this.contained.count));
}
public ContainedValue contained;
}
class Test
{
public static void Main()
{
MyObject my = new MyObject(33);
MyObject myClone = (MyObject) my.Clone();
Console.WriteLine( "Values: {0} {1}",
my.contained.count,
myClone.contained.count);
myClone.contained.count = 15;
Console.WriteLine( "Values: {0} {1}",
my.contained.count,
myClone.contained.count);
}
}
Dieser Code erzeugt die folgende Ausgabe:
Values: 33 33
Values: 33 15
Der Aufruf von MemberWiseClone() führt
nun zu einer neuen Instanz von ContainedValue, und die
Inhalte dieser Instanz können geändert werden, ohne dass dies
Auswirkungen auf die Inhalte von my hat.
Im Gegensatz zu anderen Schnittstellen, die für
ein Objekt definiert werden können, wird ICloneable
nicht von der Laufzeit aufgerufen; es wird lediglich sichergestellt,
dass die Clone()-Funktion über die geeignete Signatur
verfügt.
29.3 Entwurfsrichtlinien
 
Die implementierten virtuellen Funktionen und Schnittstellen
sollten sich nach dem beabsichtigten Zweck eines Objekts richten. Die
folgende Tabelle bietet hierzu einige Richtlinien:
Objektverwendung
|
Funktion oder Schnittstelle
|
Allgemein
|
ToString()
|
Arrays oder Auflistungen
|
Equals(), operator==(),
operator!=(), GetHashCode()
|
Sortierung oder binäre Suche
|
IComparable
|
Mehrere Sortierreihenfolgen
|
IComparer
|
Mehrere Hashsuchläufe
|
IhashCodeProvider
|
29.3.1 Funktionen und Schnittstellen der Framework-Klassen
 
In der folgenden Tabelle wird zusammengefasst, welche
Objektfunktionen oder -schnittstellen von den einzelnen Auflistungsklassen
verwendet werden.
Array
Funktion
|
Verwendung
|
IndexOf()
|
Equals()
|
LastIndexOf()
|
Equals()
|
Contains()
|
Equals()
|
Sort()
|
Equals(), IComparable
|
BinarySearch()
|
Equals(), IComparable
|
ArrayList
Funktion
|
Verwendung
|
IndexOf()
|
Equals()
|
LastIndexOf()
|
Equals()
|
Contains()
|
Equals()
|
Sort()
|
Equals(), IComparable
|
BinarySearch()
|
Equals(), IComparable
|
Hashtable
Funktion
|
Verwendung
|
HashTable()
|
IHashCodeProvider, IComparable (optional)
|
Contains()
|
GetHashCode(), Equals()
|
Item
|
GetHashCode(), Equals()
|
SortedList
Funktion
|
Verwendung
|
SortedList()
|
IComparable
|
Contains()
|
IComparable
|
ContainsKey()
|
IComparable
|
ContainsValue()
|
Equals
|
IndexOfKey()
|
IComparable
|
IndexOfValue()
|
Equals
|
Item
|
Icomparable
|
1
Es wäre möglich, mit IComparable eine
Sortierung und mit IComparer eine andere Sortierung zu implementieren,
dies wäre jedoch sehr verwirrend für den Benutzer.
|