如何使用Delphi的RTTI获取和设置记录值

11
我正在尝试使用Delphi XE或更高版本中增强的RTTI功能,将对象读写到XML中。到目前为止,我已经成功地处理了整数、浮点数、字符串、枚举类型、集合和类,但无法正确输出或读取记录。问题似乎在于获取记录属性的实例(指针)。
//Outputs Properties To XML
procedure TMyBase.SaveToXML(node: TJclSimpleXMLElem);
var
  child , subchild : TjclSimpleXMLElem ;
  FContext : TRttiContext ;
  FType    : TRttiType ;
  FProp    : TRttiProperty ;
  Value    : TValue ;
  MyObj    : TMyBase ;
  FField   : TRttiField ;
  FRecord  : TRttiRecordType ;
  Data     : TValue ;
begin
  FContext := TRttiContext.Create ;
  FType := FContext.GetType ( self.ClassType ) ;
  Child := node.Items.Add ( ClassName ) ;
  for FProp in FType.GetProperties do begin
    if FProp.IsWritable then begin
      case FProp.PropertyType.TypeKind of
        tkClass : begin
          MyObj := TMyBase ( FProp.GetValue ( self ).AsObject ) ;
          MyObj.SaveClass ( Child.Items.Add ( FProp.Name ) , FContext ) ;
          end ;
        tkRecord : begin
          subchild := Child.Items.Add ( FProp.Name ) ;
          FRecord := FContext.GetType(FProp.GetValue(self).TypeInfo).AsRecord ;
          for FField in FRecord.GetFields do begin
            >>> self is not the correct instance <<<
            Value := FField.GetValue ( self ) ;
            subchild.Items.Add ( FField.Name ).Value := Value.ToString ;
            end;
          end ;
        else begin
          Value := FProp.GetValue(self) ;
          Child.Items.Add ( FProp.Name ).Value := Value.ToString ;
          end;
        end;
      end ;
    end ;
  FContext.Free ;
end;

我猜想如果我能弄清楚如何获取这些值,那么设置它们就不应该是个问题。然后进入数组,哦,天哪!
更新:请参见下面。(已迁移为单独的答案以提高可见性)。

哪个版本的Delphi? - GolezTrol
1
你想保存“记录”而不是“类”的特定原因吗?如果没有,请检查OmniXML,它具有将TPersistent后代保存和加载到/从*.XML文件的功能。http://www.omnixml.com/ - user497849
谢谢 Dorin,我会看看的。我考虑过限制支持的类型,但由于现有应用程序已经使用了大量记录,我希望覆盖记录和记录数组。 - Mitch
2个回答

12

我猜想您正在尝试保存Self运行时类型的记录类型字段的值,对吗?

首先,您需要使用FProp.GetValue(Self)获取字段的值。假设您将其放入名为FieldValue类型为TValue的变量中。然后,您可以自由地保存记录值的字段,但是您可能需要编写递归过程,因为记录的字段本身可能是字段。记录类型的字段获取器期望记录的地址(指向其开头的指针),以使其与设置器对称;设置器期望地址而不是值,否则没有简单的方法在另一个类或记录中“原地”修改字段,因为记录通常按值传递。

您可以使用FieldValue.GetReferenceToRawData获取此指针,它将返回指向存储在TValue中的记录起始位置的指针。

希望这能给您足够的提示来继续进行。


谢谢,我会尝试一下。TMyBase将成为未来类的祖先,并将执行读取和写入这些类的工作。因此,self是要保存的实际运行时对象。 - Mitch
2
你们老一辈的人中,有没有人还记得 Turbo Pascal 的时代?那个时候,你会通过创建一个“F:File of MyRecord”,然后使用“assign(F,filename)”来持久化记录,并将二进制记录内容写入文件吗? - Warren P

5

归属: 最初由问题的发布者 (Mitch) 作为问题更新发布 - 移动为单独的答案以提高可见性。

Barry的解决方案很好用。以下是修订后的代码:

    tkRecord : begin
      subchild := Child.Items.Add ( FProp.Name ) ;
      Value := FProp.GetValue(self) ;
      FRecord := FContext.GetType(FProp.GetValue(self).TypeInfo).AsRecord ;
      for FField in FRecord.GetFields do begin
        Data := FField.GetValue ( Value.GetReferenceToRawData ) ;
        subchild.Items.Add ( FField.Name ).Value := Data.ToString ;
        end;
      end ;

对于那些需要处理数组的人:

    tkDynArray : begin
      Value := FProp.GetValue ( self ) ;
      FArray := FContext.GetType(Value.TypeInfo) as TRttiDynamicArrayType ;
      subchild := child.Items.Add ( FProp.Name ) ;
      cnt := Value.GetArrayLength ;
      subchild.Properties.Add ( 'Count' , cnt ) ;
      case FArray.ElementType.TypeKind of
        tkInteger ,
        tkFloat   : begin
          for a := 0 to cnt-1 do begin
            Data := Value.GetArrayElement ( a ) ;
            subchild.Items.Add ( IntToStr(a) , Data.ToString ) ;
            end;
          end ;
        tkRecord  : begin
          FRecord := FArray.ElementType as TRttiRecordType ;
          for a := 0 to cnt-1 do begin
            Data := Value.GetArrayElement ( a ) ;
            subsubchild := subchild.Items.Add ( IntToStr(a) ) ;
            for FField in FRecord.GetFields do
              SaveField ( subsubchild , FContext , FField , Data.GetReferenceToRawData ) ;
            end;
          end ;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接