Search code examples
delphigenericsrttitypechecking

How can I test if an unknown Delphi RTTI TValue reflects an object that is ANY type of generic TList<> (or at least TEnumerable<>)?


In Delphi, if I have a TValue instance reflecting an unknown object, how can I test if this object is an instance of ANY kind of generic TEnumerable<> (or even better, also which specific generic enumerable type it is an instance of, e.g. TList<>)?

NOTE: I already know how to easily check its exact type, i.e. with the .BaseType property of the corresponding TRttiType of the TValue, resulting in for example TList<string>, but what I want to test is rather if it is a TList<> of any sub-item type.

To exemplify how this hypothetical code "IsAnyKindOfGenericEnumerable()" would work, here is some example code:

var
   LContext : TRttiContext;
   obj_1_rtti_value : TValue;
   obj_2_rtti_value : TValue;
   obj_3_rtti_value : TValue;
   obj_1_rtti_type : TRttiType;
   obj_2_rtti_type : TRttiType;
   obj_3_rtti_type : TRttiType;

LContext := TRttiContext.Create();

{
...
obj_1_rtti_value is set to a TValue reflection of a TList<string> object here
obj_2_rtti_value is set to a TValue reflection of a plain TObject object here
obj_3_rtti_value is set to a TValue reflection of a TQueue<integer> object here
...
}

obj_1_rtti_type := LContext.GetType(obj_1_rtti_value.TypeInfo);
obj_2_rtti_type := LContext.GetType(obj_2_rtti_value.TypeInfo);
obj_3_rtti_type := LContext.GetType(obj_3_rtti_value.TypeInfo);

IsAnyKindOfGenericEnumerable(obj_1_rtti_type); //Would return true
IsAnyKindOfGenericEnumerable(obj_2_rtti_type); //Would return false
IsAnyKindOfGenericEnumerable(obj_3_rtti_type); //Would return true

And again, the very best thing would be if I could also detect which kind of TEnumerable<> type it is, like for example:

IsAnyKindOfGenericEnumerable(obj_1_rtti_type); //Will return true + `TList<>`
IsAnyKindOfGenericEnumerable(obj_2_rtti_type); //Will return false
IsAnyKindOfGenericEnumerable(obj_3_rtti_type); //Will return true + `TQueue<>`

I have tried:

if obj_1_rtti_type is TRttiEnumerationType then
begin
   //...
end;

but for some reason this evaluates to false, which I'm completely at loss as to why that is? The expression value_type.BaseType.Name does indeed evaluate to 'TEnumerable<System.string>' in this case, but there really has to be some other way than to manually parse this string in order to accomplish my objective, right?

Finally, the goal must be accomplished solely using the RTTI info, that is, any "cheating" by referring to the real object behind the TValue is not permitted (for reasons outside the scope of this question).


Solution

  • There is no RTTI generated for Generic types themselves (they don't exist at runtime), and each specific instantiation (like TList<string>) is a distinct class type with its own distinct RTTI. You would have to check for each individual type, it is not possible to test for any Generic type. Parsing class names is the only way to detect Generic types.

    1. use TRttiType.Name to get the class name as a string ('TList<System.string>').

    2. parse it to detect the presence of angle brackets ('<>').

    3. extract the substring between the brackets ('System.string')

    4. walk the ancestor tree looking for an ancestor whose TRttiType.Name is 'TEnumerable<...>', where ... is the extracted substring ('TEnumerable<System.string>').

    However, this approach fails for class types that derive from TEnumerable<T> but do not have Generics parameters themselves, eg:

    type
      TMyClass = class(TEnumerable<string>)
      end;
    

    To account for that, ignore steps 1-3 and jump right to step 4 by itself, ignoring whatever value appears between the brackets, eg:

    function IsAnyKindOfGenericEnumerable(AType: TRttiType): Boolean;
    begin
      Result := False;
      while AType <> nil do
      begin
        Result := StartsText('TEnumerable<', AType.Name);
        if Result then Exit;
        AType := AType.BaseType;
      end;
    end;
    

    As for TRttiEnumerationType, it represents enumerated types (ie: type typeName = (val1, ...,valn);). It has nothing to do with TEnumerable<T>. That is why the is operator is always returning False for you - none of the RTTI types you are testing represent enums.