![]() |
RTTI: generische TObjectList erkennen
Ich erstelle mir gerade einen eigenen JSON-Serializer. Dieser verwendet ein Attribut für die Properties einer Klasse, um diese in ein JSON-Objekt zu parsen und vice versa. Das funktioniert auch schon ziemlich gut, aber nur dann, wenn Listen als Array-Properties veröffentlicht sind. Nun suche ich eine Möglichkeit, per RTTI eine TObjectList<T> zu erkennen und zu durchlaufen. Ich habe ein wenig in REST.JsonReflect gespickt, da wird soweit ich das verstanden habe auf die Felder zugegriffen, der TListHelper gesucht und bei Fund benutzt. Bei meinen eigenen Versuchen, das auch so zu machen (Property -> Liste -> ListHelper) bin ich allerdings kläglich gescheitert, es werden lediglich 2 Felder gefunden, der ListHelper ist nicht dabei. Hat jemand einen Tipp für mich, oder ist mein Ansatz gleich zum Scheitern verurteilt?
Danke fürs Lesen. |
AW: RTTI: generische TObjectList erkennen
Ohne konkreten Quelltext ist das immer schwer zu beurteilen, aber hast du dir den Rtti-Quelltext aus REST.JsonReflect zum Studium in ein Konsolenprogramm kopiert?
Delphi-Quellcode:
program Project6;
{$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils, System.Generics.Collections, System.Rtti; type TMyObject = class(TObject) private FDateTime: string; public constructor Create; end; TMyContainer = class private FList: TObjectList<TMyObject>; public constructor Create; property List: TObjectList<TMyObject> read FList; end; constructor TMyObject.Create; begin inherited; FDateTime := FormatDateTime('YYYY-MM-DD', System.SysUtils.Now); end; type PListHelperCrack = ^TListHelperCrack; TListHelperCrack = record private FItems: Pointer; FCount: Integer; FTypeInfo: Pointer; [unsafe] FListObj: TObject; end; function GetArrayValueFromTListHelperValue(const ACtx: TRttiContext; const AListHelperValue: TValue; out ACount: Integer): TValue; var LpListHelper: PListHelperCrack; begin if (AListHelperValue.TypeInfo <> TypeInfo(System.Generics.Collections.TListHelper)) or AListHelperValue.IsEmpty then raise EInvalidCast.Create('Error'); LpListHelper := AListHelperValue.GetReferenceToRawData; TValue.Make(@LpListHelper^.FItems, LpListHelper^.FTypeInfo, Result); ACount := LpListHelper^.FCount; end; var MyContainer: TMyContainer; Data: TObject; FRTTICtx: TRttiContext; rttiType, rttiType2: TRttiType; rttiField: TRttiField; Value: TValue; valArr, SingleArrayValue: TValue; I, Len: Integer; { TMyContainer } constructor TMyContainer.Create; begin FList := TObjectList<TMyObject>.Create; end; begin try MyContainer := TMyContainer.Create; MyContainer.List.Add(TMyObject.Create); MyContainer.List.Add(TMyObject.Create); MyContainer.List.Add(TMyObject.Create); Data := MyContainer.List; FRTTICtx := TRttiContext.Create; rttiType := FRTTICtx.GetType(Data.ClassType); for rttiField in rttiType.GetFields do begin Writeln('I found this Field: ', rttiField.Name); if rttiField.Name = 'FListHelper' then begin Writeln(' That''s the right one: ', rttiField.Name); case rttiField.FieldType.TypeKind of TTypeKind.tkRecord, TTypeKind.tkMRecord: begin Writeln(' ' + rttiField.Name, ' is a record!'); Value := rttiField.GetValue(Data); case Value.Kind of TTypeKind.tkRecord, TTypeKind.tkMRecord: begin rttiType2 := FRTTICtx.GetType(Value.typeInfo); // Marshal TList<T>.FListHelper as dynamic array using 10.3 layout if rttiType2.Handle = TypeInfo(System.Generics.Collections.TListHelper) then begin valArr := GetArrayValueFromTListHelperValue(FRTTICtx, Value, Len); for I := 0 to Len - 1 do begin SingleArrayValue := valArr.GetArrayElement(I); case SingleArrayValue.Kind of tkClass: begin Writeln(' Found single object: ', I, ' ', SingleArrayValue.AsObject.ClassName); end; end; end; end; end end; end; end; end; end; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; Readln; end. |
AW: RTTI: generische TObjectList erkennen
Danke für die Mühe. Zumindest unter Alexandria wird die Bedingung
Zitat:
[edit] Gerade unter 10.4 versucht, da funktioniert es. Super, wieder etwas kaputt verbessert, ich bin immer wieder begeistert von Delphi. :-( [/edit] |
AW: RTTI: generische TObjectList erkennen
Ach guck an, wie ist denn TList<T> in Delphi 11 definiert?
In 10.4 geht's so los:
Delphi-Quellcode:
TList<T> = class(TEnumerable<T>)
public type arrayofT = array of T; ParrayofT = ^arrayofT; private var FListHelper: TListHelper; FComparer: IComparer<T>; FOnNotify: TCollectionNotifyEvent<T>; ... |
AW: RTTI: generische TObjectList erkennen
In Alexandria ist FListHelper kein Feld mehr, sondern eine Funktion.
|
AW: RTTI: generische TObjectList erkennen
FListHelper war und ist nicht einfach ohne Absicht private. Die Implementierung einer TList<T> hat von Außen einfach nicht zu interessieren. Wenn die Implementierung seitens Embarcadero geändert wird, müssen die natürlich dafür sorgen, dass ihre eigenen Funktionalitäten erhalten bleiben (z.B. JSON serialisieren), aber sie müssen nicht garantieren, dass als private Gekennzeichnetes unverändert bleibt.
|
AW: RTTI: generische TObjectList erkennen
Ach Herrje! Was fummeln die denn da immer dran rum? :shock:
Jede Version ist da anders. Und ob die Änderungen überhaupt einen Vorteil haben, sei mal dahingestellt. In 10.2.3 sah die Welt noch so aus:
Delphi-Quellcode:
TList<T> = class(TEnumerable<T>)
private type arrayofT = array of T; var FListHelper: TListHelper; // FListHelper must always be followed by FItems FItems: arrayofT; // FItems must always be preceded by FListHelper FComparer: IComparer<T>; FOnNotify: TCollectionNotifyEvent<T>; ... |
AW: RTTI: generische TObjectList erkennen
Die haben daran rumgefummelt, damit man eine TList<T> wieder im Debugger vernünftig inspekten kann und die darin enthaltenen Elemente sehen kann, you're welcome.
|
AW: RTTI: generische TObjectList erkennen
Ja, das ändert sich nicht zum ersten Mal und eigentlich immer nur weil es entsprechende Beschwerden gab, wie Stefan schon anmerkte.
Allein schon die Kommentare in REST.JsonReflect.pas sprechen für sich: Zitat:
|
AW: RTTI: generische TObjectList erkennen
Ich erinnere mich dunkel, dass in XE8 das auch nicht zu debuggen/inspekten war, oder?
Ich vermag das nicht zu beurteilen, aber bringt das denn überhaupt irgendwelche Vorteile in Sachen Performance oder Speicher(layout)-Dingenskirchen da ständig Hand anzulegen, anstatt einfach ein dynamisches Array (arrayOfT/TArray<T>) als Feld in TList<T> zu hinterlegen? Egal, kurz über Uwes ersten Post nachgedacht und (wie immer) voller Wahrheit und Sinn befunden. Private ist private! Also müssen wir uns über die Properties behelfen. Habe nur 10.4 hier, aber wenn die
Delphi-Quellcode:
immer noch da ist, dann müsste das auch in Delphi 11 klappen.
property List: arrayofT read GetList
Delphi-Quellcode:
program Project6;
{$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils, System.Generics.Collections, System.Rtti; type TMyObject = class(TObject) private FDateTime: string; public constructor Create; end; TMyContainer = class private FList: TObjectList<TMyObject>; public constructor Create; property List: TObjectList<TMyObject> read FList; end; constructor TMyObject.Create; begin inherited; FDateTime := FormatDateTime('YYYY-MM-DD', System.SysUtils.Now); end; var MyContainer: TMyContainer; Data, ArrayObject: TObject; FRTTICtx: TRttiContext; rttiType: TRttiType; rttiProperty: TRttiProperty; Value: TValue; SingleArrayValue: TValue; I, Len: Integer; { TMyContainer } constructor TMyContainer.Create; begin FList := TObjectList<TMyObject>.Create; end; begin try MyContainer := TMyContainer.Create; MyContainer.List.Add(TMyObject.Create); MyContainer.List.Add(TMyObject.Create); MyContainer.List.Add(TMyObject.Create); Data := MyContainer.List; FRTTICtx := TRttiContext.Create; rttiType := FRTTICtx.GetType(Data.ClassType); for rttiProperty in rttiType.GetProperties do begin Writeln('I found this Property: ', rttiProperty.Name); if rttiProperty.Name = 'List' then begin Writeln('..That''s the right one: ', rttiProperty.Name); case rttiProperty.PropertyType.TypeKind of TTypeKind.tkDynArray: begin Writeln('....' + rttiProperty.Name, ' is a TTypeKind.tkDynArray!'); Value := rttiProperty.GetValue(Data); case Value.Kind of TTypeKind.tkDynArray: begin Len := Value.GetArrayLength; for I := 0 to Len - 1 do begin SingleArrayValue := Value.GetArrayElement(I); case SingleArrayValue.Kind of tkClass: begin ArrayObject := SingleArrayValue.AsObject; // Len ist 3 => vier Elemente (?), so dass beim Zugriff auf das letzte // Element nil rauskommt! Daher abfangen oder bessere Lösung suchen! if Assigned(ArrayObject) then begin Writeln('.......Found single object: ', I, ' ', ArrayObject.ClassName); end; end; end; end; end end; end; end; end; end; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; Readln; end. |
AW: RTTI: generische TObjectList erkennen
Das sieht doch mal gut aus, herzlichen Dank :thumb:. Bleibt nur zu hoffen, dass Name und Typ der Property List sich künftig nicht auch noch ändern.
|
AW: RTTI: generische TObjectList erkennen
Diese .List Property ist ja schon immer™ der Zugriff auf das interne Array gewesen.
Auch schon bei der alten System.Classes.TList. Nutze ich in der Regel auch in Schleifen, wenn von 0 bis Count - 1 iteriert wird, um die GetItem-Funktion zu umgehen. Die mag zwar je nach Delphi-Version zwar inline markiert sein, aber es wird unnötig Index >= FCount geprüft. |
AW: RTTI: generische TObjectList erkennen
Zitat:
Code:
Sollte man übrigens runtime packages nutzen, fällt das inlining weg und der Zugriff auf .List wird nicht mehr geinlined und jedesmal aufgerufen, was diesen Ansatz ca 10mal langsamer macht, der nicht mehr geinlinete Getter hingegen kostet nur ca 50% mehr. Dieselbe Benchmark mit rtl als runtime package:
-------------------------------------------------------
Benchmark Time CPU Iterations ------------------------------------------------------------- RTL-getter/10 9,31 ns 9,28 ns 64000000 RTL-getter/100 114 ns 115 ns 6400000 RTL-getter/1000 1150 ns 1147 ns 640000 RTL-getter/10000 11550 ns 11475 ns 64000 RTL-getter/100000 115873 ns 117188 ns 5600 RTL-list/10 9,16 ns 9,21 ns 74666667 RTL-list/100 113 ns 115 ns 6400000 RTL-list/1000 1151 ns 1144 ns 560000 RTL-list/10000 11577 ns 11719 ns 64000 RTL-list/100000 115813 ns 117188 ns 6400
Code:
Benchmark Code:
-------------------------------------------------------
Benchmark Time CPU Iterations ------------------------------------------------------------- RTL-getter/10 16,9 ns 16,9 ns 40727273 RTL-getter/100 169 ns 169 ns 4072727 RTL-getter/1000 1639 ns 1650 ns 407273 RTL-getter/10000 16257 ns 16392 ns 44800 RTL-getter/100000 163355 ns 161122 ns 4073 RTL-list/10 146 ns 146 ns 4480000 RTL-list/100 1461 ns 1475 ns 497778 RTL-list/1000 14593 ns 14300 ns 44800 RTL-list/10000 145201 ns 142997 ns 4480 RTL-list/100000 1452860 ns 1443273 ns 498
Delphi-Quellcode:
uses
Spring.Benchmark, Generics.Collections; procedure RTLGetter(const state: TState); var list: TList<Integer>; count, i: Integer; sum: Integer; begin list := TList<Integer>.Create; count := state[0]; for i := 1 to count do list.Add(i); sum := 0; while state.KeepRunning do begin sum := 0; for i := 0 to list.Count - 1 do Inc(sum, list[i]); end; state.Counters['sum'] := sum; list.Free; end; procedure RTLList(const state: TState); var list: TList<Integer>; count, i: Integer; sum: Integer; begin list := TList<Integer>.Create; count := state[0]; for i := 1 to count do list.Add(i); sum := 0; while state.KeepRunning do begin sum := 0; for i := 0 to list.Count - 1 do Inc(sum, list.List[i]); end; state.Counters['sum'] := sum; list.Free; end; begin Benchmark(RTLGetter, 'RTL-getter').RangeMultiplier(10).Range(10, 100000); Benchmark(RTLList, 'RTL-list').RangeMultiplier(10).Range(10, 100000); Benchmark_Main(); end. |
AW: RTTI: generische TObjectList erkennen
Da messe ich mit Hausmitteln mit D10.4 auf einen i5-7600 in Debug Win32 anderes:
Code:
RTL-Getter -> sum: 5000000050000000 calculated in 205,2559 ms
RTL-list -> sum: 5000000050000000 calculated in 192,9382 ms ---------------- RTL-Getter -> sum: 5000000050000000 calculated in 204,5107 ms RTL-list -> sum: 5000000050000000 calculated in 191,9137 ms ---------------- RTL-Getter -> sum: 5000000050000000 calculated in 205,1316 ms RTL-list -> sum: 5000000050000000 calculated in 191,7222 ms ----------------
Delphi-Quellcode:
Richtig spannend wird's aber mit Release Win64, da muss ich mal in mich gehen und drüber nachdenken warum das sich so eklatant umkehrt.
program Project6;
{$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils, System.Diagnostics, Generics.Collections; const cCount: Integer = 100000000; var W: TStopwatch; procedure RTLGetter; var list: TList<Integer>; count, i: Integer; sum: Int64; begin list := TList<Integer>.Create; count := cCount; for i := 1 to count do list.Add(i); sum := 0; W := TStopwatch.StartNew; for i := 0 to list.Count - 1 do Inc(sum, list[i]); W.Stop; Writeln('RTL-Getter -> ','sum: ', sum, ' calculated in ', W.Elapsed.TotalMilliseconds.ToString, ' ms'); list.Free; end; procedure RTLList; var list: TList<Integer>; count, i: Integer; sum: int64; begin list := TList<Integer>.Create; count := cCount; for i := 1 to count do list.Add(i); sum := 0; W := TStopwatch.StartNew; for i := 0 to list.Count - 1 do Inc(sum, list.List[i]); W.Stop; Writeln('RTL-list -> ', 'sum: ', sum, ' calculated in ', W.Elapsed.TotalMilliseconds.ToString, ' ms'); list.Free; end; procedure Benchmark_Main(); begin RTLList; RTLGetter; Writeln('----------------'); end; begin try for var I := 1 to 3 do Benchmark_Main(); except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; Readln; end.
Code:
RTL-Getter -> sum: 5000000050000000 calculated in 65,1854 ms
RTL-list -> sum: 5000000050000000 calculated in 108,6827 ms ---------------- RTL-Getter -> sum: 5000000050000000 calculated in 58,9665 ms RTL-list -> sum: 5000000050000000 calculated in 108,7423 ms ---------------- RTL-Getter -> sum: 5000000050000000 calculated in 60,7217 ms RTL-list -> sum: 5000000050000000 calculated in 105,2705 ms ---------------- |
AW: RTTI: generische TObjectList erkennen
Zitat:
|
AW: RTTI: generische TObjectList erkennen
Zitat:
|
AW: RTTI: generische TObjectList erkennen
Ich sehe bei deinem Win32 Ergebnis nix groß anders - deine Ergebnisse sind ~7% voneinander entfernt - außerdem ist Debug witzlos, um irgendwelche Performancemessungen durchzuführen.
Außerdem habe ich mir schon was dabei gedacht, meine sum Variable als Integer zu deklarieren (die ist nur dafür da, dass der Listenzugriff nicht wegoptimiert wird) und nicht Int64. Eine Int64 Addition auf Win32 kostet nämlich genug, um in diesem Benchmark einen erheblichen Ausschlag zu geben. Zitat:
Außer, dass der Delphi Compiler Schrott ist und oftmals viel zu viel register herumgemove produziert, ist es komplett egal, ob da die "Index in Range" Überprüfung ausgeführt wird. Was außerdem öfters (mal wieder, weil der Compiler dämlich ist) Auswirkung hat, ist die Größe der Integer Variablen. Bei einem direktzugriff auf ein Array mag der Compiler lieber die native Bittigkeit, wohingegen bei dem Getter Index ja vom Typ Integer ist. Spiel mal mit dem Typen für i herum und schau, wie sich die Ergebnisse ändern. |
AW: RTTI: generische TObjectList erkennen
Zitat:
|
AW: RTTI: generische TObjectList erkennen
Zitat:
Zitat:
In diesem speziellen Fall kann es außerdem sein, dass man Microbenchmarkabweichungen zwischen den beiden Methoden haben kann, die die Unterschiede erklären. Hierzu empfehle ich dieses Video: ![]() Es kommt außerdem oftmals auch auf die CPU an, denn die machen inzwischen so fancy sachen wie "move elimination", bei denen manchmal das vom Compiler generierte Rumgegurke einfach wegfliegt, da die CPU sieht, dass es nur Hin- und Hergeschiebe ist. |
Alle Zeitangaben in WEZ +1. Es ist jetzt 22:30 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz