16 Einige wichtige .NET-Klassen
16.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.
Object hat nur einen parameterlosen Konstruktor und insgesamt sieben Methoden. Fünf dieser Methoden sind public und damit öffentlich, die beiden anderen Methoden sind protected und erlauben daher nur den Zugriff aus einer erbenden Klasse heraus. Sehen wir uns zunächst in Tabelle 16.1 alle Methoden in einem Überblick an.
Methoden | Beschreibung |
Diese Methode vergleicht zwei Objektreferenzen und liefert einen booleschen Wert zurück, dem entnommen werden kann, ob die beiden Referenzen auf dasselbe Objekt zeigen. |
|
Dient dazu, Ressourcen der Klasse freizugeben, wenn das Objekt zerstört wird. |
|
Liefert einen eindeutigen numerischen Identifizierer. |
|
Liefert die Referenz auf eine Type-Instanz zurück, die den Typ des aktuellen Objekts beschreibt. |
|
Dupliziert die aktuelle Instanz und liefert die Referenz auf das Duplikat zurück. |
|
Vergleicht zwei Objektreferenzen und liefert einen booleschen Wert zurück, dem entnommen werden kann, ob die beiden Referenzen auf dasselbe Objekt zeigen. |
|
Liefert den vollqualifizierten Namen einer Klasse. |
16.1.1 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:
Demo object1 = new Demo();
Demo object2;
object2 = object1;
Console.WriteLine(Object.Equals(object1, object2));
Listing 16.1 Referenzvergleich mit »Equals«
In diesem Codefragment wird die Referenz object1 der Variablen object2 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.
Equals wird sowohl als Instanz- als auch als Klassenmethode angeboten. Die Instanzmethode ist virtual gekennzeichnet und kann von jeder Klasse polymorph überschrieben werden. 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.
16.1.2 »ToString« und »GetType«

ToString liefert per Definition eine Zeichenfolge zurück, die den vollqualifizierten Namen der Klasse, also einschließlich der Angabe des Namespaces, enthält. Viele Klassen überschreiben diese Methode und haben somit einen abweichenden Rückgabewert. Sehen wir uns das an zwei Beispielen an:
string text = "Visual C# 5.0 ist spitze!";
Console.WriteLine(text.ToString());
int value = 4711;
Console.WriteLine(value.ToString());
Listing 16.2 Rückgabewerte der Methode »ToString«
Die Ausgabe lautet:
Visual C# 5.0 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 value = 10;
Console.WriteLine(Convert.ToString(value.GetType()));
Jetzt wird nicht der Inhalt der Variablen value, sondern der Datentyp ausgegeben. Sie müssen an dieser Stelle eine Konvertierung vornehmen, weil der Rückgabewert vom Typ Type ist. 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.
16.1.3 Die Methode »MemberwiseClone« und das Problem des Klonens

Kommen wir zu einem leider etwas nebulösen Teil des .NET Frameworks. Es handelt sich dabei um das Klonen von Objekten. Nebulös deshalb, weil es Microsoft versäumt hat, exakt zu spezifizieren, ob bei den jeweiligen Klonvorgängen eine flache oder tiefe Kopie erzeugt werden soll.
Flache Kopien
Unter einer flachen Kopie versteht man, dass nur eine einfache Kopie eines Objekts erstellt wird. Enthält das zu kopierende Objekt einen Verweis auf ein anderes Objekt (nennen wir es »untergeordnetes Objekt«), wird letzteres nicht dupliziert. Stattdessen verweist auch die Kopie auf dasselbe untergeordnete Objekt wie das Original. Sehen Sie dazu das folgende Listing an, in dem die beiden Klassen Owner und Account beschrieben werden.
public class Owner {
public string Name { get; set; }
public int Alter { get; set; }
}
public class Account {
public int AccountNo { get; set; }
public Owner Owner { get; set; }
}
Listing 16.3 Die Definition der Klassen »Owner« und »Account«
Beachten Sie, dass die Klasse Account eine Eigenschaft vom Typ Owner beschreibt, einem Referenztyp. Das von dieser Eigenschaft referenzierte Objekt ist das oben erwähnte »untergeordnete Objekt«. Wenn wir die flache Kopie eines bestehenden Account-Objekts erzeugen, wird zwar ein neues Account-Objekt erstellt, aber die Kopie verweist in ihrer Eigenschaft Owner auf dasselbe Owner-Objekt wie das Original-Account-Objekt. Das Owner-Objekt wird bei einer flachen Kopie nicht neu erzeugt, also dupliziert.
Die Methode MemberwiseClone der Klasse Object erstellt eine flache Kopie des aktuellen Objekts und liefert die Referenz auf die Kopie. Da MemberwiseClone protected definiert ist, wird eine andere Methode erforderlich, um das Klon an den Aufrufer zurückzugeben. Lassen Sie uns eine solche Methode bereitstellen. Wir können dazu auf das Interface ICloneable des .NET Frameworks zurückgreifen, über das die Methode Clone vorgeschrieben wird.
public class Account : ICloneable {
public int AccountNo { get; set; }
public Owner Owner { get; set; }
// Methode des Interfaces ICloneable
public object Clone() {
return this.MemberwiseClone();
}
}
Listing 16.4 Die Klasse »Account« mit dem Interface »ICloneable«
Clone ruft MemberwiseClone des aktuellen Objekts auf. Dabei werden alle Felder des Originals bitweise kopiert. Das betrifft sowohl die Felder, die auf Wertetypen basieren, als auch die Felder, die auf Referenztypen basieren. Das hat zur Konsequenz, dass die Eigenschaften, die Referenztypen beschreiben, nur die ursprüngliche Referenz in den Klon schreiben, aber kein neues Objekt erzeugen (siehe Abbildung 16.1).
Abbildung 16.1 Prinzip der »flachen Kopie«
Tiefe Kopien
Bei einer tiefen Kopie werden nicht nur die auf Wertetypen basierenden Felder kopiert, sondern auch die Felder, die auf Referenztypen basieren und damit untergeordnete Objekte beschreiben. Bezogen auf die Klassen in Listing 16.4 würde das bedeuten, dass beim Klonen eines Account-Objekts auch eine Kopie des untergeordneten Objekts Owner erstellt wird (siehe Abbildung 16.2).
Die unpräzise Spezifizierung des Klonens bei der Implementierung der Schnittstelle ICloneable hat zu vielen Diskussionen geführt. Soll mit diesem Interface flach oder tief kopiert werden? Es gibt darüber keine allgemeingültige Aussage. Es gibt leider auch keine andere Schnittstelle, die eine tiefe Kopie vorschreiben würde. Da sehr viele Klassen des .NET Frameworks die Schnittstelle ICloneable implementieren, bleibt Ihnen im Zweifelsfall nichts anderes übrig, als die entsprechende Dokumentation zu lesen.
Abbildung 16.2 Prinzip der »tiefen Kopie«
Wenden wir uns wieder den beiden Klasse Owner und Account zu. Optimalerweise implementieren wir in beiden Klassen das Interface ICloneable. Die Methode Clone in der Klasse Owner erzeugt intern mit MemberwiseClone nur einen »einfachen« Klon von sich selbst. In Account hingegen muss darüber hinaus auch noch ein Klon des Eigenschaftswertes Owner erstellt werden. Erst damit ist eine tiefe Kopie eines Account-Objekts gewährleistet.
Im folgenden Beispielprogramm ist der gesamt Code der beiden angesprochenen Klassen gezeigt. Zum Testen des Erfolgs des tiefen Klonens reicht es in diesem Fall völlig aus, in Main den Hashcode des untergeordneten Owner-Objekts abzufragen.
// Beispiel: ..\Kapitel 16\MemberwiseClonen
class Program {
static void Main(string[] args) {
Owner owner = new Owner { Name = "Herbert Meier", Alter = 28 };
Account account = new Account { Owner = owner, AccountNo = 1 };
// Erstellen einer tiefen Kopie
Account copy = (Account)account.Clone();
Console.WriteLine("Hash des Originals: {0}",
account.Owner.GetHashCode());
Console.WriteLine("Hash der Kopie: {0}", copy.Owner.GetHashCode());
Console.ReadLine();
}
}
public class Owner : ICloneable {
public string Name { get; set; }
public int Alter { get; set; }
public object Clone()
{
return this.MemberwiseClone();
}
}
public class Account : ICloneable {
public int AccountNo { get; set; }
public Owner Owner { get; set; }
public object Clone()
{
Account acc = (Account)MemberwiseClone();
acc.Owner = (Owner)Owner.Clone();
return acc;
}
}
Listing 16.5 Erstellen einer »tiefen Kopie«
Der Prozess des tiefen Kopierens muss den gesamten Objektgraphen erfassen und alle darin beschriebenen Objekte duplizieren. Am Ende haben die geklonten Objekte keinerlei Bezug mehr zu ihrem Original. Eine tiefe Kopie zu erstellen ist daher nicht immer so einfach, wie dieses Beispielprogramm vielleicht suggerieren mag. Je nach Typ und je nachdem, ob untergeordnete Objekte ihrerseits selbst wieder untergeordnete Objekte haben, die tief kopiert werden müssen, kann das Erstellen einer tiefen Kopie sehr aufwendig werden.
Es gibt mit dem Prozess der Serialisierung noch eine andere Möglichkeit, eine tiefe Kopie zu erstellen. Das setzt voraus, dass die Klassen mit dem Serializable-Attribut verknüpft sind. Das zu serialisierende Objekt wird einfach in einen Stream vom Typ MemoryStream serialisiert und anschließend sofort wieder deserialisiert.
public object Clone()
{
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, this);
stream.Position = 0;
return formatter.Deserialize(stream);
}
Dabei darf nicht vergessen werden, die aktuelle Position des Streams nach der Serialisierung auf »0« zurückzusetzen.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.