Reputation: 699
I'm implementing generic code for streaming arbitrary Delphi objects using RTTI, and in order to get this to work (more specifically, in order to get the loading part to work), I need to somehow get the sub-item type of a TObjectList<T>
field without making use of any actual object instance.
The obvious reason for the requirement to not use any actual object instance is that in the case of loading an object from a stream (based solely on the knowledge of the class type of the object to be loaded), I won't have any instance at all available before the loading has completed - I will rather only have access to the pure RTTI data of the class in question.
An example of such a class that I'd like to be able to load is the following:
TTestClass = class(TObject)
public
test_list : TList<string>;
end;
What I want is to be able to conclude that the test_list
field is a generic TList<T>
where T
is string
(i.e. in order to know what data to expect from the stream for the sub-items).
If the class did instead look as follows:
TTestClassWithArr = class(TObject)
public
test_arr : array of string;
end;
I could use the ElementType()
method of the TRttiDynamicArrayType
RTTI class of the test_arr
field to extract this information purely through RTTI, but I cannot find any corresponding such explicit RTTI type for TObjectList<T>
.
Another Stack Overflow question (Delphi Rtti: how to get objects from TObjectList<T>
) is related, but does indeed use an actual instance of the object that the RTTI data reflects to "cheat" in order to get to the sub-items, which, again, is not an option for me since these sub-items do not exist at the time I must know this.
It really feels like there should be some way to do this by solely using the RTTI information of the class though, since all the type information is obviously present for it at compile-time, regardless of object instantiation.
Upvotes: 0
Views: 3063
Reputation: 31
I've been looking for a solution about this and I had an ideia that I'd like to share with you. That solution uses rtti and get the method's argument "Add" from a list(TList,TObjectList,etc). In my function I just return the Class Type, but you can easyly implements to primivite types. I hope it can help someone. Regards.
Folow:
class function TUtilRtti.GetSubTypeItemClassFromList(ObjectList: TObject): TClass;
var
ctxRtti : TRttiContext;
typeRtti : TRttiType;
atrbRtti : TCustomAttribute;
methodRtti: TRttiMethod;
parameterRtti: TRttiParameter;
begin
result := nil;
ctxRtti := TRttiContext.Create;
typeRtti := ctxRtti.GetType( ObjectList.ClassType );
methodRtti := typeRtti.GetMethod('Add');
for parameterRtti in methodRtti.GetParameters do
begin
if SameText(parameterRtti.Name,'Value') then
begin
if parameterRtti.ParamType.IsInstance then
result := parameterRtti.ParamType.AsInstance.MetaclassType;
break;
end;
end;
ctxRtti.Free;
end;
Sample
...
var
List: TList<TCustomer>;
begin
List := TList<Customer>.Create();
ShowMessage(TUtilRtti.GetSubTypeItemClassFromList(List).ClassName);
end;
Upvotes: 3
Reputation: 597941
Unfortunately, there is no RTTI generated for Generic parameters. The only way to discover the value of T
in a Generic container like TList<T>
is to get the TRttiType
for the target field itself, call its ToString()
method to get its class name as a string, and parse out the substring that is between the brackets. For example:
uses
..., System.StrUtils, System.Rtti;
var
Ctx: TRttiContext;
s: string;
OpenBracket, CloseBracket: Integer;
...
begin
...
s := Ctx.GetType(TTestClass).GetField('test_list').FieldType.ToString; // returns 'TList<System.string>'
OpenBracket := Pos('<', s);
CloseBracket := PosEx('>', s, OpenBracket+1);
s := Copy(s, OpenBracket+1, CloseBracket-OpenBracket-1); // returns 'System.string'
// if multiple Generic parameters are present, they will be separated by commas...
...
end;
Once you have extracted the Generic parameter as a string, you can use TRttiContext.FindType()
if you need to access the RTTI for that type.
With that said, the following code provides a bunch of RTTI helpers:
DSharp.Core.Reflection.pas (Google Code)
DSharp.Core.Reflection.pas (BitBucket)
Amongst other things, it defines a TRttiTypeHelper
class helper that adds a GetGenericArguments()
method to TRttiType
:
TRttiTypeHelper = class helper for TRttiType
...
public
...
function GetGenericArguments: TArray<TRttiType>;
...
end;
Internally, GetGenericArguments()
uses the same technique I mention above. With it, you can do this instead:
uses
..., System.Rtti, DSharp.Core.Reflection;
var
Ctx: TRttiContext;
arr: TArray<TRttiType>;
typ: TRttiType;
s: string;
...
begin
...
arr := Ctx.GetType(TTestClass).GetField('test_list').FieldType.GetGenericArguments;
typ := arr[0]; // returns RTTI for System.String
s := typ.ToString; // returns 'System.string'
...
end;
Upvotes: 3