12 Einige wichtige .NET-Klassen
12.1 Die Klasse »Object« 

Alle Klassen in der Klassenbibliothek des .NET Frameworks sind Mitglieder einer Klassenhierarchie, die sich über viele Verzweigungen in aufgabenspezifische Bereiche gliedert. Alle Klassen, so tief sie auch im Dickicht dieser Hierarchie stecken mögen, lassen sich aber auf die gemeinsame Basisklasse Object zurückführen. Wenn Sie eine benutzerdefinierte Klasse entwickeln, müssen Sie nicht explizit angeben, dass Ihre Klasse von Object abgeleitet ist – diese Ableitung geschieht implizit. Dass sich alle Klassen von Object ableiten, hat eine ganz wesentliche Konsequenz: Jeder Typ des Systems weist ein Minimum gemeinsamer Verhaltensweisen auf.
12.1.1 Der Konstruktor 

Die Liste der Konstruktoren ist nicht sehr lang – es gibt nur einen, der parameterlos ist und von jeder abgeleiteten Klasse aufgerufen wird, wenn ein Objekt erstellt wird. Wie Sie wissen, erfolgt dieser Aufruf implizit.
12.1.2 Die Methoden der Klasse »Object« 

Insgesamt sieben Methoden vererbt die Klasse Object an ihre Subklassen. Fünf dieser Methoden sind public und damit öffentlich, die beiden anderen Methoden protected und werden daher zwar vererbt, erlauben aber nur den Zugriff aus der erbenden Klasse heraus.
Sehen wir uns zunächst in Tabelle 12.1 alle Methoden in einem Überblick an.
| Methoden | Beschreibung |
|
Equals |
Diese Methode vergleicht zwei Objektreferenzen und liefert einen booleschen Wert zurück, dem entnommen werden kann, ob die beiden Referenzen auf dasselbe Objekt zeigen. |
|
Finalize |
Dient dazu, Ressourcen der Klasse freizugeben, wenn das Objekt zerstört wird. |
|
GetHashCode |
Liefert einen objektspezifischen Identifizierer. |
|
GetType |
Liefert die Referenz auf eine Type-Instanz zurück, die den Typ des aktuellen Objekts beschreibt. |
|
MemberwiseClone |
Dupliziert die aktuelle Instanz und liefert die Referenz auf das Duplikat zurück. |
|
ReferenceEquals |
Vergleicht zwei Objektreferenzen und liefert einen booleschen Wert zurück, dem entnommen werden kann, ob die beiden Referenzen auf dasselbe Objekt zeigen. |
|
ToString |
Liefert den vollqualifizierten Namen einer Klasse. |
Referenzvergleiche mit »Equals« und »ReferenceEquals«
Die beiden Methoden Equals und ReferenceEquals sind sich per Definition sehr ähnlich. Es werden zwei Objektvariablen miteinander verglichen, um festzustellen, ob beide dasselbe Objekt im Speicher referenzieren:
ClassA firstRef = new ClassA(); ClassA secondRef; secondRef = firstRef; Console.WriteLine(Object.Equals(firstRef, secondRef));
In diesem Codefragment wird die Referenz firstRef der Variablen secondRef zugewiesen. Beide Referenzen zeigen auf dasselbe konkrete Objekt, was der Aufruf der Equals-Methode bestätigt: Es wird true ausgegeben, was als referenzielle Identität der beiden Objektvariablen zu interpretieren ist. In diesem Fall können Sie sogar Equals gegen ReferenceEquals austauschen, am Ergebnis wird sich nichts ändern.
Sehen wir uns die Definition dieser beiden Methoden an, von denen Equals überladen ist:
public virtual bool Equals(object); public static bool Equals(object, object); public static bool ReferenceEquals(object, object);Die erste Variante der Equals-Methode ist als Instanzmethode, die zweite als Klassenmethode implementiert. Beachten Sie, dass die Instanzmethode als virtual gekennzeichnet ist und von jeder Klasse polymorph überschrieben werden kann. Die statische Equals-Variante ist nicht überschreibbar, ebenso die ähnlich lautende Methode ReferenceEquals. Damit ist auch garantiert, dass das Ergebnis des Aufrufs einer dieser beiden Methoden immer den Vergleich zwischen zwei Objektreferenzen liefert: Es ist true, wenn beide Referenzen auf ein und dasselbe Objekt verweisen, andernfalls lautet das Ergebnis false.
Das Überschreiben von »Equals« und die Methode »GetHashCode«
Wollen Sie selbst die Equals-Methode überschreiben, sollten Sie immer eine wichtige Regel beachten:
|
Wird die Equals-Methode einer Klasse überschrieben, sollte auch die Methode GetHashCode überschrieben werden. |
Schauen wir uns kurz die Definition dieser Methode an:
public virtual int GetHashCode();Was aber leistet die Methode GetHashCode, und wozu dient sie?
Ein Hashcode ist eine Zahl vom Typ int, die ein bestimmtes Objekt eindeutig identifiziert. Diese Zahl wird auch als Schlüssel (Key) bezeichnet, der immer zusammen mit einem Wert, beispielsweise einer Referenz, in Erscheinung tritt. Das Schlüssel-Wert-Paar wird in einer Hashtabelle verwaltet.
Jedes .NET-Objekt ist durch einen eindeutigen Schlüssel identifizierbar. Was hat das aber mit der Empfehlung zu tun, dass Sie als Entwickler GetHashCode überschreiben sollten, wenn Sie die Methode Equals überschreiben?
Standardmäßig benutzen wir Equals zum Referenzvergleich. Es könnte, wie Sie oben schon gesehen haben, natürlich auch ReferenceEquals sein oder – und genau das ist der entscheidende Punkt – auch über den Hashcode verglichen werden. Das folgende Beispiel beweist das. Es enthält eine Klasse mit einem parametrisierten Konstruktor, dem ein int übergeben wird. In der Main-Prozedur wird sowohl über Equals als auch über GetHashCode ein Vergleich angestellt, um zu bestimmen, ob zwei Variablen dasselbe Objekt referenzieren.
// ---------------------------------------------------------// Beispiel: ...\Kapitel 12\HashCode
// ------------------------------------------------------------
class Program { static void Main(string[] args) { ClassA obj1 = new ClassA(5); ClassA obj2 = obj1; // Vergleich auf Basis des Hashcodes if(obj2.GetHashCode() == obj1.GetHashCode()) Console.WriteLine("Hashcode: Objekte sind gleich"); // Vergleich auf Basis der Equals-Methode if(obj2.Equals(obj1)) Console.WriteLine("Equals: Objekte sind gleich"); Console.ReadLine(); } } class ClassA { public int MyProp; public ClassA(int i) { MyProp = i; } }
Die Ausgabe ist wie erwartet: Über beide Methodenaufrufe wird die Gleichheit festgestellt, da mit der Zuweisung der Objektvariablen obj1 an obj2 auch der Hashcode des Objekts obj1 gleichermaßen Berücksichtigung findet – wie alle anderen Felder des Objekts.
Equals vergleicht zwei Referenzen. Dem Überschreiben dieser Methode könnte die Idee zugrunde liegen, andere Vergleichskriterien für den betreffenden Typ heranzuziehen, beispielsweise bestimmte Feldinhalte zu vergleichen. Für eine solche Klasse würde gelten, dass zwei Objektvariablen dieses Typs per Definition dann »gleich« sind, wenn sie sich in den Feldinhalten nicht unterscheiden.
Es könnte beispielsweise festgelegt werden, dass zwei Variablen mit gleichem Feldinhalt grundsätzlich immer dasselbe Objekt referenzieren sollen. Diese Technik, die der Schonung der Speicherressourcen dient, ist nicht neu. String-Objekte werden nach ähnlichen Kriterien erzeugt:
string str1 = "Hallo Welt"; string str2 = "Hallo Welt"; Console.WriteLine(str1.GetHashCode()); Console.WriteLine(str1.GetHashCode());
Beide Zeichenfolgen haben denselben Inhalt, referenzieren dieselbe Adresse im Hauptspeicher und zeigen demnach auf dasselbe konkrete Objekt.
Wir wollen nun die Aussage von Equals so definieren, dass zwei Objekte vom Typ ClassA dann als gleich angesehen werden, wenn ihre Eigenschaft MyProp jeweils denselben Inhalt aufweist. Mit dieser Vorgabe wollen wir die Klasse ClassA unseres Beispiels durch die überschriebene Methode Equals ergänzen:
class ClassA { public int MyProp; public ClassA(int i) { MyProp = i; } public override bool Equals(object obj) { // Prüfen, ob ein gültiges Objekt übergeben wurde und ob // dieses auch dem Typ der Klasse entspricht if((obj == null) || !(obj is ClassA)) return false; // Rückgabewert des Feldvergleichs return MyProp == ((ClassA)obj).MyProp; } ... }
Als Erstes wird in der Methode geprüft, ob ein gültiges Objekt über den Parameter obj an die Methode übergeben wird und ob dieses auch dem Typ ClassA entspricht. Der letzte Vergleich ist notwendig, da der Parameter vom Typ Object ist und damit die Übergabe jedes x-beliebigen Typs erlaubt.
Sind beide Bedingungen erfüllt, kommt es mit
return MyProp == ((ClassA)obj).MyProp;
zu einem Vergleich der beiden Eigenschaften. Sind deren Inhalte identisch, liefert die Vergleichsoperation true zurück. Weichen die Inhalte voneinander ab, ist der Rückgabewert false.
Um die Änderung zu testen, müssen wir nur noch ein wenig am Code in Main ändern. Wir wollen jetzt nicht mehr den Nachweis führen, dass ein konkretes Objekt, das über zwei Referenzen angesprochen werden kann, denselben Hashcode aufweist, sondern das Ziel soll lauten, den Nachweis zu führen, dass mit der Überschreibung der Methode Equals auch die Methode GetHashCode überschrieben werden soll.
static void Main(string[] args) { ClassA obj1 = new ClassA(5); ClassA obj2 = new ClassA(5); // Vergleich auf Basis des Hashcodes if(obj2.GetHashCode() == obj1.GetHashCode()) Console.WriteLine("Hashcode: Objekte sind gleich"); else Console.WriteLine("Hashcode: Objekte sind nicht gleich"); // Vergleich auf Basis der Equals-Methode if(obj2.Equals(obj1)) Console.WriteLine("Equals: Objekte sind gleich"); else Console.WriteLine("Equals: Objekte sind nicht gleich"); Console.ReadLine(); }
Der Vergleich mit der Equals-Methode liefert die Aussage, dass beide Objekte gleich sind, aber der Vergleich über den Hashcode schlägt natürlich fehl, weil es sich um zwei verschiedene Objekte handelt.
Und genau in diesem Punkt besteht das Dilemma, wenn Sie zwar Equals, aber nicht GetHashCode überschreiben. Ein Benutzer Ihrer Klasse, der Objekte des Typs ClassA in einer HashTable sammelt, wird vielleicht einen Vergleich anhand des Hashcodes ziehen und nicht mit Equals. Es kommt zu einer widersprüchlichen Aussage, und die Klasse ist inkonsistent.
Es gehört nicht zu den trivialen Aufgaben, einen passenden Algorithmus zur Generierung des Hashcodes zu implementieren, wenn vom Standard abweichende Vergleichskriterien eine Rolle spielen. Es gibt zu diesem Thema einige gute, wenn auch langatmige Abhandlungen. Es wird jedoch immer ein Wert des Objekts zur Generierung des Hashcodes herangezogen – im einfachsten Fall der Inhalt einer Instanzvariablen des Objekts, z. B.:
public class ClassA { int intVar; // weiterer Klassencode public override int GetHashCode() { // mathematische Operation mit intVar // return Hashcode; } }
Das komplette Beispiel finden Sie auf der Buch-DVD unter:
...\Kapitel 12\HashCodeOverriding.
»ToString« und »GetType«
Wenden wir uns den beiden nächsten Methoden zu: ToString und GetType. Widmen wir uns zunächst der erstgenannten, die wir in den vergangenen Kapiteln schon oft benutzt haben:
public virtual string ToString();ToString liefert per Definition eine Zeichenfolge zurück, der den vollqualifizierten Namen der Klasse, also einschließlich der Angabe des Namespace, enthält. Viele Klassen überschreiben diese Methode, die dann einen anderen Rückgabewert hat. Sehen wir uns das an zwei Beispielen an:
string strText = "Visual C# ist spitze!"; Console.WriteLine(strText.ToString()); int intVar = 4711; Console.WriteLine(intVar.ToString());
Die Ausgabe lautet:
Visual C# ist spitze!
und
4711
Die Typen String und int überschreiben demnach ToString und liefern den Inhalt der Variablen, auf der die Methode aufgerufen worden ist.
Mit GetType können Sie sich den Typ der Klasse besorgen, allerdings müssen Sie dazu die Rückgabe in einen String konvertieren, z. B.:
int intVar = 10; Console.WriteLine(Convert.ToString(intVar.GetType()));
Jetzt wird nicht der Inhalt der Variablen, sondern der Datentyp ausgegeben. Sie müssen an dieser Stelle eine Konvertierung vornehmen, weil der Rückgabewert vom Typ Type ist:
public Type GetType();Die Klasse Type liefert eine Referenz auf das Type-Objekt eines konkreten Objekts zurück. Dieses versetzt uns in die Lage, den Datentyp einer genaueren Analyse zu unterziehen.
Die Methode »MemberwiseClone«
Die Methode MemberwiseClone kopiert ein vorhandenes Objekt und liefert die Referenz auf die Kopie.
protected object MemberwiseClone();Dabei werden alle Felder des Originals dupliziert: Felder, die auf Wertetypen basieren, werden bitweise kopiert, ebenso die auf Referenztypen basierenden Felder. Das bedeutet, dass die in einem Objekt referenzierten Subobjekte ihrerseits nicht dupliziert werden. Sowohl das Originalobjekt als auch die Kopie greifen auf dieselben Subobjekte zu.
MemberwiseClone ist geschützt deklariert und kann nur aus der aktuellen Instanz heraus aufgerufen werden. Sie müssen daher eine öffentliche Methode bereitstellen, auf die externer Code zugreifen kann. Die .NET-Klassenbibliothek bietet dazu mit ICloneable ein Interface an, dessen Sie sich bedienen sollten. Dieses Interface veröffentlicht nur die Methode Clone:
object Clone();Innerhalb einer zu duplizierenden Klasse wird Clone überschrieben. Clone ruft MemberwiseClone auf das aktuelle Objekt auf und liefert als Rückgabe die Referenz auf das Duplikat:
public object Clone() {
return this.MemberwiseClone();
}Etwas komplizierter wird es, wenn in einer Klasse Felder definiert sind, die auf Referenztypen basieren, und Sie gleichzeitig auch alle Subobjekte duplizieren möchten. Schauen Sie sich dazu die Implementierung der Klasse CloneableClass an:
public class CloneableClass : ICloneable { public long MyProp; public ClassA internClass = new ClassA(); public object Clone() return this.MemberwiseClone(); } ... }
Die Methode Clone wird an einen Aufrufer die Referenz auf eine Kopie zurückliefern. In der Kopie wird das Feld MyProp denselben Inhalt aufweisen wie das des Originals. Da MemberwiseClone alle Felder bitweise kopiert, referenziert die Eigenschaft internClass des Duplikats jedoch dasselbe Objekt wie das Original. Benötigen wir auch vom internen Objekt eine Kopie, haben wir zwei Möglichkeiten:
- Wir erzeugen ein neues Objekt vom Typ ClassA und weisen diesem alle Eigenschaftswerte der Instanz internClass explizit zu.
- Wir hoffen, dass der Entwickler der Klasse ClassA weitsichtig genug war und die Schnittstelle ICloneable-Methode implementiert.
Wir wollen an einem Beispiel die unter Punkt 2 genannte Variante studieren. Das Objekt dupliziert sich selbst mit MemberwiseClone und liefert die Referenz der Kopie an den Aufrufer zurück. Exemplarisch nehmen wir die folgende Implementierung an:
public class ClassA : ICloneable { public int intVar; public ClassA() { Random rand = new Random(); intVar = rand.Next(0, 1000); } public object Clone() { return this.MemberwiseClone(); } }
Beim Aufruf des parameterlosen Konstruktors wird nach dem Zufallsprinzip eine Zahl zwischen 0 und kleiner 1000 in der Instanzvariablen intVar festgehalten. Sie dient dazu, später den Erfolg des Klonens zu beweisen.
Nun können wir uns noch einmal der Klasse CloneableClass zuwenden und die Methode Clone gemäß unseren Anforderungen überarbeiten.
public class CloneableClass : ICloneable { public long MyProp; private ClassA internClass = new ClassA(); public object Clone() { CloneableClass internObj; internObj = (CloneableClass)this.MemberwiseClone(); internObj.internClass = (ClassA)this.internClass.Clone(); return internObj; } }
Zuerst wird in der Methode Clone die Variable internObj vom Typ CloneableClass deklariert und dieser über MemberwiseClone die Referenz auf das Duplikat zugewiesen:
internObj = (CloneableClass)this.MemberwiseClone();
Der Rückgabewert von MemberwiseClone ist vom Typ Object und muss in den Typ CloneableClass konvertiert werden. Die Kopie referenziert noch dasselbe interne Objekt wie das Original. Vom Objekt internClass besorgen wir uns deshalb eine Kopie und weisen diese dem Feld internClass der Klone zu.
Damit haben wir unser Ziel erreicht. Was uns jetzt noch fehlt, ist die Bestätigung unserer Überlegungen. Dazu benutzen wir die Methode GetHashCode, da sich jedes Objekt durch einen eigenen Hashcode identifizieren lässt.
// ---------------------------------------------------------// Beispiel: ...\Kapitel 12\MemberwiseClonen
// -------------------------------------------------------------
class Program { static void Main(string[] args) { CloneableClass obj = new CloneableClass(); CloneableClass dupli = (CloneableClass)obj.Clone(); Console.WriteLine("Hashcode(obj) = {0}", obj.GetHashCode()); Console.WriteLine("Hashcode(dupli) = {0}", dupli.GetHashCode()); Console.WriteLine("Hashcode(obj.internClass) = {0}", obj.internClass.GetHashCode()); Console.WriteLine("Hashcode(dupli.internClass) = {0}",dupli.internClass.GetHashCode()); Console.WriteLine("intVar(obj) = {0}", obj.internClass.intVar); Console.WriteLine("intVar (dupli) = {0}", dupli.internClass.intVar); Console.ReadLine(); } }
Der Testcode könnte an der Konsole zur folgenden Anzeige führen:
Hashcode(obj) = 70 Hashcode(dupli) = 72 Hashcode(obj.internClass) = 73 Hashcode(dupli.internClass) = 74 internVar(obj) = 768 internVar(dupli) = 768
Die Hashcodes der Referenzen dupli und obj sind unterschiedlich, ebenso die des enthaltenen Feldes vom Typ ClassA. Daraus kann der Schluss gezogen werden, dass das ClassA-Feld des Originals ebenfalls geklont worden ist.




Jetzt bestellen





