如何在 Delphi 中仅通过 RTTI 信息(即不使用任何实际对象实例)获取 TObjectList<T> 的子项类型?

How can I get the sub-item type of a TObjectList<T> purely by RTTI information (i.e. without using any actual object instance) in Delphi?

我正在实现使用 RTTI 流式传输任意 Delphi 对象的通用代码,为了让它工作(更具体地说,为了让加载部分工作),我需要以某种方式在不使用任何实际对象实例的情况下获取 TObjectList<T> 字段的子项类型。

要求不使用任何实际对象实例的明显原因是,在从流加载对象的情况下(仅基于要加载的对象的 class 类型的知识), 在加载完成之前我根本没有任何可用实例 - 我宁愿只能访问相关 class 的纯 RTTI 数据。

我希望能够加载的 class 示例如下:

TTestClass = class(TObject)
public
   test_list : TList<string>;
end;

我想要的是能够得出结论,test_list 字段是一个通用的 TList<T>,其中 Tstring(即为了知道什么数据期望从流中获取子项)。

如果 class 确实如下所示:

TTestClassWithArr = class(TObject)
public
   test_arr : array of string;
end;

我可以使用 TRttiDynamicArrayType RTTI class 的 test_arr 字段的 ElementType() 方法来纯粹通过 RTTI 提取此信息,但我找不到任何对应的TObjectList<T>.

的显式 RTTI 类型

另一个堆栈溢出问题 (Delphi Rtti: how to get objects from TObjectList<T>) 是相关的,但确实使用了 RTTI 数据反映到 "cheat" 的对象的实际实例以获取子项,同样,这对我来说不是一个选择,因为当时我必须知道这些子项目不存在。

虽然感觉应该有某种方法可以通过单独使用 class 的 RTTI 信息来做到这一点,因为所有类型信息在编译时显然都存在,无论对象如何实例化。

很遗憾,没有为通用参数生成 RTTI。在像 TList<T> 这样的通用容器中发现 T 的值的唯一方法是获取目标字段本身的 TRttiType,调用它的 ToString() 方法来获取它的 class name 为字符串,解析出括号内的子串。例如:

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;

将通用参数提取为字符串后,如果需要访问该类型的 RTTI,则可以使用 TRttiContext.FindType()

话虽如此,以下代码提供了一堆 RTTI 助手:

DSharp.Core.Reflection.pas(Google代码)

DSharp.Core.Reflection.pas(比特桶)

除其他外,它定义了一个 TRttiTypeHelper class 帮助器,它向 TRttiType:

添加了一个 GetGenericArguments() 方法
TRttiTypeHelper = class helper for TRttiType
...
public 
  ...
  function GetGenericArguments: TArray<TRttiType>;
  ...
end;

在内部,GetGenericArguments() 使用我上面提到的相同技术。有了它,您可以改为这样做:

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;

我一直在寻找解决方案,我有一个想法想与您分享。该解决方案使用 rtti 并从列表(TList、TObjectList 等)中获取方法的参数 "Add"。在我的函数中,我只是 return Class 类型,但您可以轻松实现原始类型。我希望它可以帮助某人。问候。

关注:

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;

样本

...
var
  List: TList<TCustomer>;
begin
  List := TList<Customer>.Create();

  ShowMessage(TUtilRtti.GetSubTypeItemClassFromList(List).ClassName);
end;