Das Klonen und die Tabelle "Dolly" (Teil 1)

Veröffentlicht: 19. Jul 2001 | Aktualisiert: 18. Jun 2004
Von Dino Esposito

Im richtigen Leben ist das Klonen ein sehr heißes Eisen, egal welche Auffassung man vertritt. Im Softwarebereich kann das Klonen in einigen Fällen jedoch ein sehr nützliches Werkzeug sein. Ich ertappe mich jedenfalls oft dabei, dass ich Instanzen ausgeführter Objekte dupliziere, um fast identische Objekte zu erstellen, die sich codetechnisch auf ziemlich unabhängige Weise verwalten lassen.

Dies ist kein spezifisches Merkmal von .NET oder einem anderen Framework wie Microsoft Foundation Class (MFC), Active Template Library (ATL) oder ActiveX Data Object (ADO). Die Möglichkeit, aus einer vorhandenen Instanz eines Objekts ein anderes Objekt zu erstellen, ist in allen Sprachen und Programmierkontexten üblich.

Wie im richtigen Leben auch, gibt es keine klar festgelegten und weltweit anerkannten Regeln für das Klonen im Bereich der Software. Bei jedem Objektklonvorgang muss experimentiert werden, und er kann neue und unvorhersehbare Auswirkungen nach sich ziehen. Aus diesem Grund können nicht alle Objekte geklont werden (unabhängig vom Framework), und das Verhalten kann von Objekt zu Objekt stark variieren. Beispiele gefällig?
Zeichenfolgen lassen sich in MFC mit Hilfe des Klassenkonstruktors oder mit Hilfe der C-Laufzeit-Bibliotheksfunktion strdup() schnell und einfach duplizieren. Nachdem Sie die Kopie erstellt haben, verwenden Sie die Kopie und das Original als vollkommen unterschiedliche Elemente, ohne daran zu denken, wie sie zustande kamen. ADO-Recordsets weisen beispielsweise ganz andere Features auf.
Unter .NET wird das Klonen über die ICloneable-Schnittstelle unterstützt. Bei einigen Objekten wird sie automatisch implementiert, während andere Objekte die Klonfunktionen auf flaches Kopieren beschränken. Für diese Objekte kann eine tiefe Kopie nur manuell erstellt werden. (Mehr dazu weiter unten und in Teil 2.)

In diesem zweiteiligen Artikel werde ich zuerst die Unterstützung des Klonens in ADO und ADO.NET erläutern. Danach spreche ich einen berühmten Fall an, der im Zusammenhang mit dem Klonen von Menschen die Nachrichten in letzter Zeit beherrscht hat - der Fall Dolly.
Sie wissen vielleicht bereits, dass Dolly der Name des ersten DataTable-Objekts ist, für das ich in meinem Beta 1-Code zum ersten Mal den Versuch einer Duplizierung unternommen habe.
Zuerst sehen wir uns aber das Klonen von Tabellen aus Sicht von ADO an.

ADO und das Klonen von Recordsets
Ein ADO-Recordset kann mit Hilfe der Clone-Methode auf einfache Weise geklont werden. Sie verwenden Clone, wenn Sie ein vorhandenes Recordset duplizieren möchten (Sie möchten ein zweites Recordset mit einem anderen Variablennamen und dem gleichen Inhalt erhalten). Die Verwendung von Clone ist natürlich schneller als das Initialisieren und Füllen eines neuen Recordsets mit Verbindungs- und Befehlsobjekten.

Sie erhalten beim Klonen einen Zeiger auf die gleichen Daten, die programmtechnisch über ein bestimmtes Recordset-Objekt verfügbar sind. Dies hat eine Reihe von guten und weniger guten Nebeneffekten. Kurz gesagt, können Sie beide Instanzen nicht nach Belieben verwenden, nachdem Sie die Kopie des Recordset erstellt haben. Stattdessen müssen Sie sich einiger Eigentümlichkeiten bewusst sein und sehr sorgfältig vorgehen.

Ein positiver Aspekt eines geklonten Recordsets ist der geringe Overhead, der für die Erstellung und Verwendung erforderlich ist. Ein geklontes Recordset ist lediglich eine Objektvariable mit einem Verweis auf die ursprünglichen Recordsetdaten. Zusätzlich weist das geklonte Recordset einen separaten Puffer zum Verfolgen von Lesezeichen auf. Die Navigation in den Recordsets erfolgt vollkommen separat, und die Recordsets können sich an völlig unterschiedlichen Orten befinden. Das Original und der Klon erscheinen auch unabhängig, wenn es um das Schließen und Filtern geht.

Ein nicht so schöner Aspekt des Klonens - Sie müssen sich dessen nur bewusst sein - ist, dass sich der Datensatzzeiger des geklonten Objekts zu Anfang immer auf dem ersten Datensatz befindet. Dabei spielt es keine Rolle, welche Position er zum Zeitpunkt des Klonens im Original eingenommen hat. Außerdem gehen beim Erstellen eines Klons alle Filtermasken verloren, die dem Original zugewiesen sind. In beiden Fällen können Sie den Originalzustand mit Hilfe von zwei zusätzlichen Anweisungen wiederherstellen:

Set rsClone = rsOrig.Clone  
rsClone.Move rsOrig.AbsolutePosition 
rsClone.Filter = rsOrig.Filter

Beachten Sie dabei aber, dass die Positionierung im Datensatz vom verwendeten Cursortyp abhängt und dass ein Fehler auftreten kann, wenn Sie versuchen, den standardmäßigen, vorwärtsgerichteten Cursor an einer beliebigen Stelle im Recordset zu positionieren.

In der Abbildung unten ist die Architektur grafisch dargestellt, die es sowohl dem Original als auch dem Klon ermöglicht, persönliche Lesezeichen zu verwenden und gleichzeitig die Daten gemeinsam zu nutzen.

Bild01













Abbildung 1. Original und geklontes Recordset zeigen ursprünglich auf die gleichen Daten

Die Verwendung eines Klons bietet sich vor allem an, wenn Sie mehrere "aktuelle" Datensätze verwalten müssen und dabei nicht immer von einem Ort zum anderen springen möchten, um mit Lesezeichen versehene Datensätze aus einem Recordset abzurufen.

Was passiert eigentlich, wenn Sie in einem der beiden Recordsets einen Datensatz ändern? Das Original und der Klon sind im Hinblick auf Updates synchronisiert, und beide Recordsets bemerken die Änderung. Wenn wir uns die dargestellte Architektur ansehen, überrascht diese Tatsache nicht, da beide den gleichen Datenpuffer nutzen.

Dabei können trotzdem bereits ernsthafte Probleme auftreten, was aber je nach den Umständen noch nicht alles ist. Wenn Daten in einem Recordset geändert werden, erhalten alle dazugehörigen Klone Benachrichtigungen, so als ob sie von dem Update unmittelbar betroffen wären. Dies liegt wiederum daran, dass sie den gleichen Datenpuffer nutzen.

Ereignisse, die bei einer Änderung ausgelöst werden, z.B. WillChangeField oder FieldChangeComplete, übergeben ein Recordset und Informationen zu den betroffenen Feldern an die Ereignishandler. Das Recordset ist jedoch lediglich ein Verweis auf den Ort der eigentlichen Änderung. In anderen Worten, der aktuelle Datensatz im Recordset, den ein Klon aufgrund von Ereignissen empfängt, kann sich vom tatsächlichen Datensatz im Klon selbst unterscheiden. Ereignisse, die für mehrere Klone gelten, positionieren den Datensatzzeiger nicht automatisch neu. Dies ist nicht unbedingt ein Nachteil, aber es sollte den Entwicklern auf jeden Fall bewusst sein.

Die Verknüpfung, die das Original und den Klon aus Gründen der Updatefähigkeit verbindet, wird nur gelöst, wenn Sie von einem der beiden Orte aus die Requery-Methode aufrufen. Mit Requery werden die Daten in einem Recordset-Objekt aktualisiert, indem der Abfragebefehl erneut ausgeführt wird, den Sie ursprünglich zum Füllen verwendet haben.

Durch Requery erhält ein Klon seine eigenen Daten und wird von den vorher gemeinsam genutzten Daten getrennt. Daher zeigen das Original und der Klon dann auf verschiedene Datengruppen. Dies gilt ebenso, wenn Sie Requery für die Originalversion des Recordset ausführen. In der folgenden Abbildung ist dies grafisch dargestellt.

Bild02















Abbildung 2. Der Aufruf von "Requery" für einen Klon führt zu seiner eigenen Datengruppe

Beachten Sie, dass das Original durch Requery nur von dem Klon getrennt wird, das es aufruft. Alle anderen Klone, die vom gleichen ursprünglichen Recordset stammen, sind von diesem Vorgang nicht betroffen und verwenden weiterhin die gleichen Daten.

Tiefe und flache Kopie
Nur unerfahrene Programmierer glauben, dass sich Objekte duplizieren lassen, indem sie die Objekte einfach einer neuen Variable zuweisen. Mit dem folgenden Code wird das Recordset nicht dupliziert, sondern es wird lediglich ein neuer Zeiger für das gleiche Objekt definiert.

Set rs2 = rs1

Dieser Vorgang wird i.Allg. als flaches Kopieren bezeichnet. Dabei handelt es sich um eine Teilkopie, bei der nicht die gesamte Struktur des Objekts verwendet wird, sondern lediglich die Schnittstelle der obersten Ebene. Der Umfang der Schnittstelle der obersten Ebene ist nicht in allen Kontexten gleich und kann sich von Framework zu Framework unterscheiden. Es kann nur der Zeiger auf die aktuelle Instanz sein oder auch das Objekt selbst beinhalten, jedoch nicht die untergeordneten Objekte.

Sie erhalten eine tiefe Kopie (engl.: "Deep Copy" im Gegensatz zu "Shallow Copy"), wenn Sie das Objekt nur klonen, um eine perfekte, unabhängige Kopie mit identischen Funktionen zu erhalten.

Da wir gerade von ADO-Recordsets sprechen: Sie erhalten eine flache Kopie, wenn Sie die Variable duplizieren. Sie erhalten eine etwas tiefere flache Kopie, wenn Sie Clone aufrufen. Die Objekte sind dabei nicht vollständig unabhängig, da sie immer noch den gleichen Datenpuffer nutzen. Nur mit Hilfe des folgenden Codeabschnitts wird die Variable

rs2

zu einer wirklich tiefen Kopie des ursprünglichen ADO-Recordset-Objekts rs1.

Set rs2 = rs1.Clone 
rs2.Requery

Die Begriffe "flache Kopie" (Shallow Copy) und tiefe Kopie (Deep Copy) werden in der .NET-Dokumentation sehr häufig verwendet, um das Klonen innerhalb des Frameworks zu erklären.

Klonen bei .NET
Unter .NET gibt es zwei Arten von Typen: Werttypen und Verweistypen. Werttypen umfassen primitive Typen, Enumerationen und Strukturen und werden über den Stack zugewiesen. Verweistypen beinhalten Klassen und Arrays und werden über den Heap abgewickelt. Ein Werttyp trägt seinen Inhalt sozusagen immer mit sich herum. Ein Verweistyp zeigt dagegen auf einen Speicherpuffer, wo Teile des Inhalts bzw. der gesamte Inhalt verwaltet wird.
Unter .NET können Sie Objekte auf zwei Arten duplizieren, die unterschiedliche Funktionen und Leistungen bieten. Sie können von einem .NET-Objekt entweder eine flache oder eine tiefe Kopie erstellen.

Eine flache Kopie ist nur eine Kopie des Objekts. Wenn das Objekt eine komplexe Struktur enthält und Verweise auf untergeordnete Objekte aufweist, werden diese nicht mit dupliziert. Alle untergeordneten Objekte in der flachen Kopie verweisen weiterhin auf das Originalobjekt. (Dies entspricht mehr oder weniger dem, was bei einem geklonten ADO-Recordset mit den Daten passiert, jedoch nicht mit den Lesezeichen und Filtern.)

Bei einer tiefen Kopie wird das gesamte Objekt übertragen. Sie erhalten ein neues Objekt, bei dem alle Elemente, auf die das ursprüngliche Objekt direkt oder indirekt verweist, dupliziert werden.

Das Stammobjekt von .NET Framework, nämlich das Object-Objekt, verfügt über eine Methode mit der Bezeichnung MemberwiseClone, die eine flache Kopie des aktuellen Objekts erstellt.

protected object MemberwiseClone();

Diese Methode stellt die integrierte Fähigkeit von .NET-Objekten dar, flache Kopien zu implementieren. Die Methode ist geschützt und kann nicht außer Kraft gesetzt werden.

Wenn Sie der Meinung sind, dass der Standardmechanismus zur Erzeugung von flachen Kopien für Ihre Objekte nicht funktioniert, müssen Sie entweder eine ganz neue Methode zum Klonen/Kopieren bereitstellen oder die ICloneable-Schnittstelle implementieren. Wenn Sie mehr als nur die Standardklone benötigen, also nicht nur wie oben beschrieben eine flache Kopie, müssen Sie selbst die Initiative ergreifen.

Die "ICloneable"-Schnittstelle
ICloneable ist die typische Schnittstelle, die .NET-Klassen implementieren, wenn sie das Klonen ermöglichen möchten. Beachten Sie, dass Sie durch nichts daran gehindert werden, eine benutzerdefinierte Schnittstelle zu verwenden oder eine Methode für das Klonen offen zu legen. Da ICloneable jedoch die Standardschnittstelle zum Klonen von Objekten ist, sollte sie für die entsprechenden Objekte auch verwendet werden.

ICloneable enthält nur eine Methode, und zwar Clone. Was Sie innerhalb dieser Methode tun, hängt von dem Objekt ab, das Sie klonen möchten. Im Allgemeinen verwenden Sie die Methode, wenn Sie das Erstellen von tiefen Kopien ermöglichen oder einfach nur mehr als die Standardfunktionen von MemberwiseClone bereitstellen möchten.

Sie können Clone sowohl als flache Kopie als auch als tiefe Kopie implementieren. Aus Gründen der Konsistenz sollten Sie sicherstellen, dass Sie immer eine Kopie der aktuellen Instanz des Objekts bereitstellen.

Das .NET Framework verfügt über eine Reihe von Objekten, die ICloneable bereits implementieren. So finden Sie z.B. Array, HashTable, Queue, String, eine Vielzahl von GDI-Objekten, XmlNavigator und XmlNode.

Klonen von ADO.NET-Objekten
Unter den klonfähigen Objekten befinden sich auch verschiedene ADO.NET-Objekte wie DBCommand, DBConnection, DBDataSetCommand, DataTableMapping und SQLParameter. Alle Objekte (wie auch die davon abgeleiteten Klassen) verfügen über eine Clone-Methode, durch die eine geeignete, mehr oder weniger tiefe Duplizierung möglich ist.

Die Clone-Methode der DBDataSetCommand-Klasse wird über die Klassen SQLDataSetCommand und ADODataSetCommand abgewickelt. Sie erhalten dabei keine vollständige Kopie der Klasse, aber es wird ein neues Objekt ausgegeben, für das so viele Informationen wie möglich dupliziert werden, ohne gemeinsam genutzte Ressourcen wie Verbindungen zu sehr zu beeinträchtigen.

Nicht bei allen in DBDataSetCommand eingebetteten Objekten handelt es sich um tiefe Kopien. Konfigurationsobjekte wie TableMappings, MissingSchemaAction und MissingMappingAction werden dupliziert. Dies ist bei den Befehlsobjekten SelectDBCommand, InsertDBCommand, DeleteDBCommand oder UpdateDBCommand nicht der Fall.

Genauer gesagt, ruft die Clone-Methode von DBDataSetCommand wiederum die Clone-Methode für alle Command-Objekte auf, die sie enthält. Aus Gründen der Skalierbarkeit ergeben sich beim Klonen von DBCommand-Objekten keine tiefen Kopien. Die aktive Verbindung für die Befehle wird nicht kopiert, sondern nur gemeinsam genutzt.

Es ist erkennbar, dass diese offensichtliche Verletzung der einfachsten Klongesetze sehr günstig für Anwendungen ist, da Klone von DBDataSetCommand mit der gleichen Verbindung wie beim Original verwendet werden können. Wenn Sie lediglich eine neue Verbindung benötigen, müssen Sie nur die ActiveConnection-Eigenschaft des Command-Objekts ersetzen.

Die Tabelle "Dolly"
Nicht alle ADO.NET-Klassen weisen eine erweiterte Unterstützung für das Klonen auf. Nicht bei allen wird ICloneable tatsächlich implementiert. Besonders in Beta 1 finden Sie unter DataTable, DataRow und DataColumn keine speziellen Klonfunktionen.

Beim DataSet-Objekt sieht es dagegen etwas anders aus. Die ICloneable-Schnittstelle wird dabei nicht implementiert, aber es werden zwei Methoden mit den Bezeichnungen Clone und Copy bereitgestellt, um zwei Arten des Klonens zu ermöglichen. Bei Clone wird nur die Struktur des DataSet dupliziert, also Tabellen, Beziehungen und Einschränkungen. Bei Copy hingegen werden Schema und Daten geklont, wodurch eine wirklich tiefe Kopie des Inhalts von DataSet angelegt wird.

In Beta 2 legen DataTable und DataRow das Methodenpaar Clone/Copy genauso wie DataSet offen.

Während meiner Arbeit an den Datentabellen einer typischen "getrennten Anwendung", wobei es sich unter .NET übrigens um die bevorzugte Art von Anwendung handelt, habe ich erkannt, dass eine Untertabelle erforderlich ist. Zuerst sah das Schreiben des Codes nicht sehr anspruchsvoll aus. Bei genauerem Hinsehen stellte es sich jedoch als große Herausforderung dar, die nicht ohne genetische Veränderungen am völlig ahnungslosen Versuchskaninchen DataTable zu meistern war - es geht um die arme Tabelle "Dolly".

Bis zum nächsten Mal!

Der Dialog: Wozu eignet sich "DataSet"?

Ich habe gesehen, dass in Teilen der gegenwärtigen ADO.NET-Dokumentation "DataSets" verwendet werden, um Daten aus einer Datenquelle abzurufen. Wenn ich lediglich eine Schleife durch eine Reihe von Datensätzen durchführen muss, um z.B. eine Tabelle zu erstellen, warum wird dann nicht einfach ein einfacherer und schnellerer "DataReader" verwendet?

Wenn ich Datensätze über mehrere Seitenanforderungen hinweg verwalten muss, wie kann ich dies mit einem "DataSet"-Objekt erreichen, ohne es zuerst unter "Session" oder "Application" zu speichern?

Wann soll ich was verwenden? Welche Vorteile hat "DataSet" für mich? Und was ist mit "DataSet" nicht möglich?

Bei DataReader und DataSet handelt es sich um ziemlich verschiedene Arten von Objekten. Ersteres ist verbindungsfähig und ermöglicht es Ihnen, eine Schleife auf vorwärtsgerichtete, schreibgeschützte Weise durchzuführen. Anders gesagt, DataReader ist eigentlich der folgende Codeauszug in Form eines Objekts.

While Not rs.EOF 
   ' Aktion ausführen 
   rs.MoveNext 
Wend

Wenn Sie Datensätze nicht hinzufügen, löschen oder ändern müssen und auch nicht hin- und herspringen möchten, warum sollten Sie für solch eine leichte Aufgabe dann so schweres Geschütz auffahren? Dafür gibt es DataReader. Und für solch eine Aufgabe ist DataSet nicht das geeignete Objekt.

DataSets sind speicherinterne Datencontainer, die als Behälter für Daten und Regeln fungieren. Sie können diesen Behälter als serverseitigen Cache zwischen dem Client und der Datenquelle im Speicher belassen. Den Inhalt können Sie auf einfache Weise für XML serialisieren und an eine Datenträgerdatei oder per HTTP an einen verbundenen Client senden. Wichtig ist dabei, dass Sie sich nicht um die Zielplattform und die Unternehmensfirewalls entlang des Weges kümmern müssen.

Bei WinForm-Anwendungen und Webdiensten ist DataSet das Schlüsselobjekt, das zwischen der Middle-tier-Anwendung und der Clientanwendung übergeben wird. Es geht bei DataSet also hauptsächlich um das Speichern von Daten, die mehr als eine Anfrage unbeschadet überstehen. Das Speichern unter "Session" oder "Application" ist also ganz normal.

Wenn Sie nur innerhalb einer Anforderung auf Daten zugreifen, sie lesen und verwenden möchten, ist DataSet u.U. nicht das beste Werkzeug. Andererseits ist DataReader ein vorwärtsgerichteter Cursor mit Schreibschutz. Wenn Sie also noch weitere Funktionen benötigen, ist DataSet das zu empfehlende Objekt. Am besten eignet sich das Objekt jedoch zum Speichern von Daten, die den Sitzungsumfang betreffen.


Anzeigen: