Laden...

Kopie ohne ICloneable [oder warum man Objekte nicht kopieren sollte; Transaktionen auf Objekten]

Erstellt von Quallo vor 18 Jahren Letzter Beitrag vor 16 Jahren 50.662 Views
Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren
Kopie ohne ICloneable [oder warum man Objekte nicht kopieren sollte; Transaktionen auf Objekten]

Ist es möglich auch ohne manuelle Implementierung(z.B. via IClonable) eine Kopie eines Objektes zu erstellen? inklusive referenzierter Objekte?
Kann man das via binärer Serialisierung in einen MemoryStream und zurück lösen?
Oder ist das zu langsam bzw. hat irgendwelche anderen Macken?

Grüße Christoph

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

also erstmal ist Klonen vermutlich nicht das was du willst, was daran liegt, das Klonen fast nie das ist, was man will.

Es gibt keinen mir bekannten Grund, warum man Objekte, die es in der Realität nur einmal gibt, im Programm mehrmals haben sollte. Im Gegenteil! Was es in der Realität nur einmal gibt, sollte es im Programm ebenfalls nur einmal geben.

Sicher gibt es Objekte mit Wert-Semantik. Aber diese werden ja schon bei einer normlen Zuweisung "geklont". Dafür brauch man Klonen normalerweise also auch nicht.

Warum meinst du denn Objekte klonen zu müssen?

Aber abstrahieren wir mal von dem Sinn. Dann kann man mit Object.MemberwiseClone schonmal eine flache Kopie eines Objekts erzeugen.

Die Serialisierung ist (bis auf [NonSerialized]) keine schlechte, aber eine langsame (wenn auch vermutlich akzeptabele) Möglichkeit. Allerdings solltest du auch in diesem Fall ICloneable (eben über Serializierung) implementieren.

herbivore

PS: Obwohl ich die obige Aussage gerne so absolut hätte stehen lassen wollen, sind mir doch noch Zwitter eingefallen, also Objekte mit Wertsemantik (sprich: ohne eigene Identität), die aber nicht als struct implementiert sind, nämlich ArrayList & Co.

308 Beiträge seit 2005
vor 18 Jahren

hallo herbivore,

ein grund für das klonen kann sein, das man den status eines Objektes festhalten will.

Oder wenn man eine Kopie für Worker-Threads erstellen will, die jeder autarg das Objekt in seinem Kontext manipulieren darf, ohne das die Kopien der anderen Threads verändert werden dürfen.

Beispiel ist eine workflow, der asynchrone sub-workflows startet die alle eine Kopie des Root-Workflow-Kontextes bekommen (lokale Variablen, aktueller Ausführungsschritt etc... )

Also es gibt durchaus Anwendungen für das Klonen!

Gruß,
cadi

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo cadi,

die von dir beschriebenen Situationen waren mir bewußt. Allerdings halte ich auch hier Klonen nicht für das geeignete Mittel. Beim Klonen wird ja nicht nur der Zustand, sondern auch die Identität gedoppelt (oder härter: zerstört). Sprich der normale Referenzsematik-Vergleichoperator '==' liefert für das Objekt und seinen Klon nicht mehr true. Das finde ich nicht wünschenswert.

Wenn man in eine relationale Datenbank schreibt, muss man sich auch nicht darum kümmern, das mehrere Prozesse/Threads die gleiche Zeile (=Objekt) schreiben wollen und man muss sich auch nicht darum kömmern, wenn Änderungen doch nicht durchgeführt werden sollen. Jedenfalls sollte man sich darum nicht kümmern müssen. Für den ersten Fall gibt es den Isolation Level und für den zweiten Fall Rollback bzw. Commit. Oder kurz es gibt einen Transaktionsmechanismus.

Wenn man also mit verschiedenen Zuständen des gleichen Objekts abeiten will oder muss, braucht man m.E. einen Transaktionsmechanismus; braucht und sollte aber keine Klone verwenden!

herbivore

308 Beiträge seit 2005
vor 18 Jahren

Die identität spielt aber in meinem Beispiel keine Rolle mehr.

Es ist den Workerthreads eh nicht erlaubt auf den context eines anderen threads zuzugreifen. Sollen und brauchen sie auch nicht. Sie sollen mit einer lokalen Kopie arbeiten.
Im Prinzip ist der geklonte original Kontext eher ein Template für die geklonten Kontexte.

Ich bin zumindest sher zufrieden mit meinen geklonten objekten.

(btw. ich habe noch transaktionelle globale Variablen, die von allen Worker-Threads erreichbar sind. Durch das Synchonisieren geht aber Perfomance verloren. Daher die kopien der lokalen variablen.)

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo cadi,

Die identität spielt aber in meinem Beispiel keine Rolle mehr.

Ich finde der Identität der Objekte wird leider viel zu wenig Beachtung geschenkt. Für mich ist die Identität eines Objekts genauso wichtig wie sein Zustand und sein Verhalten. Für mich sind Zustand, Verhalten und Identität so was wie die "Dreieinigkeit" der Objektorientierung. Ein Objekt setzt sich für mich also aus diesen drei Teilen gleichberechtigt zusammen.

Wenn man mehrere Zustände eines Objekts braucht, braucht man also nur Kopien eines der drei Teile des Objekts, eben des Zustandes. Wenn man also nur 1/3 eines Objekts mehrfach braucht, warum sollte man dann 2/3 oder gar 3/3 des Objekts kopieren?

Am schönsten wäre, wenn C# bzw. .NET dieses Modell direkt unterstützen würden. Also wenn man Isolation Level und Transaktionen direkt auf Objektebene zur Verfügung hätte. Solange das nicht so ist, kopiere ich eben den Zustand des Objekts innerhalb des Objekts statt das Objekt selbst. Und da man - so oder so - was implementieren muss (entweder das Kopieren des Zustands oder das Kopieren des Objekts), ist es auch nicht aufwändiger nur den Zustand zu kopieren.

Es ist den Workerthreads eh nicht erlaubt auf den context eines anderen threads zuzugreifen.

Warum denn das nicht? Das ist doch gerade der große Vorteil von Threads gegenüber Prozessen.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Danke für eure Antworten!

Was heisst, dass die Identität zerstört wird?

Ich brauche das, da ich einen Einstellungsdialog habe, bei dem die Änderungen nicht sofort auf das eigentliche Objekt durchschlagen sollen und dürfen, sondern erst nach manueller Bestätigung übernommen werden dürfen.

Wie sieht es aus, wenn ich ein array von Objekten oder eine ArrayList habe? Werden dann auch flache Kopien davon erstellt?
Ich denke, dass ich das über IClonable implementieren muss, oder?

Grüße Christoph

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

Was heisst, dass die Identität zerstört wird?

Im Prinzip geht es hier um die Frage, ob etwas dasselbe oder das gleiche ist. Wenn man eine genaue Kopie eines Objekts (z.B. Versicherter) herstellt, ist die Kopie eben ein neues Objekt und nicht mehr dasselbe. Die beiden Objekte sind also nicht identisch. Was mal aber eigentlich will, ist aber nur von demselben Objekt mehrere Zustände zu verwalten.

Ich brauche das, da ich einen Einstellungsdialog habe, bei dem die Änderungen nicht sofort auf das eigentliche Objekt durchschlagen sollen und dürfen, sondern erst nach manueller Bestätigung übernommen werden dürfen.

Einstellungen klingen nach Werten, also nach struct und nicht class.

Ansonsten ist das mit dem Durchschlagen genau das, was ich mit dem Isolation Level und dem Transaktionsmechanismus gemeint habe - den es aber leider (noch) nicht gibt.

Wie sieht es aus, wenn ich ein array von Objekten oder eine ArrayList habe?

Was Clone bei Array oder ArrayList macht, hängt davon ab, ob die enthaltenen Objekte Wert- oder Referenzsemantik haben. Im ersten Fall werden die Objekte quasi mit geklont, im zweiten Fall nicht.

Ich denke, dass ich das über IClonable implementieren muss, oder?

ICloneable ist besser als die Objekte "von Hand" zu kopieren. Aber bei ICloneable wird immer noch das Objekt kopiert, was schlecht ist, wenn es sich um Objekte mit Identität handelt. Am besten finde ich es, wenn solche Objekte selbst mehrere Zustände verwalten können.

Wenn es jedoch um Objekt ohne Identität geht, kann man diese natürlich auch klonen, wenn das nicht - wie bei structs - ohnehin bei jeder Zuweisung passiert.

herbivore

308 Beiträge seit 2005
vor 18 Jahren

Hallo Herbivore,

Zur identität:
Ok, sie spielt nicht keine Rolle mehr, sonder im gegenteil! Es soll explizit eine Kopie genutzt werden.

Zitat:
Es ist den Workerthreads eh nicht erlaubt auf den context eines anderen threads zuzugreifen.

Warum denn das nicht? Das ist doch gerade der große Vorteil von Threads gegenüber Prozessen.

Naja... manchmal braucht man Dinge halt nicht. Warum muss ein asynchroner Prozess wissen, was ein anderer Prozess gerade macht?

Der Witz ist ja gerade, das die völlig losgelöst von einander arbeiten. Eben nur mit einer gemeinsamen Ausgangsbasis.

Glaub es oder nicht, es macht Sinn....

/cadi

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo cadi,

Glaub es oder nicht, es macht Sinn....

Das was du als letztes geschrieben hast, glaube ich dir ja. Insbesondere (wie du schreibst) Prozesse können schön unabhängig nebeneinanderher arbeiten.

Bei Threads (und darum ging ja die Frage) wird zumindest der Start automatisch synchronisiert (der Thread läuft erst los, wenn man ihn explizit startet) und das Ende sollte synchronisiert werden. Ich will auch zugestehen, dass es Threads geben kann, die ohne weitere Synchronisation nebeneinanderher arbeiten - aber der Normalfall wird das m.E. nicht sein.

Was aber noch viel wichtiger ist: Du hast geschrieben, dass es "den Workerthreads eh nicht erlaubt auf den context eines anderen threads zuzugreifen". Und das stimmt nun nicht. Darauf bezog sich meine Nachfrage.

herbivore

P
939 Beiträge seit 2003
vor 18 Jahren

Hi Quallo,

ist der richtige Weg, mach es so.

Beim Serialisieren per Serializable-Attribut wird eine tiefe Kopie erstellt. Es sollte problemlos möglich sein, ein ICloneable-Interface für das Einstellungs-Objekt per Serialisierung zu implementieren, ist nicht zu langsam und hat auch keine Macken. Mit Arrays und ArrayList gibt es auch keine Schwierigkeiten, die implementieren Serializable. Auch deren Elemente werden tief kopiert.

D.h. du kannst im Einstellungsdialog eine Schattenkopie der Einstellungen erzeugen und vom Benutzer editieren lassen. Klickt er im Anschluss "Übernehmen", werden die geänderten Einstellungen übernommen, klickt er "Abbrechen", werden die alten Werte beibehalten. So war es gedacht oder?

Du schreibst im Startpost, dass es ohne ICloneable gehen soll. Geht natürlich auch. Dann muss eine andere Klasse die Kopie erstellen. Muss man sehen wo die Funktionalität am besten aufgehoben ist. Je nach Fall kann es sinnvoll sein, Operationen auf einer Klasse ausserhalb der Klasse zu implementieren.

Gruss
Pulpapex

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Danke für die Ermutigung. Dann werde ich das wohl per binärer Serialisierung machen, da denke ich, dass ich am wenigsten Probleme bekommen werde! Oder ist eine andere Methode schneller? Ich denke mal nicht, oder?

Grüße Christoph

21 Beiträge seit 2004
vor 18 Jahren

Kann man auch ohne Serialisierung tief kopieren?
Ein Control z.B. ist ja nicht Serialisierbar und hat auch keine ICloneable implementiert.

308 Beiträge seit 2005
vor 18 Jahren

Hallo bayeror,

Im prinzip ja, hängt aber im einzelnen vom object ab, das kopiert werden soll.
Wenn es möglich ist, durch reines kopieren aller Felder den kompletten state sauber zu kopieren ann geht das per reflection.
Das funktioniert aber zum Beispiel nicht, wenn der Quelltyp keinen parameterlosen Konstruktor hat (woher soll ein Kopiermechanismus wiessen, welche Felder evtl. die Parameter des Konstruktors beinhalten?)

Gefährlich wird es auch bei objekten, die thread-spezifische Felder haben (z.B. der StringBuilder mit dem Feld m_currentThread. Ein Kopieren über Threadgrenzen kann da unvorhersehbare Folgen haben).

Du kannst ja mal mit dem folgenden Kode experimentieren.

Für produktiven Einsatz würde ich das aber nur unter erheblichen Vorbehalten empfehlen. Da musst du schon sehr genau wissen was du da machst....


using System;
using System.Reflection;

namespace CodeProject
{
	public class Cloner
	{
		public static object Clone(object source)
		{
			return Clone(source,false);	
		}
		public static object Clone(object source, bool useIClonableForFields)
		{
			object result = null;
			Type srcType = source.GetType();

			// create target
			// will throw an exception of no parameterless constructor is available
			result = srcType.Assembly.CreateInstance(srcType.FullName);

			// get a list of all instance fields
			FieldInfo[] fields = srcType.GetFields(BindingFlags.Instance | BindingFlags.Public |BindingFlags.NonPublic);

			// copy fields from source to result
			foreach(FieldInfo fieldInfo in fields)
			{
				object fieldValue = fieldInfo.GetValue(source);
				// clone value?
				if ( (fieldValue is ICloneable) && useIClonableForFields )
				{
					// set result's field value to cloned source value
					fieldInfo.SetValue(result,((ICloneable)fieldValue).Clone());
				}
				else
				{
					// set result's field value to source value
					fieldInfo.SetValue(result,fieldValue);
				}
				
			}
			return result;
		}
	}
}

21 Beiträge seit 2004
vor 18 Jahren

Dankschön!
Das dürfte das richtige sein.

4.221 Beiträge seit 2005
vor 18 Jahren
Hinweis von herbivore vor 10 Jahren

Der folgende Code ist eine (in Rekordzeit erstellte) Skizze. Eine etwas weiter ausgearbeitete Variante findet sich weiter unten.

Bei einer interessanten Diskussion mit herbivore sind wir auf das folgende Pattern gekommen, welches die volle Idendität erhält.

Das Interface für jede Datenklasse


using System;

namespace RollbackSample
{
	public interface IRollback
	{
		void StartTransaction();
		void CommitTransaction();
		void RollbackTransaction();
	}
}

Die Standard-Implementierung



using System;
using System.Collections;
using System.Reflection;

namespace RollbackSample
{
	public class RollbackBase : IRollback
	{
		private Hashtable _Properties;
		private Hashtable _IRollbacks;
		public RollbackBase()
		{
		}

		~RollbackBase()
		{
			if (this._Properties!=null)
			{
				this.RollbackTransaction();
			}
		}

		#region IRollback Members

		public void StartTransaction()
		{
			if (this._Properties!=null)
			{
				throw new Exception("Transaction pending");
			}

			this._Properties=new Hashtable();
			this._IRollbacks=new Hashtable();
			
			Type typ=this.GetType();
			PropertyInfo[] props=typ.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetProperty | System.Reflection.BindingFlags.Public);
			foreach (PropertyInfo prop in props)
			{
				if (prop.PropertyType.GetInterface("IRollback")==null)
				{
					//den Wert speichern wir mal
					this._Properties.Add(prop,prop.GetValue(this,null));
				}
				else
				{
					this._IRollbacks.Add(prop,prop.GetValue(this,null));
					//nächste Stufe ankicken
					IRollback ir=prop.GetValue(this,null)as IRollback;
					ir.StartTransaction();
				}
			}
		}
		
		public void CommitTransaction()
		{
			if (this._Properties==null)
			{
				throw new Exception("No Transaction started");
			}
			foreach (PropertyInfo prop in this._IRollbacks.Keys)
			{
				IRollback ir=(IRollback)this._IRollbacks[prop];
				ir.CommitTransaction();
			}
			this._Properties=null;
			this._IRollbacks=null;
		}

		public void RollbackTransaction()
		{
			if (this._Properties==null)
			{
				throw new Exception("No Transaction started");
			}
			foreach (PropertyInfo prop in this._Properties.Keys)
			{
				prop.SetValue(this,this._Properties[prop],null);
			}
			foreach (PropertyInfo prop in this._IRollbacks.Keys)
			{
				IRollback ir=(IRollback)this._IRollbacks[prop];
				ir.RollbackTransaction();
			}
			this._Properties=null;
			this._IRollbacks=null;
		}
		#endregion
	}
}



Eine Datenklasse:



using System;

namespace RollbackSample
{
	public class Beispiel : RollbackBase
	{
		private string _Name;
		private string _Vorname;
		private BeispielKlein _BeispielKlein=new BeispielKlein();

		public Beispiel()
		{
		}

		public string Name
		{
			get{return this._Name;}
			set{this._Name=value;}
		}

		public string Vorname
		{
			get{return this._Vorname;}
			set{this._Vorname=value;}
		}

		public BeispielKlein BeispielKlein
		{
			get{return this._BeispielKlein;}
		}
		
	}
}


Eine ChildDaten-Klasse



using System;

namespace RollbackSample
{
	public class BeispielKlein : RollbackBase
	{
		private string _Hobby;
		public BeispielKlein()
		{
		}
		public string Hobby
		{
			get{return this._Hobby;}
			set{this._Hobby=value;}
		}
	}
}


und noch ein wenig TestCode:



Beispiel bsp=new Beispiel();
			bsp.Name="Meier";
			bsp.Vorname="Hans";
			bsp.BeispielKlein.Hobby="A";
			System.Diagnostics.Debug.WriteLine(string.Concat(bsp.Name, " " ,bsp.Vorname, " ",bsp.BeispielKlein.Hobby));
			bsp.StartTransaction();
			bsp.Name="Müller";
			bsp.Vorname="Hugo";
			bsp.BeispielKlein.Hobby="B";
			System.Diagnostics.Debug.WriteLine(string.Concat(bsp.Name, " " ,bsp.Vorname, " ",bsp.BeispielKlein.Hobby));
			bsp.RollbackTransaction();
			System.Diagnostics.Debug.WriteLine(string.Concat(bsp.Name, " " ,bsp.Vorname, " ",bsp.BeispielKlein.Hobby));
			


Was meint ihr dazu ?

Programmierhans

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

1.373 Beiträge seit 2004
vor 18 Jahren

Gefällt mir! Vor allem, dass die Benutzung so transparent ist. Man muss nur vor unendlicher Rekursion aufpassen, wenn im Objektgraph ein Untergeordnetes Objekt eine Referenz auf ein Übergeordnetes Objekt enthält. Entweder man führt hinzu eine Liste, welche Objekte bereits StartTransaction empfangen haben, oder man geht schaut nach, ob _Properties schon != null. Wobei bei letzterem natürlich nicht mehr zu unterscheiden ist, ob eine Transaktion mehrfach gestartet worden ist, oder ob das gleiche Objekt bereits einen StartTransaction-Auftrag erhalten hat.

MfG VizOne

4.221 Beiträge seit 2005
vor 18 Jahren

Genau um so Spezialfälle abzudecken ist alles gegen Interface programmiert 🙂

Denkbar wäre auch eigene Attribute zu verwenden so ala [ISupportRollback] oder besser eine verneinende Form welche auf denjenigen Members angebracht wird, welche nicht berührt werden sollen...

Eine prüfung ob this == dem Property des Childs entspricht würde direkte Child-Parent-Beziehungen verschonen...

Wege gibt es viele

Man kann IRollback auch noch mit einer TransactionID (vorschlag Type Guid) versehen und jedes SubObjekt über das Interface auf eine allfällig laufende Transaction (ID) überprüfen usw....

Es ist alles offen.

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo zusammen,

in dem o.g. Gespräch zwischen Programmierhans und mir, hatte ich ihn gefragt "kennst du eigentlich eine möglichkeit den zustand eines objekt zu sichern und dann in dasselbe objekt zurückzuspielen?" und das noch präzisiert mit "also was ich letztendlich will, sind transaktionen auf objektebene. man sagt sowas wie start transaction, dann ändert man lustig und sagt am schluss commit oder rollback. aber eben nicht für die datenbank, sondern für die objekte im speicher".

Was er daraufhin vorschlug, deckte sich ziemlich genau mit dem, was ich schon überlegt hatte - nur hatte ich gehofft, dass es noch was besseres gäbe. Naja, nachdem wir keinen besseren Ansatz gefunden haben, hat Programmierhans in nur 37 Minuten den obigen Code erstellt.

Respekt!

Das bei einer so kurzen Entwicklungszeit noch nicht alles berücksichtigt werden/sein kann, ist klar. So gibt es denn auch folgenden Probleme mit dem Code:

Es werden nur die öffentlichen Eigenschaften gesichert. Dabei klappt das Zurückspielen nicht, wenn eine Eigenschaft nur einen Getter hat oder läuft schräg, wenn Getter und Setter "asymmetrisch" sind, wie bei:


public int Murx 
{ 
   get { return _iMurx; } 
   set { _iMurx = value + 1; } 
}

Außerdem wird dadurch nur ein Teil des Zustandes des Objekts gesichert, z.B. werden Get- Und Set-Methoden nicht erfasst, da diese keine Eigenschaften sind.

Das alles kann man lösen, indem man alle Felder und nicht die Eigenschaften sichert.

Ich habe leider erst jetzt Zeit gefunden, meine Sicht der Dinge in Code zu gießen. Diese enthält zusätzlich noch folgende Verbesserungen:

Bei rekursiven Strukturen kommt es nicht mehr zu einer Exception. Eine Exception kommt aber weiterhin, wenn ein Objekt schon in einer anderen Transaktion enthalten ist. Zum erkennen, zu welcher Transaktion ein Objekt gehört, habe ich Programmierhans Vorschlag einer Guid aufgegriffen. Darüberhinaus wird sichergestellt, dass eine Transaktion für das Objekt beendet wird/werden muss, für dass sie auch begonnen wurde (Wurzelobjekt). Man kann also keine Unter- oder Teilstrukturen mehr committen.

Ein Umstand ist Programmierhans so richtig durch die Lappen gegangen: Man kann ja nicht nur den Zustand eines Business-Unterobjekts verändert, sondern es auch ganz austauschen (Objekt B statt Objekt A eingetragen). Zum einen wurde dann bei einem Rollback das Objekt nicht zurückgetauscht (wieder A eingetragen), zum anderen wurde A selbst gar nicht zurückgesetzt. Außerdem kam es zu einer Exception wenn bzw. weil Bs _Properties-Eigenschaft null ist. Analog für Commit.

All dies ist behoben, indem alle Objekte in einer Transaktion in der Liste _listScope gespeichert werden. Dadurch fällt auch das ausgetauschte Objekt A nicht versehentlich dem CG in die Hände.

Zu guter Letzt sind auch die Probleme, die der Destruktor verursachen konnte, behoben, da jetzt das erste Objekt einer Transaktion, dass der GC bearbeitet zu einem Rollback der gesamten Transaktion (beginnend an der Wurzel) führt.

Eins konnte ich aber auch nicht lösen. Nur bei Feldern mit Werttypen (Int32) oder Readonly-Typen (z.B. String), wird ja echt der Zustand gesichert. Bei Referenztypen (außer BusinessObjects natürlich) wird nur gesichert, welches Objekt eingetragen war (und beim Rollback dieses Objekt wieder eingetragen). Wenn sich aber der Zustand dieses Objekts geändert hat, kann und wird dieser durch das Rollback nicht wiederhergestellt.

Programmierhans hielt nach dem Produkthaftungsgesetz folgenden Hinweis für erforderlich: Databinding ist in der BusinessObject-Klasse nicht berücksichtigt. 🙂

Hier kommt der Code (der Einfachheit halber in einem Rutsch). Auch ich würde mich über Kommentare und Rückmeldungen freuen.


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;

//*****************************************************************************
public abstract class BusinessObject
{
   //--------------------------------------------------------------------------

   // In welcher Transaktion das Objekt enthalten ist
   private Guid                           _guidTransaction = Guid.Empty;

   // Bei welchem Objekt die Transaktion begonnen wurde
   private BusinessObject                 _boRoot         = null;

   // Alle Objekte in der Transaktion; nur beim WurzelObjekt gesetzt
   private List <BusinessObject>          _listScope      = null;

   // Der gespeicherte Zustand des Objekts
   private Dictionary <FieldInfo, Object> _dictFields     = null;

   //==========================================================================
   public void StartTransaction ()
   {
      if (_listScope != null) {
         throw new Exception ("Keine geschachtelten Transaktionen erlaubt.");
      }

      _listScope = new List <BusinessObject> ();

      try {
         _StartTransaction (0, Guid.NewGuid (), this);
      }
      catch (Exception) {
         foreach (BusinessObject boCurr in _listScope) {
            boCurr._guidTransaction = Guid.Empty;
            boCurr._boRoot          = null;
            boCurr._dictFields      = null;
         }
         _listScope = null;
         throw;
      }
   }

   //==========================================================================
   private void _StartTransaction (int iLevel,
                                   Guid guidTransaction,
                                   BusinessObject boRoot)
   {
      if (_guidTransaction != Guid.Empty) {
         if (_guidTransaction == guidTransaction) {
            return; // zyklischen Struktur, Objekt schon besucht
         }
         throw new Exception ("Objekt ist schon in einer anderen "
                            + "Transaktion enthalten.");
      }

      boRoot._listScope.Add (this);
      _guidTransaction = guidTransaction; // als besucht kennzeichnen
      _boRoot = boRoot;
      _dictFields = new Dictionary <FieldInfo, Object> ();

      FieldInfo [] afi;
      List <FieldInfo> listfiAll = new List <FieldInfo> ();
      BusinessObject boChild;

      Type t;

      // Die Schleife ist notwending, weil man sonst nicht an die privaten
      // Felder der Oberklassen kommt.
      for (t = GetType (); t != typeof (BusinessObject); t = t.BaseType) {
         afi = t.GetFields (BindingFlags.GetField
                          | BindingFlags.Instance
                          | BindingFlags.NonPublic
                          | BindingFlags.DeclaredOnly
                          | BindingFlags.Public); // nur zur Sicherheit
         foreach (FieldInfo fi in afi) {
            #if SW_VERBOSE
            Console.WriteLine (new String (' ', 3 * iLevel)
                               + fi.DeclaringType + "." + fi.Name);
            #endif
            _dictFields [fi] = fi.GetValue (this);
         }
         listfiAll.AddRange (afi);
      }

      // Rekursiver Abstieg
      foreach (FieldInfo fi in listfiAll) {
         if (fi.FieldType.IsSubclassOf (typeof (BusinessObject))) {
            #if SW_VERBOSE
            Console.WriteLine ("=> " + new String (' ', 3 * iLevel)
                               + fi.DeclaringType  + "." + fi.Name);
            #endif
            boChild = (BusinessObject)fi.GetValue (this);
            if (boChild != null) {
               boChild._StartTransaction (iLevel + 1, guidTransaction, boRoot);
            }
         }
      }
   }

   //==========================================================================
   public void CommitTransaction ()
   {
      if (_guidTransaction == Guid.Empty) {
         throw new Exception ("Objekt ist in keiner Transaktion enthalten.");
      }

      if (_listScope == null) {
         throw new Exception ("Die Transaktion wurde nicht bei diesem "
                            + "Objekt begonnen");
      }

      foreach (BusinessObject boCurr in _listScope) {
         boCurr._guidTransaction = Guid.Empty;
         boCurr._boRoot          = null;
         boCurr._dictFields      = null;
      }
      _listScope = null;
   }

   //==========================================================================
   public void RollbackTransaction ()
   {
      if (_guidTransaction == Guid.Empty) {
         throw new Exception ("Objekt ist in keiner Transaktion enthalten.");
      }

      if (_listScope == null) {
         throw new Exception ("Die Transaktion wurde nicht bei diesem "
                            + "Objekt begonnen");
      }

      foreach (BusinessObject boCurr in _listScope) {
         #if SW_VERBOSE
         Console.WriteLine (boCurr.GetType ());
         #endif
         foreach (FieldInfo fi in boCurr._dictFields.Keys) {
            #if SW_VERBOSE
            Console.WriteLine ("..."  + fi.DeclaringType + "." + fi.Name);
            #endif
            fi.SetValue (boCurr, boCurr._dictFields [fi]);
         }
         boCurr._guidTransaction = Guid.Empty;
         boCurr._boRoot          = null;
         boCurr._dictFields      = null;
      }
      _listScope = null;
   }

   //==========================================================================
   ~BusinessObject ()
   {
      if (_listScope != null) {
         RollbackTransaction ();
      } else if (_boRoot != null) {
         _boRoot.RollbackTransaction ();
      }
   }
}

//*****************************************************************************
public class Beispiel : BusinessObject
{
   private   string         _strName     = "";
   protected string         _strVorname  = "";
   public    string         _strTitel    = ""; // zu Testzwecken public
   private   DateTime       _dtGebDat;
//   private   readonly int   _i           = 5;
   private   Beispiel       _bspZyklisch = null;
   private   BeispielKlein  _bspklNull   = null;
   private   BeispielKlein  _bspklStufe1 = new BeispielKlein ();
   private   BeispielGeerbt _bspklStufe2 = new BeispielGeerbt ();

   //==========================================================================
   public Beispiel ()
   {
      _bspZyklisch = this;
   }

   //==========================================================================
   public String Name
   {
      get { return _strName; }
      set { _strName = value; }
   }

   //==========================================================================
   public String Vorname
   {
      get { return _strVorname; }
      set { _strVorname = value; }
   }

   //==========================================================================
   public String Titel
   {
      get { return _strTitel; }
      set { _strTitel = value; }
   }

   //==========================================================================
   public DateTime GebDat
   {
      get { return _dtGebDat; }
      set { _dtGebDat = value; }
   }

   //==========================================================================
   public BeispielKlein BeispielKlein
   {
      get { return _bspklStufe1; }
      set { _bspklStufe1 = value; }
   }

   //==========================================================================
   public BeispielGeerbt BeispielGeerbt
   {
      get { return _bspklStufe2; }
      set { _bspklStufe2 = value; }
   }

   //==========================================================================
   public override String ToString ()
   {
      return "{ "
            + String.Join (" | ", new String [] { _strName,
                                                  _strVorname,
                                                  _strTitel,
                                                  _dtGebDat.ToString () } )
            + Environment.NewLine + "   "
            + (_bspZyklisch == null ? "<null>" : (_bspZyklisch == this
                                                ? "<this>"
                                                : _bspZyklisch.ToString ()))
            + Environment.NewLine + "   "
            + (_bspklNull == null ? "<null>" : _bspklNull.ToString ())
            + Environment.NewLine + "   "
            + (_bspklStufe1 == null ? "<null>" : _bspklStufe1.ToString ())
            + Environment.NewLine + "   "
            + (_bspklStufe1 == null ? "<null>" : _bspklStufe2.ToString ())
            + Environment.NewLine
            + "}";
   }
}


//*****************************************************************************
public class BeispielKlein : BusinessObject
{
   private   string _strPrivateHobby = "";
   protected string _strProtectedHobby = "";

   //==========================================================================
   public String PrivateHobby
   {
      get { return _strPrivateHobby; }
      set { _strPrivateHobby = value; }
   }

   //==========================================================================
   public String ProtectedHobby
   {
      get { return _strProtectedHobby; }
      set { _strProtectedHobby = value; }
   }

   //==========================================================================
   public override String ToString ()
   {
      return "{ "
            + String.Join (" | ", new String [] { _strPrivateHobby,
                                                  _strProtectedHobby } )
            + "}";
   }
}

//*****************************************************************************
public class BeispielGeerbt : BeispielKlein
{
   private String _strSport;

   //==========================================================================
   public String Sport
   {
      get { return _strSport; }
      set { _strSport = value; }
   }

   //==========================================================================
   public override String ToString ()
   {
      return "{ "
            + String.Join (" | ", new String [] { base.ToString (),
                                                  _strSport } )
            + "}";
   }
}

//*****************************************************************************
abstract class App
{
   public static void Main (string [] astrArg)
   {
      Beispiel bsp = new Beispiel();
      bsp.Name = "Meier";
      bsp.Vorname = "Hans";
      bsp.GebDat  =  DateTime.Parse ("12.12.1912");
      bsp.BeispielKlein.PrivateHobby = "priv A";
      bsp.BeispielKlein.ProtectedHobby = "prot A";
      bsp.BeispielGeerbt.PrivateHobby = "priv AA";
      bsp.BeispielGeerbt.ProtectedHobby = "prot AA";
      bsp.BeispielGeerbt.Sport = "AAA";

      Console.WriteLine (bsp);

      bsp.StartTransaction();
      bsp.Name = "Müller";
      bsp.Vorname = "Hugo";
      bsp.BeispielKlein.PrivateHobby = "priv B";
      bsp.BeispielKlein.ProtectedHobby = "prot B";
      bsp.BeispielGeerbt.PrivateHobby = "priv BB";
      bsp.BeispielGeerbt.ProtectedHobby = "prot BB";
      bsp.BeispielGeerbt.Sport = "BBB";
      bsp.BeispielKlein = new BeispielKlein ();
      bsp.BeispielKlein.PrivateHobby = "priv C";
      bsp.BeispielKlein.ProtectedHobby = "prot C";

      Console.WriteLine (bsp);
      bsp.RollbackTransaction();

      Console.WriteLine (bsp);

      Beispiel bsp2 = new Beispiel();
      bsp2.StartTransaction();
   }
}

herbivore

PS: In "Zeiger" oder Kopie gibt es eine Variante von codester, die auch Collections berücksichtigt/behandelt.

4.221 Beiträge seit 2005
vor 18 Jahren

@herbivore

Was mir jetzt im Nachhinein erst auffällt ist, dass Du leider das Interface rausgenommen hast.... so ist man gezwungen von Deiner Klasse abzuleiten. Da die Methoden aber nicht virtual sind wird's übel wenn man noch aus welchen Gründen auch immer was dazwischenklemmen muss.

Allenfalls könnte man den rekursiven Abstieg vorziehen... so kannst Du schneller erkennen wenn ein SubObjekt nicht gesperrt werden kann.... Da ja dann eh die ganze Transaktion nichtig ist, müsstest Du nirgendwo Daten einlesen...

Je länger man sich mit der Materie beschäftigt, desto mehr Details fallen halt auf.... aber dies ist ja der Grund wieso man über den Code diskutiert... jeder sieht es aus seiner Perspektive... und mehr Augen sehen nun mal mehr.

Gruss
Programmierhans

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Programmierhans,

das mit dem Interface ist Absicht. Habe ich vergessen zu schreiben, obwohl mir das wichtig war. Ich glaube das gäbe Murx, wenn verschiedene Implementierungen von StartTransaction in einer Transaktion zusammnentreffen.

Trotzdem ich Fan von Vererbung bin, habe ich die Exemplarvariablen von BusinessObject absichtlich private gemacht. Aus dem gleichen Grund, um inkompatible Implementierungen zu verhinden. Daher ist es auch passend, wenn die Methoden von BusinessObject nicht virtual sind (auch wenn ich das nur zufällig so gemacht habe).

Aber vielleicht überzeugt mich auch jemand vom Gegenteil!

Das mit dem Vorziehen des rekursiven Abstiegs ist eine gute Idee. Ich hatte das Speichern der Felder und den rekursiven Abstieg schon extra getrennt, obwohl es ja einfacher gewesen wäre das zu mischen. Aber auf die Idee den Abstieg vorzuziehe bin ich nicht gekommen. Super!

herbivore

4.207 Beiträge seit 2003
vor 18 Jahren

Hi,

nach einem sehr informativen und ergiebigen Telefonat mit Herbivore habe ich noch folgende Anregung:

Es wäre praktisch, wenn man mehrere Root-BOs in eine Transaktion zusammen fassen könnte ... dies wäre quasi eine Art Transaktionshandler, der die einzelnen Root-BOs verwaltet.

Vorstellen würde ich mir also, dass zunächst am Transaktionshandler eine globale Transaktion gestartet wird, an diesem dann die einzelnen Root-BOs registriert werden, die an der Transaktion teilnehmen sollen, was einen automatischen Start deren Transaktion zur Folge hat.

Setzt man dann auf dem Transaktionshandler ein Commit oder ein Rollback ab, so muss dieser lediglich hingehen, und auf allen registrierten Objekten die entsprechende Methode auslösen. Zu beachten ist dabei allerdings, dass der Handler von außen eine GUID vorgibt, damit alle Objekte in der Transaktion als Objekte derselben Transaktion erkannt werden.

Frage wäre hier, ob es Sinn ergibt, mehrere Transaktionen pro Handler zu ermöglichen (IMHO nein). Falls nämlich nicht, könnte dieser als Singleton implementiert werden.

Any further comments?

Viele Grüße,

der Eisbär

Wissensvermittler und Technologieberater
für .NET, Codequalität und agile Methoden

www.goloroden.de
www.des-eisbaeren-blog.de

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Der Eisbär,

den Vorschlag, mehrere Objekte in eine Transaktion zu packen, finde ich sehr sinnvoll. Vielen Dank dafür.

Ich habe mal eine Variante implementiert, die ohne einen extra Transaktionshandler auskommt und auch sonst minimalinvasiv ist. Man kann einfach weitere Objekte über das Wurzelobjekt einer Transaktion zu eben dieser Transaktion hinzufügen, also z.B.:


bsp.StartTransaction ();
bsp.AddToTransaction (bsp2);

Es ist schon spät und ich hoffe ich habe keine Fehler eingebaut, aber m.E. müsste es reichen zu dem Code von oben, AddToTransaction mit folgender Implementierung hinzuzufügen:


public void AddToTransaction (BusinessObject boToAdd)
{
   if (_listScope == null) {
      throw new Exception ("Objekte können nur über das Wurzelobjekt "
                         + "einer Transaktion hinzugefügt werden.");
   }

   int iListScopeCount = _listScope.Count;

   try {
      boToAdd._StartTransaction (1, _guidTransaction, this);
   }
   catch (Exception) {
      BusinessObject boCurr;
      for (int i = _listScope.Count - 1; i >= iListScopeCount; --i) {
         boCurr = _listScope [i];
         boCurr._guidTransaction = Guid.Empty;
         boCurr._boRoot          = null;
         boCurr._dictFields      = null;
         _listScope.RemoveAt (i);
      }
      throw;
   }
}

Du kannst ja mal schreiben, ob dir das besser, auch oder gar nicht gefällt.

herbivore

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Der Eisbär,

wie wir gestern Abend noch besprochen haben, kommt man mit AddToTransaction auch billig zu dem von dir gewünschten Transaktionshandler. Dazu implementiert man einfach eine leere Klasse TransactionsHandler, die von BusinessObject erbt, und startet Transaktionen per Konvention nur über Objekte dieser Klasse. Ob man den Transaktionshandler als komlett leere Klasse oder als Singleton implementiert, kann man sich natürlich auch aussuchen.

Die Guid von außen ist m.E. gar nicht nötig. Das Transaktionshandler-Objekt bekommt ja bei StartTransaction eine Guid verpasst. Wenn weitere Objekte mit AddToTransaction hinzugefügt werden, wird diese Guid an sie weitergegeben.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Wäre es nicht praktischer, das Ganze etwas anders aufzuziehen?

Ich finde es schwach, wenn ich extra ein Interface implementieren muss oder von der Klasse ableiten muss.
Das hemmt die Benutzung und die Akzeptanz eines solchen Mechanismus.
Ich würde das viel lieber als eine Liste haben, etwa so:


TransActionList<T> : IList
{
    //Hier kann ich Objekte hinzufügen per Add oder Addrange(Intern kann ja einfach eine List<T> benutzt werden).

   public void StartTransaction()

   public void Commit();

   public void Rollback();
}

In diese Liste kann ich dann beliebige Objekte hineinpacken und Transactionen darauf anwenden.

Wenn ich etwas aus der Liste heraushole, bekomme ich eine flache Kopie mit dem aktuellen Stand eines Objekts.

Meine Vorschläge hier sind sicherlich noch nicht ausgereift, aber zwei Kernaussagen habe ich:

  1. Das soll auf jedes beliebige Objekt anwendbar sein!
  2. Das soll auf einem allgemeingültigen Standard(wie zum Beispiel IList) aufbauen.

Beides zusammen bedeutet super Usability(geiles Marketinggeblubber, was? g) und
eine allgemeine Anwendbarkeit!

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

ich denke, das Erben von BusinessObject ist bei Bussines-Objekten keine Einschränkung. Bei meinen größeren Anwendungen gibt es ohnehin so eine Oberklasse. Dann kann sie auch das Transactionhandling übernehmen.

Allerdings können durch den bisherigen Ansatz nur Business-Objekte "transaktioniert" werden. Das ist schon eher der Nachteil, für den dein Vorschlag eine Lösung aufzeigt.

Ich habe darauf hin mal in diese Richtung überlegt. Das zeigt sich z.B. durch meine Anfrage Technische Identität (Adresse) ermitten? Denn wenn man sich den Zustand nicht in dem Objekt merkt (_dictFields), muss man ihn sich zu dem Objekt merken und das geht m.E. nur über seine (technische) Identität. Leider bin ich momentan noch nicht weiter gekommen, weil ich mich mit einer merkwüdigen Exception rumärgere und noch nicht die Zeit und Ruhe hatte, sie zu knacken. Aber selbst ohne die Exception muss man ziemlich weit runter auf GC-Ebene und das gefällt mir nicht.

Wenn ich etwas aus der Liste heraushole, bekomme ich eine flache Kopie mit dem aktuellen Stand eines Objekts.

Na das nun gerade nicht! Dann kann man auch klonen. Mir geht es ja gerade darum Kopien, von Objekten zu vermeiden und nur den Zustand zu kopieren. Außerdem hat man ja nicht gewonnen, wenn man den aktuellen Stand rausholt. Den aktuellen Stand hat man ja eh im Objekt. Falls du den alten Stand meinst, hat das wiederum nichts mit Transaktionen zu tun, weil es bei denen nicht darum geht, den alten Stand abzurufen sondern wiederherzustellen.

Dein Vorschlag mach m.E. dann Sinn, wenn TransActionList<T> eine write-only-Liste ist oder wenn man beim Lesen die Originalobjekte (zwansgläufig im aktuellen Stand) bekommt (also keine Kopien - auf keinen Fall - Hände über dem Kopf zusammenschlag 🙂

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Wieso soll das nur über die technische Identität gehen?
Ich kann mir doch einfach eine Referenz auf das Objekt speichern, auf das ich eine Transaktion anwenden will, oder wieso soll das nicht klappen?

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

naja, darum geht es ja. Sinnvollerweise speichert man diese Referenz als Key eines Dictionary, damit man darüber schnell an den zugehörigen (gesicherten) Zustand des Objekts kommt. Wenn man aber einfach schreibt:

dictZustand [obj] = ...

dann wird eben nicht die Referenz des Objekts als Key verwendet, sondern (vereinfacht und nicht ganz korrekt) sein Hashcode. Außerdem soll es ja für beliebige Objekte gehen und dann ist nicht mal sichergestellt, dass GetHashCode vernüftig implementiert ist. Tja, und so bin ich quasi auf der Suche nach etwas, was die Referenz des Objekts als Zahl liefert. In dem anderen Thread steht ja dazu mehr.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Brauch man wirklich ein Dictionary?
Das ist die Frage die man sich stellen sollte, falls man für das Problem keine Lösung findet!

Grüße Christoph

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

das habe ich schon überlegt.

Ich habe überlegt, ob man eine SortedList nehmen kann, aber da hat man das gleiche Problem, weil man dann nach Referenz sortieren können müsste (also einen größer-Vergleich bräuchte, man hat aber nur Gleichheit).

Was dann noch bleibt ist eine unsortierte Liste. Da hätte man kein Problem mit den Referenzen, aber dafür mit dem Aufwand, weil dieser dann statt linear quadratisch wird. Und das ist für eine allgemeingültige Lösung - wie du sie ja gerne hättest 😉 - m.E. nicht akzeptabel.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Muss man denn wirklich so eine Zuordnung haben?

Ich denke, dass man die nur braucht, wenn man eine Gesamtliste führt, welche Objekte in einer Transaction sind, damit man nicht rekursiv immer wieder reinpackt. kann man diese Liste nicht anders führen?

Grüße Christoph

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

natürlich braucht man diese Gesamtliste. Man braucht sie bei BusinessObjects (Begründung s.o.). Und man braucht sie hier, wobei es hier ja noch klarer ist, da man ja den Zustand nicht in dem Objekt merken kann, sondern das zu dem Objekt tun muss. Aber ich wiederhole mich. Das hat noch nicht mal was damit zu tun, ob man sich nur den Zustand der Objekte merken will, oder die Objekte innerhalb von TransActionList klonen würde.

Versuchs mal selbst zu implementieren. Ich denke, dass wirst du merken, wo es hakt.

Ich denke nicht, dass man die Liste anders führen kann, bin aber für konkrete Vorschläge offen.

herbivore

4.221 Beiträge seit 2005
vor 18 Jahren

@herbivore

Wenn das BusinessObject von einer BaseClass ableitet (was ja Deine Grundidee war welche Du auch verteidigt hast) dann würde sich doch folgendes anbieten:

die BaseClass erhält ein private Field mit einer Guid.... --> ID diese wird im Default-Constructor des BusinessObject's mit Guid.NewGuid angelegt.

Dann GetHashCode überschreiben, Guid returnen.... das war's

Gruss
Programmierhans

PS: Wieso kompliziert wenn's auch einfach geht.... allerdings halt nur bei einer gemeinsamen BaseClass

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

I
1.739 Beiträge seit 2005
vor 18 Jahren

Hallo,

mal als Erweiterungsvorschlag. Das Kopieren von Eventhandlern wäre sicherlich auch eine interessante Option(wenn auch wahrscheinlich selten gebraucht).

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Wenn es wirklich nur um die Zuordnung geht, dann reicht auch ein object[x,2] wo man ein object dem anderen zuordnet.
Das Problem ist da eher die Performance.
Und da man sowohl bei StartTransaction, als auch bei Commit und bei Rollback nur seriellen zugriff auf die wie auch immer gearteten Listen hat(man iteriert nur durch), sehe ich das mit einem Dictionary nicht unbedingt performanter.

Das Problem ist nur die Liste zu führen, welche Objekte man schon in einer Transaction drin hat, damit man keine endlos rekursionen hat. Da geht DictionaryInstanz.ContainsKey(irgendwas) natürlich wesentlich schneller als eine andere IList.Contains-Implementierung.
Ich verstehe deswegen auch, warum du gerne das Dictionary benutzen möchtest und die Adresse des Objects zum Hashen nehmen willst.
Das ist mir klar. Die Frage ist, ob es nicht eine andere Möglichkeit gibt. Wie sieht es mit einer BinarySearch aus?

Grüße, der wahrscheinlich deine Geduld sehr strapazierende, Christoph

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Programmierhans,

dein Vorschlag ist in der Tat bestechend einfach. Und es ist sicher sinnvoll, ihn umzusetzen, weil man dann Business-Objekte auch in anderen Fällen als Key in Hashtables/Dictionaries benutzen kann. Allerdings gibt es ja durch das Zusammenspiel unser beiden Vorschläge schon ein Proof-of-Concept für die Transaktionierung von Business-Objekten. Für diese stellt sich das Problem m.E. also gar nicht mehr, wogegen die TransactionList ja gerade mit beliebigen Objekten arbeiten können soll und auf deren GetHashCode hat man eben leider keine Einfluss. Man müsste schon Object die Guid und GetHashCode "unterjuben", damit es für TransactionList was bingt.

Hallo ikaros,

ich glaube EventHandler-Objekte müsste man gar nicht gesondert behandeln. Der Vorschlag soll ja ohnehin mit beliebigen Objekten klarkommen. Da wären die EventHandler einfach mit drin.

Hallo Quallo,

fein, dass du meinem Vorschlag gefolgt bist. Du hast jetzt genau die springenden Punkte herausgearbeitet. Ich stimme mit deiner Einschätzung vollkommen überein. Die sich aus dem Zusammenspiel von "Dictionary geht nicht" und "man muss Endlosrekursion vermeiden" ergebenden Performance-Problem (quadratisch statt linear), ist genau die Klippe.

Für BinarySearch braucht man eine Ordnung auf den Objekten. Auch bei allen anderen "beschleunigenden" Datenstukturen, wie B-Bäume o.ä. Und diese Ordnung kann man m.E. ohne Zugriff auf die Identität "als Zahl" nicht definieren/verwenden.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Habt ihr euch schon mal System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(object) angeschaut?

Ich weiß nicht direkt ob das weiterhilft, aber es wurde in vielen Newsgroups als Ersatz für Javas IdentityHashcode genannt.
Die Funktion findet sich glaube ich unter java.lang.identityhashcode wieder...

Vielleicht muss man auch einen Ausflug in die Managed C++ Welt machen. Dort kann man doch mit Pointern arbeiten und dadurch eine Adresse kriegen, oder nicht? Wäre es nicht möglich in MC++ eine Klasse abgeleitet von Hashtable zu machen, die Add, AddRange und ContainsKey überschreibt und die Adresse des übergebenen Objekts nimmt.
Praktisch so:


public class adresshashtable : hashtable
{
  public override Add(object key, object value)
  {
     base.Add(getadressofobject(key),value));
  }
    
...


  private int getadressofobject(object adressobject)
  {
     int adress = 0;
    ...
    return adress
  }
}

Wäre das eine Möglichkeit?

Mit partial Classes könnte man auch nur die getadressofobject(object adressobject) in MC++ implementieren, wenn ich das richtig sehe!

Eine weitere Möglichkeit wäre es doch mit Listen in Listen zu arbeiten.
Dass man zum Beispiel pro Type eine Liste macht, z.B. eine Hashtable in der Type zu List<Type>) zugeordnet ist. Dann sucht man zu dem aktuellen Type die Liste und sucht da über Contains das entsprechende Objekt. Das ist auf jeden Fall schon mal schneller(Ich glaube ähnlich arbeitet auch Hashtable intern(mehrere Container ineinander)).
Vielleicht gibt es außer Type und der Adresse noch andere Kriterien eines Objektes, die sich nicht ändern, aus denen man vielleicht einen Hashcode erstellen kann oder es zumindest für eine Teilliste nehmen kann.

Grüße Christoph

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Ich habe ein Beispiel in Mc++ gemacht:

namespace Adresseint
{
	__gc public class AdressTest
	{
		public :

			static int GiveAdress(System::thumbsup:bject __gc* AdressObject)
			{
				return (int)(&AdressObject);
			};


	};
}

Er gibt mir immer brav die Adresse zurück, allerdings die Adresse von dem lokalen AdressObject auf dem Stack.

Wie muss ich das verändern, um die Adresse zu bekommen, auf die AddressObject zeigt?

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(object) klingt vielversprechend. Ob es wirklich hilft, ist nicht klar. Es ist wie folgt implementiert:


public static int GetHashCode(object o)
{
  return object.InternalGetHashCode(o);
}

und damit quasi genauso wie Object.GetHashCode ():


public virtual int GetHashCode()
{
   return object.InternalGetHashCode(this);
}

Was man gegenüber Object.GetHashCode () gewonnen hat, ist dass man den Hashcode unabhängig vom Objekttyp immer über object.InternalGetHashCode berechnet bekommt. Hefen tut das aber nur dann, wenn object.InternalGetHashCode nur und genau die Idendentät des Objekts in einen HashCode wuselt. Ob das so ist, weiß ich aber leider nicht.

Zu Managed C++ kann ich nicht viel sagen. Ich würde aber denken, dass man in Managed C++ nicht mit Pointern arbeiten kann/darf, höchsten wieder mit unsafe wie in C# auch. Ist aber nur eine Vermutung.

Immerhin hat mich sehr aufgebaut, dass es in Java IdentityHashcode gibt. Das zeigt mir zumindestens mal, dass ich nicht spinne, sondern dass ich wirklich was ganz normales will.

Die Aufteilung der Listen nach Typ könnte praktisch in vielen Fällen was bringen, ändert aber nichts am eigentlichen Problem. Wenn man z.B. einen großen TreeNode-Baum transaktionieren will, sind ja alle Objekte vom gleichen Typ.

Deine anderen Fragen kann ich nicht beantworten. Aber vielleicht weiß ja jemand anders noch was.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Es gibt eine Klasse names ObjectID:

TestCode:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;

namespace ObjectID
{
    class Program
    {

        DateTime dt1 = new DateTime(2005, 12, 24, 20, 0, 0);
        DateTime dt2 = new DateTime(2005, 12, 24, 20, 0, 0);
        string s1 = "Stein";
        string s2 = "Stein";
        string s3 = "Haus";


        static void Main(string[] args)
        {
            Program currProg = new Program();
            currProg.test();
        }

        public void test()
        {

            ObjectIDGenerator oidgen = new ObjectIDGenerator();

            bool b;

            Console.WriteLine(dt1);
            Console.WriteLine(oidgen.GetId(dt1, out b));
            Console.WriteLine(b);

            Console.WriteLine();
            Console.WriteLine(dt2);
            Console.WriteLine(oidgen.GetId(dt2, out b));
            Console.WriteLine(b);

            Console.WriteLine();
            Console.WriteLine(dt1);
            Console.WriteLine(oidgen.GetId(dt1, out b));
            Console.WriteLine(b);


            Console.WriteLine();
            Console.WriteLine(s1);
            Console.WriteLine(oidgen.GetId(s1, out b));
            Console.WriteLine(b);

            Console.WriteLine();
            Console.WriteLine(s2);
            Console.WriteLine(oidgen.GetId(s2, out b));
            Console.WriteLine(b);

            Console.WriteLine();
            Console.WriteLine(s1);
            Console.WriteLine(oidgen.GetId(s1, out b));
            Console.WriteLine(b);

            
            s1 = "Haus";

            Console.WriteLine();
            Console.WriteLine(s1);
            Console.WriteLine(oidgen.GetId(s1, out b));
            Console.WriteLine(b);

            Console.WriteLine();
            Console.WriteLine(s3);
            Console.WriteLine(oidgen.GetId(s3, out b));
            Console.WriteLine(b);




        }
    }
}

Ausgabe:

24.12.2005 20:00:00
1
True

24.12.2005 20:00:00
2
True

24.12.2005 20:00:00
3
True

Stein
4
True

Stein
4
False

Stein
4
False

Haus
5
True

Haus
5
False

Sieht also nicht sehr vielversprechend aus.

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

wieso nicht? Das ist doch genau, was wir suchen! Auch die Ausgabe sieht doch gut aus.

herbivore

PS: Allerdings ist nicht klar, ob GetId auch wirklich den gewünschten Aufwand von O(1) hat.

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Ich habe noch ein paar Anmerkungen:

24.12.2005 20:00:00
1
True

24.12.2005 20:00:00
2
True

24.12.2005 20:00:00
3 --> Wieso drei? Das ist doch s1, also müsste so wie wir es wollen 1, false rauskommen. Aber das ist ja ein WerteTyp, der wird geboxt, da kann das gut sein, dass das nicht klappt
True

Stein
4
True

Stein
4 --> Der String ist gleich, hat aber nicht die gleiche Adresse wie s1, also dürfte kein true rauskommen.
False

Stein
4
False

Haus
5
True

Haus
5 --> siehe dem Kommentar davor
False

Das Testen mit selbst erstellen Klassen wäre noch nett!

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

3: DateTime ist ein Value-Type. Der wird bei der Parameterübergabe geboxt und GetId bekommt daher jedesmal eine neue Kopie übergeben.

4: Der Compiler legt für "Stein" nur ein String-Exemplar an, egal wieoft du "Stein" in den Code schreibst. Es wird also dreimal dasselbe Objekt übergeben.

5: siehe die Antwort davor🙂

Sieht also alles gut aus.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 18 Jahren

Wunderbar!
Dann können du/ich/wir oder ich/du/wir oder wir/ich/du oder wer anders sich in den nächsten tagen ja mal an die Implementierung machen.
Die Frage ist ob ObjectID auch funktioniert, wenn man Felder eines Objektes verändert.

Grüße Christoph

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo Quallo,

ich werde in den nächsten Tagen sicher nicht dazu kommen. Aber du hast ja jetzt selbst alle Voraussetzungen. Und eine gute Übung ist es alle mal.

Hier noch spaßeshalber der Code von GetId. Hab ihn mir noch nicht genau angeguckt, aber er basiert wohl tatsächlich auf RuntimeHelpers.GetHashCode:


public virtual long GetId(object obj, out bool firstTime)
{
      bool flag1;
      long num1;
      if (obj == null)
      {
            throw new ArgumentNullException("obj", Environment.GetResourceString("ArgumentNull_Obj"));
      }
      int num2 = this.FindElement(obj, out flag1);
      if (!flag1)
      {
            this.m_objs[num2] = obj;
            this.m_ids[num2] = this.m_currentCount++;
            num1 = this.m_ids[num2];
            if (this.m_currentCount > ((this.m_currentSize * 4) / 2))
            {
                  this.Rehash();
            }
      }
      else
      {
            num1 = this.m_ids[num2];
      }
      firstTime = !flag1;
      return num1;
}
 
private int FindElement(object obj, out bool found)
{
      int num1 = RuntimeHelpers.GetHashCode(obj);
      int num2 = 1 + ((num1 & 0x7fffffff) % (this.m_currentSize - 2));
      while (true)
      {
            int num3 = ((num1 & 0x7fffffff) % this.m_currentSize) * 4;
            for (int num4 = num3; num4 < (num3 + 4); num4++)
            {
                  if (this.m_objs[num4] == null)
                  {
                        found = false;
                        return num4;
                  }
                  if (this.m_objs[num4] == obj)
                  {
                        found = true;
                        return num4;
                  }
            }
            num1 += num2;
      }
}

private void Rehash()
{
      int num1 = 0;
      int num3 = this.m_currentSize;
      while ((num1 < ObjectIDGenerator.sizes.Length) && (ObjectIDGenerator.sizes[num1] <= num3))
      {
            num1++;
      }
      if (num1 == ObjectIDGenerator.sizes.Length)
      {
            throw new SerializationException(Environment.GetResourceString("Serialization_TooManyElements"));
      }
      this.m_currentSize = ObjectIDGenerator.sizes[num1];
      long[] numArray1 = new long[this.m_currentSize * 4];
      object[] objArray1 = new object[this.m_currentSize * 4];
      long[] numArray2 = this.m_ids;
      object[] objArray2 = this.m_objs;
      this.m_ids = numArray1;
      this.m_objs = objArray1;
      for (int num4 = 0; num4 < objArray2.Length; num4++)
      {
            if (objArray2[num4] != null)
            {
                  bool flag1;
                  int num2 = this.FindElement(objArray2[num4], out flag1);
                  this.m_objs[num2] = objArray2[num4];
                  this.m_ids[num2] = numArray2[num4];
            }
      }
}

Die Frage ist ob ObjectID auch funktioniert, wenn man Felder eines Objektes verändert.

Kannst du ja mal testen. Aber mittlerweile denke ich, dass RuntimeHelpers.GetHashCode dem IdentityHashcode von Java entspricht.

herbivore

2.891 Beiträge seit 2004
vor 17 Jahren

Öhm, gibt's eigentlich schon Ergebnisse?
Bin nämlich gerade dabei, mir ein tolles Beispiel für mein AOP-Seminar einfallen zu lassen. Transaktionalität für Objekte gefiele mir da sehr gut. Mit AOP lässt sich da ja sicher noch einiges rausholen...

Gruß
dN!3L

49.485 Beiträge seit 2005
vor 17 Jahren

Hallo dN!3L,

ich weiß nicht, ob Quallo noch was gemacht hat. Was mich anbelangt, sind alle Ergebnisse hier in diesem Thread veröffentlicht.

Wenn du einen Seimiarvortrag halten willst (was mich freuen würde), wäre es ja m.E. ohnehin nicht legitim, das Endergebnis einfach zu übernehmen. Anderseits bieten die bisherigen Beiträge aber doch eine sehr solide Grundlage für einen eigenen Transactionmanager. Du könntest ihn dann hier posten.

herbivore

2.891 Beiträge seit 2004
vor 17 Jahren

Original von herbivore
Wenn du einen Seimiarvortrag halten willst (was mich freuen würde), wäre es ja m.E. ohnehin nicht legitim, das Endergebnis einfach zu übernehmen. Anderseits bieten die bisherigen Beiträge aber doch eine sehr solide Grundlage für einen eigenen Transactionmanager. Du könntest ihn dann hier posten.

Das ganze hier könnte ich dann eh nicht übernehmen (da es ja um AOP geht). Hier ginge es mir eher um die Idee, wie man den Zustand eines Objektes speichern kann. Aber mal sehen, wie weit ich mit AOP komme, das zu verwendende Framework hat noch einige Einschränkungen, die nicht ganz so transparent zulässt, wie ich es mir erhofft habe. Falls es was wird, kann ich die Ergebnisse dann natürlich posten.

Gruß
dN!3L

49.485 Beiträge seit 2005
vor 17 Jahren

Hallo dN!3L,

Hier ginge es mir eher um die Idee, wie man den Zustand eines Objektes speichern kann.

hm, das ist doch grade Thema des Threads und es wird ja nicht nur genau diese Idee beschrieben, sondern sogar durch Code demonstriert.

herbivore

Q
Quallo Themenstarter:in
992 Beiträge seit 2005
vor 17 Jahren

Leider hat es der Unternehmensalltag nicht zugelassen was zu entwickeln. Aber die Grundlagen sollten wir hier haben!

Grüße, Christoph