3

我正在尝试在 Delphi XE 中使用 RTTI 设置记录的值。我可以使用 GetValue 方法从记录中获取值,但无法使用 SetValue 方法设置值。

有谁知道如何做到这一点/为什么它不起作用?

提前致谢!

我的上下文:最终目标是编写一个组件,该组件将读取任何 XML 文件并使用来自 XML 的数据自动填充应用程序的数据模型。数据模型将被注释以确定所有元素的 XPath。对于对象和基本数据类型,我已经启动并运行了它。

  TSize = record
    X, Y: double;
  end;

  TMyTest = class
  protected
    FSize: TSize;
  public
    constructor Create;
    procedure DoStuff;
  end;

constructor TMyTest.Create;
begin
  FSize.X := 2.7;
  FSize.Y := 3.1;
end;

procedure TMyTest.DoStuff;
var
  MyContext: TRttiContext;
  MyField: TRttiField;
  MySizeField: TRttiField;
  MyVal: TValue;
  MyRecord: TRttiRecordType;
  NewVal: TValue;
begin 
  // Explicit Create of MyContext does not help (as expected)
  for MyField in MyContext.GetType(ClassType).GetFields do
    if MyField.Name = 'FSize' then //For debugging
    begin
      MyRecord := MyField.FieldType.AsRecord;
      MyVal := MyField.GetValue(Self);
      for MySizeField in MyRecord.GetFields do
      begin
        //This works
        NewVal := MySizeField.GetValue(MyVal.GetReferenceToRawData).AsExtended;
        NewVal := NewVal.AsExtended + 5.0;
        try
          // This does not work. (no feedback)
          MySizeField.SetValue(MyVal.GetReferenceToRawData, NewVal);
          // This however does work. Now to find out what the difference between the two is.
          MySizeField.SetValue(@FSize, NewVal);
        except
          on e: Exception do //Never happens
            ShowMessage('Oops!' + sLineBreak + e.Message);
        end;
      end;
    end;
  // Shows 'X=2.7 Y=3.1'
  // Expected 'X=7.7 Y=8.1'
  ShowMessage(Format('X=%f Y=%f', [FSize.X, FSize.Y]));
end;

如果 TSize 被声明为类,则MyVal.GetReferenceToRawData在代码中应替换为TObject(MyVal.GetReferenceToRawData^). 如果你这样做,一切都会按预期工作。(是的,MyVal.AsObject在这种情况下也可以解决问题)这使我找到了一个可能的解决方案:MyVal.GetReferenceToRawData^将类型转换为正确的记录类型。这怎么能做到呢?

我刚刚尝试在 SetValue 中直接使用@FSize。正如您所期望的那样,这很有效。这引发了一个问题:@FSize 和 MyVal.GetReferenceToRawData 有什么不同


经过大量进一步的调查,我发现 MyVal 实际上是记录的副本,因此确实如 Serg 在他的第一个答案中提到的那样正确设置了值,但是,它是在副本中设置的。我觉得很傻,没有早点意识到这一点……

无论如何,下面是一个有效的代码示例。此外,如果您愿意使用“字段名称路径”,您可以在这篇文章中查看 Barry Kelly 的方法,这让我走上了正轨。我只是不喜欢Follow例程所需的路径。

procedure TMyTest.DoStuff;
var
  MyContext: TRttiContext;
  MyField: TRttiField;
  MySizeField: TRttiField;
  NewVal: TValue;
  dMyVal: double;
begin
  for MyField in MyContext.GetType(ClassType).GetFields do
    if MyField.Name = 'FSize' then
    begin
      for MySizeField in MyField.FieldType.GetFields do
      begin
        dMyVal := MySizeField.GetValue(PByte(Self) + MyField.Offset).AsExtended;
        NewVal := TValue.From(dMyVal + 5.1);
        try
          MySizeField.SetValue(PByte(Self) + MyField.Offset, NewVal);
        except
          on e: Exception do
            ShowMessage('Oops!' + sLineBreak + e.Message);
        end;
      end;
    end;
  if FSize.X > 5.0 then
    ShowMessage(Format('X=%f Y=%f', [FSize.X, FSize.Y]));
end;
4

3 回答 3

1

MyVal.GetReferenceToRawData不返回指向FSize字段的指针,这就是FSize字段值不会改变的原因。

我不确定我的代码是你想要写的,但我希望它会有所帮助:

uses rtti, typinfo;

type
  TSize = record
    X, Y: double;
  end;

  TMyTest = class
  protected
    FSize: TSize;
  public
    constructor Create;
    procedure DoStuff;
  end;

constructor TMyTest.Create;
begin
  FSize.X := 2.7;
  FSize.Y := 3.1;
end;

procedure TMyTest.DoStuff;
var
  MyContext: TRttiContext;
  MyField: TRttiField;
  MyVal: TValue;
  NewSize: TSize;

begin
  // Explicit Create of MyContext does not help (as expected)
  for MyField in MyContext.GetType(ClassType).GetFields do
    if MyField.Name = 'FSize' then //For debugging
    begin
      MyVal := MyField.GetValue(Self);
      NewSize:= MyVal.AsType<TSize>;
      NewSize.X:= NewSize.X + 5;
      NewSize.Y:= NewSize.Y + 5;
      TValue.Make(@NewSize, TypeInfo(TSize), MyVal);
      MyField.SetValue(Self, MyVal);
    end;
  ShowMessage(Format('X=%f Y=%f', [FSize.X, FSize.Y]));
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Test: TMyTest;

begin
  Test:= TMyTest.Create;
  Test.DoStuff;
  Test.Free;
end;
于 2012-04-25T10:28:50.767 回答
1

我遇到了同样的困难,但在我的情况下,我使用的是属性而不是字段,这非常复杂,因为 TRttiProperty 没有属性 Offset 来解决问题,在搜索了很多并运行了几个测试后,我找到了一个解决方案,我在这里分享。

procedure TMyTest.DoStuff;
var
  MyContext: TRttiContext;
  MyProp: TRttiProperty;
  MySizeField: TRttiField;
  NewVal: TValue;
  dMyVal: double;
  MyPointer:Pointer;
begin
  for MyProp in MyContext.GetType(ClassType).GetProperties do
    if MyProp.Name = 'Size' then
    begin
      MyPointer := TRttiInstanceProperty(MyProp).PropInfo^.GetProc;
      for MySizeField in MyProp.PropertyType.GetFields do
      begin
        dMyVal := MySizeField.GetValue(PByte(Self) + Smallint(MyPointer)).AsExtended;
        NewVal := TValue.From(dMyVal + 5.1);
        try
          MySizeField.SetValue(PByte(Self) + Smallint(MyPointer), NewVal);
        except
          on e: Exception do
            ShowMessage('Oops!' + sLineBreak + e.Message);
        end;
      end;
    end;
  if FSize.X > 5.0 then
    ShowMessage(Format('X=%f Y=%f', [FSize.X, FSize.Y]));
end;
于 2015-10-23T19:33:58.670 回答
-2

RTTI 在已发布的属性上效果最好。所以如果你添加

发布属性大小:TSize read fSize write fSize;

到你的课我想你会有更好的成绩。

于 2012-04-24T16:48:14.803 回答