Entwickler-Ecke

Sonstiges (Delphi) - Welcher Record ist in Item.Data gespeichert?


ml-kuen - Mo 02.06.14 23:29
Titel: Welcher Record ist in Item.Data gespeichert?
Hallo,

Hab nach längerer Suche nichts gefunden, darum möchte ich im Forum mal nachfragen.

Ich habe verschiedene Records mit 2, 3 oder 4 Feldern. Diese werden wahlweise durch TreeNode.Data referenziert. Die Zuweisung ist auch kein Problem.
Nur, wie bekomme ich später heraus, welcher Rekord-Typ in Data hinterlegt wurde um diesen zu Re-Referenzieren?

Hoffentlich brauche ich dazu kein Object, dann müsste ich den Quelltext komplett überarbeiten.

Gruß

Michael L.


jaenicke - Mo 02.06.14 23:50

Prinzipiell würde ich immer zu Objekten greifen, insbesondere in solch einem Fall, da hier die Vererbung wie die Faust aufs Auge passt. Dazu kommt, dass du dir die ganze Pointerspielerei sparen kannst...

Aber wenn du in die Records als erstes ein Flag z.B. in Form eines Integerwerts einbaust, das den Typ angibt, kannst du es als Notlösung auch so lösen, dass du den Pointer auf PInteger castest und so diesen Integerwert auswertest... Schön ist das nicht, funktioniert aber.


Xion - Di 03.06.14 08:16

Würde ich anders machen (wobei auch ich vermutlich Objekte an dieser Stelle bevorzugen würde).

Du erzeugst dir pro Record-Typ ein Array, dort steckst du deine Records rein. So kannst du diese sehr effizient auch für andere Zwecke aufzählen. Und die fummelige Speicherverwaltung entfällt (new/dispose).

In das Data Feld hängst du dann ein TDataLink Record ein:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
type TDataLink=record
  recType: integer; //oder ein enum  -> definiert, welches array benutzt wird
  recIdx : integer; //-> definiert, welcher index im array das record ist
end;
type PDataLink=PDataLink;

var
recType1Array: array of TRecType1;
recType2Array: array of TRecType2;
recType3Array: array of TRecType3;
recDataLinks : array of TDataLink;


ml-kuen - Mi 04.06.14 21:31

Hallo jaenicke, hallo Xion,

Im Grunde habe ich das auch so gemacht wie jeanicke beschrieben hat. Ich zeig mal, was ich gemacht habe:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
Type
  pScopeLst   = ^aScopeLst;
  aScopeLst   = RECORD
    Caption   : String;
    IP        : String;
    Mask      : String;
    Name      : String;
    Comment   : String;
  END;

  pRangeLst   = ^aRangeLst;
  aRangeLst   = RECORD
    Caption   : String;
    IP_Begin  : String;
    IP_End    : String;
  END;

  pExcludeLst = ^aExcludeLst;
  aExcludeLst = RECORD
    Caption   : String;
    IP_Begin  : String;
    IP_End    : String;
  END;

  pOptionLst   = ^aOptionLst;
  aOptionLst   = RECORD
    Caption    : String;
    Value: String;
  END;

TFrmMain = class(TForm)
  ...
public
    ScopeLst   : TList;
    aScopeRec  : pScopeLst;
    RangeLst   : TList;
    aRangeRec  : pRangeLst;
    ExcludeLst : TList;
    aExcludeRec: pExcludeLst;
    OptionLst  : TList;
    aOptionRec : pOptionLst;

procedure TFrmMain.FormCreate(Sender: TObject);
begin
  ScopeLst   := TList.Create;
  RangeLst   := TList.Create;
  ExcludeLst := TList.Create;
  OptionLst  := TList.Create;
end;

procedure TFrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
var Node : TTreeNode;
begin
  ScopeLst.Free;
  RangeLst.Free;
  ExcludeLst.Free;
  OptionLst.Free;
end;

procedure TFrmMain.CreateTree(Sender: TObject; SList : TStrings); 
var j: Integer;
    Node : TTreeNode;
Begin
   For j:= 0 To SList.Count - 1 do Begin
     If c > 6
       then If (SList.Strings[3] = 'add'and (SList.Strings[4] = 'scope')
         then Begin //Child-Nodes mit Bereich (Scope) hinzufügen
                 New(aScopeRec);
                 aScopeRec^.Caption   := 'Scope';
                 aScopeRec^.IP        := SList.Strings[5];
                 aScopeRec^.Mask      := SList.Strings[6];
                 aScopeRec^.Name      := SList.Strings[7];
                 If c > 7 then Begin
                   aScopeRec^.Comment :=SList.Strings[8];
                   Node := Sender.Items.AddChild(Node,SList.Strings[7]);
                 end else Begin
                   aScopeRec^.Comment :=SList.Strings[7];
                   Node := Sender.Items.AddChild(Node,SList.Strings[5]);
                 end;
                 ScopeLst.Add(aScopeRec);
                 Node.Data       := aScopeRec;
                 Node.ImageIndex := 0;
                 Node.SelectedIndex :=0;
              end;
     If c = 9
       then If (SList.Strings[5] = 'Add'and (SList.Strings[6] = 'iprange')
         then Begin //Child-Nodes mit IP-Range hinzufügen
                 New(aRangeRec);
                 aRangeRec^.Caption  := 'Range';
                 aRangeRec^.IP_Begin := SList.Strings[7];
                 aRangeRec^.IP_End   := SList.Strings[8];
                 Node := Sender.Items.AddChild(Node,SList.Strings[7] + ' - ' + SList.Strings[8]);
                 RangeLst.Add(aRangeRec);
                 Node.Data          := aRangeRec;
                 Node.ImageIndex    := 116;
                 Node.SelectedIndex := 116;
              end;
     If c = 9
       then If (SList.Strings[5] = 'add'and (SList.Strings[6] = 'excluderange')
         then Begin //Child-Nodes mit IP-Ausschlussbereich hinzufügen
                 New(aExcludeRec);
                 aExcludeRec^.Caption  := 'ExcludeRange';
                 aExcludeRec^.IP_Begin := SList.Strings[7];
                 aExcludeRec^.IP_End   := SList.Strings[8];
                 Node := Sender.Items.AddChild(Node,SList.Strings[7] + ' - ' + SList.Strings[8]);
                 ExcludeLst.Add(aExcludeRec);
                 Node.Data          := aExcludeRec;
                 Node.ImageIndex    := 13
                 Node.SelectedIndex := 13;
              end;
     If c > 9
       then If (SList.Strings[6] = 'optionvalue')
         then Begin //Child-Nodes mit Server-Optionen hinzufügen
                 Ov := StrToInt(SList.Strings[7]);
                 case Ov of
                   1:  Begin
                         New(aOptionRec);
                         aOptionRec^.Caption := 'SubnetMask';
                         aOptionRec^.Value   := SList.Strings[9];
                         Node := Sender.Items.AddChild(Node,'Subnetzmaske ' + '[' + SList.Strings[9] + ']');
                         Node.ImageIndex := 149; Node.SelectedIndex := 149;
                         OptionLst.Add(aOptionRec);
                         Node.Data := aOptionRec;
                       end;
                   3:  Begin
                         New(aOptionRec);
                         aOptionRec^.Caption := 'Router';
                         aOptionRec^.Value   := SList.Strings[9];
                         Node := Sender.Items.AddChild(Node,'Router ' + '[' + SList.Strings[9] + ']');
                         Node.ImageIndex    := 149
                         Node.SelectedIndex := 149;
                         OptionLst.Add(aOptionRec);
                         Node.Data := aOptionRec;
                       end;
                   6:  Begin
                         New(aOptionRec);
                         aOptionRec^.Caption := 'DNS';
                         aOptionRec^.Value   := SList.Strings[9];
                         Node := Sender.Items.AddChild(Node,'DNS-Server ' + '[' + SList.Strings[9] + ']');
                         Node.ImageIndex    := 149;
                         Node.SelectedIndex := 149;
                         OptionLst.Add(aOptionRec);
                         Node.Data := aOptionRec;
                       end;
                 end;
              end;
   end//For
end;

Zugegeben, ich habe den Quelltext etwas angepasst, aber nur um es etwas anschaulicher darzustellen.
Durch das Feld 'Caption' kann man den Typ des Records nun zwar identifizieren, aber wie mache ich das praktisch im Quelltext?
Wie kann man dies beispielsweise in der folgenden Ereignisroutine realisieren?


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
procedure TFrmMain.JvTVChange(Sender: TObject; Node: TTreeNode);
var n : TTreeNode;
    NL: Integer;
begin
  NL := Node.Level;
  case NL of
    3Begin //Einträge in Childs suchen und in Edits eintragen
         N := Node.getFirstChild;
         While (N <> NILdo Begin
           if Assigned(N) and Assigned(N.Data) then Begin

             ???

           end;
           N := N.getNextSibling
         end;
       end;
  end;
end;

hat jemand eine Idee?


Xion - Do 05.06.14 00:33

Na so gehts wohl nicht, weil das Caption ja im Record steht, dessen Typ du erst kennen musst, um in das Record reinzugucken. Das ginge nur mit Objekten. Deine Caption müsste Teil von TTreeNode sein. Wie user profile iconjaenicke sagte ginge es, wenn Caption integer ist (hmm, könnte auch mit String gehen), aber ich persönlich finde den Ansatz sehr unsauber.


Vorsicht! Böser Code!
1:
2:
3:
4:
5:
6:
 if Assigned(N) and Assigned(N.Data) then
      Begin
         case PString(N) of
            'Range': ShowMessage('I''m a range record');
         end;
      end;


Aus verschiedenen Gründen ist es besser, statt Caption: String lieber Enumerates zu benutzen (Performanz, weniger Fehleranfällig bzgl. vertippen etc.).


Delphi-Quelltext
1:
2:
3:
type TCaptionEnum = (ceRange, ceScope, ce...);
...
caption: TCaptionEnum;


ml-kuen - Do 05.06.14 09:04

Guten Morgen,

ihr habt natürlich beide Recht. Nach dem ich die Sache überschlafen habe, verstehe ich es auch. Der Record muss selbstverständlich eindeutig identifiziert werden. Im Grunde bin ich natürlich auch für Erneuerung und dafür veraltete Dinge über Bord zu werfen. Die vorgehensweise mit den Pointern hatte ich allerdings schon einige Male praktiziert und die haben auch funktioniert. Die Strings wollte ich natürlich später durch Integer oder Aufzählung ersetzen.
Abschließend nur noch eine Frage. Reicht es für mein Vorhaben ein einfaches Objekt zu erstellen, oder sollte ich von Persistent erben?

Danke schon mal an euch beide für die Anregungen und Hilfestellung.


Blup - Do 05.06.14 14:08

Wenn es nur darum geht die Records zu ersetzen, genügt eine Ableitung von TObject.
Ich würde eine eigene Basisklasse von TObject ableiten, um später die Möglichkeit zu haben, zentral die Basisklasse zu ersetzen.
z.B.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
type
  TMyCustomDataObject = class(TObject)
  end;

  TMyDataObject1 = class(TMyCustomDataObject)
    {...}
  end;

  TMyDataObject2 = class(TMyCustomDataObject)
    {...}
  end;
// usw.

Der wesentliche Unterschied zum Record ist die Notwendigkeit die Objekte zu Erzeugen und zum Schluss wieder freizugeben.
In der Regel verwaltet man Objekte auch deshalb in eine TObjectList (statt Array).


ml-kuen - Fr 06.06.14 15:57

Dank auch an user profile iconBlup für seinen Kommentar. Ich habe mich nun doch entschlossen mit Objekten zu arbeiten und werde in Kürze noch einen Status dazu abgeben. Warum soll man sich den Stress mit den Pointern auch noch machen. Schließlich ist Delphi effizient genug und was sind schon ein paar zusätzliche Byte im Arbeitsspeicher bei heutigen Rechnern. Da überwiegt doch klar die Übersichtlichkeit und Sicherheit des objektorientierten Ansatzes.


catweasel - Sa 07.06.14 16:07

An der Stelle habe ich auch einmal eine Frage zur objektorientierten Variante:

Welche der beiden Möglichkeiten ist besser/bevorzugt ihr:

Variante A: Das Objekt wird durch seine RTTI identifiziert.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
type 
  TClassA=class(TObject)
  end;

type 
  TClassB=class(TObject)
  end;


var
obj : TClassA;
begin
obj := TClassA.Create;
if obj is TClassA then showmessage('ClassA object')
  else showmessage('ClassB object');
obj.Free;
end;


oder

Variante B: Der Objekttyp wird als Feld einer gemeinsamen Basisklasse geführt und zuerst ausgeweret. Dann kann entspechend weiter gecastet werden.


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
type
  TClassTypeID = (ctTypeA,ctTypeB);

type 
  TBaseClass=class(TObject)
    FType : TClassTypeID;
  end;

type 
  TClassA=class(TBaseClass)
    constructor Create;
  end;

type 
  TClassB=class(TBaseClass)
    constructor Create;
  end;


constructor TClassA.Create;
begin
FType := ctTypeA;
end;

constructor TClassB.Create;
begin
FType := ctTypeB;
end;



var
obj : TClassA;
begin
obj := TClassA.Create;
if TBAseClass(obj).FType = ctTypeA then showmessage('ClassA object')
  else showmessage('ClassB object');
obj.Free;
end;


Ich entscheide mich immer für Variante B weil mal wo gelesen habe das der RTTI Zugriff (generell die IS und AS Operatoren) sehr langsam ist.
Aber vielleicht ist das ja auch falsch?!

Mich würde eure MEinung dazu mal interessieren.

Cheers,
Catweasel


jaenicke - So 08.06.14 12:50

Variante A ist die deutlich bessere, da Variante B nicht viel mit OOP zu tun hat...
Von der Geschwindigkeit wird es kaum einen spürbaren Unterschied machen.


catweasel - So 08.06.14 13:11

Ich fände Varaiante A auch eleganter und manchmal mache ich es so.
Nur lese ich immer wider Dinge wie die hier : http://etutorials.org/Programming/mastering+delphi+7/Part+I+Foundations/Chapter+2+The+Delphi+Programming+Language/Type-Safe+Down-Casting/

Zitat:
The two RTTI operators, is and as, are extremely powerful, and you might be tempted to consider them as standard programming constructs. Although they are indeed powerful, you should probably limit their use to special cases. [...] RTTI has a negative impact on performance, because it must walk the hierarchy of classes to see whether the typecast is correct.


Und da dachte ich eben bisher immer das mitgeführte Feld der Basisklasse ist schneller da nichts mehr "gesucht" werden muss..
Und da gibts noch mehr Quellen die uni sono sagen "RTTI ist schön, aber bitte vermeiden". DAs verunsichert dann schon etwas...


Cheers,
Catweasel


jfheins - So 08.06.14 13:18

Tja, das Problem ist jetzt, dass "sehr langsam" relativ zu sehen ist. Es mag sein, dass is und as hunderte von CPU-Zyklen brauchen. Aber letztlich sind 600 Zyklen auf heutigen CPU's in 200 Nanosekunden abgearbeitet.
Solange du das also nicht in einer Schleife sehr oft aufruft (so circa 1 Mio mal) ist das absolut kein Problem. Bedenke, selbst bei 1 Mio Aufrufen bist du bei 200 Millisekunden Laufzeit.
Also einfach benutzen. Und falls du irgendwann wirklich Performance-Probleme haben solltest, kannst du meistens etwas lokal an der Stelle ändern um das zu beheben.

Ich persönlich würde die is und as Operatoren aber auch nicht zu RTTI zählen.


catweasel - So 08.06.14 13:52

Stimmt auch wieder. Aber man will ja als guter Programmierer immer so effizient wie möglich sein :lol:


Im Bezug auf die Orginalfrage wäre das hier dann wohl eine mögliche Vorgehensweise:

http://docwiki.embarcadero.com/RADStudio/XE6/de/Informationen_f%C3%BCr_strukturierte_Typen
Da kann man dann sehen ob es drei vier oder mehr Felder sind. GetFields sollte es verraten...

Toll das das auch direkt mit den Feldnamen geht :)
(Hatte um RTTI immeer einen Bogen gemacht, ist aber sich sinnvoll wenn man seinen eigenen Objektinspektor schreiben will :)
http://docwiki.embarcadero.com/RADStudio/XE6/de/Laufzeitoperationen_mit_Typen

Aber schonmal Danke für die Anregung mal ein bischen zu schmökern :)

Cheers,
Catweasel


jfheins - So 08.06.14 14:20

Lass es mich aber mal so formulieren: Mit dem Einsatz von RTTI (mit Ausnahme von is und as) solltest du auch vorsichtig sein, weil der Code tendenziell schwerer zu durchschauen wird.
Das ist insbesondere dann von Bedeutung wenn du Sachen, die das Typensystem schon hergibt, selber nochmal "in schlecht" neu implementierst :P

Was sich (wie hier) problemlos mit Objekten und Klassen abbilden lässt, rechtfertigt also keine Emulation der Klassenhierarchie durch records mit RTTI. Interessant wird es übrigens auch, wenn du sowas machen kannst:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
type 

  TBase = class(TObject)
  procedure DoStuff(a: Integer); virtualabstract;
  end;

  TClassA = class(TBase)
  procedure DoStuff(a: Integer); override;
  end;

  TClassB = class(TBase)
  procedure DoStuff(a: Integer); override;
  end;


var
obj : TBase;
begin
obj := TClassA.Create();
obj.DoStuff(4); // Ruft automatisch TClassA.DoStuff() auf.
obj.Free;
end;


Du sparst damit die Stellen, an denen du ein fettes case-Statements mit ähnlichem Code für jeden Möglichen Typen hast. Ganz interessant ist es dann, wenn ein Objekt neu dazu kommt, ob du daran denkst in jedem case die neue Klasse zu behandeln.

P.S.: Du wirst feststellen, dass auch in der offiziellen Dokumentation der as-Operator nicht mit RTTI in Verbindung gebracht wird.


catweasel - So 08.06.14 14:54

Zitat:
P.S.: Du wirst feststellen, dass auch in der offiziellen Dokumentation der as-Operator nicht mit RTTI in Verbindung gebracht wird.


Wirds doch :tongue: (*klugscheissennichtverkneifenkann*)

Zitat:
Mit der Eigenschaft TRttiType.TypeKind können Sie feststellen, ob der ermittelte TRttiType einem gegebenen Typ entspricht. Der Wert der Eigenschaft gibt den tatsächlichen Typ an. Eine weitere Methode zum Feststellen des tatsächlichen Klassentyps einer TRttiType-Instanz stellt der Operator is dar:


Und wenn man mit RTTI arbeitet kommt man um is und as nicht herum


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
    for LType in LContext.GetTypes() do
        if LType is TRttiInt64Type then
        begin
            Writeln('Type name:', LType.Name);
            Writeln('Min allowed value:', TRttiInt64Type(LType).MinValue);
            Writeln('Max allowed value:', TRttiInt64Type(LType).MaxValue);
            Writeln;
        end;


Aber ich weis worauf du hinauswillst. Man sollte sich jedesmal fragen, wenn man meint RTTI zu brauchen, ob da nicht irgendwo eine Designschwäche in der Objekthierachie ist.
Doch muss ich zugeben das meine Skepsis is und as gegenüber doch ein wenig schwindet :)

Cheers,
Catweasel


Martok - So 08.06.14 19:13

Kurz gesagt: is und as haben nichts mit RTTI in dem Sinne wie es überall benutzt wird zu tun.

Beide arbeiten auf der Class/ClassParent-Kette, welche integraler Bestandteil des Objektkonzepts von Delphi(-artigen Sprachen) ist. Zusätzlich zu den "sichtbaren" Feldern welche von TObject geerbt werden, hat jede Klasseninstanz (nicht also objects) diverse andere Attribute, welche mit im Speicher liegen. Dazu gehört die virtuelle Methodentabelle VMT, ein Zeiger auf die Klasse als TClass, der Self-Pointer, der System-Exception-Record (für Exception und abgeleitete), in neueren Delphis die TMonitor-Instanz (warum auch immer die da drin ist und nicht TObject hinzugefügt wurde) und noch einiges anderes was ich grade nicht aus'm Kopf weiß. System.pas, Suchwort 'vmtSelf' oder so ähnlich. Einer dieser Zeiger geht auch auf den RTTI-Block, sofern denn beim Compilieren eingestellt war, dass die generiert werden sollen.

is tut jetzt nichts anderes als der dadurch entstandenen verketteten Liste zu folgen:


Pseudocode
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
function Is(Instance: TObject; Class: TClass): boolean;
var
  classptr: TClass;
begin
  Result:= false;
  classptr:= Instance.vmt[vmtParent];
  while assigned(classptr) do begin
    if Class = classptr then
      Exit(true);
    classptr:= classptr.vmt[vmtParent];
  end;  
end;


as ist einfach nur eine Zuweisung mit Exception falls is nicht true ist.

Bei mäßig komplizierten Klassenbäumen sind das einige 10 CMP,MOV,LEA-Instruktionen. Kann man vernachlässigen ;)

"RTTI" im Sinne von "Such mir mal die Eigenschaft mit dem Namen 'abc' und weise der den Integerwert 42 zu" ist eine ganz andere Geschichte. AFAIR auch sehr schön im Doberstein erklärt, aber hier Irr-Elefant.