Rtti accessing fields and properties in complex data structures

前端 未结 3 1124
情书的邮戳
情书的邮戳 2020-12-28 11:34

As already discussed in Rtti data manipulation and consistency in Delphi 2010 a consistency between the original data and rtti values can be reached by accessing members by

3条回答
  •  有刺的猬
    2020-12-28 12:27

    You're touching a few concepts and problems with this question. First of all you've mixed in some record types and some properties, and I'd like to handle this first. Then I'll give you some short info on how to read the "Left" and "Top" fields of a record when that record is part of an field in a class... Then I'll give you suggestions on how to make this work generically. I'm probably going to explain a bit more then it's required, but it's midnight over here and I can't sleep!

    Example:

    TPoint = record
      Top: Integer;
      Left: Integer;
    end;
    
    TMyClass = class
    protected
      function GetMyPoint: TPoint;
      procedure SetMyPoint(Value:TPoint);
    public
      AnPoint: TPoint;           
      property MyPoint: TPoint read GetMyPoint write SetMyPoint;
    end;
    
    function TMyClass.GetMyPoint:Tpoint;
    begin
      Result := AnPoint;
    end;
    
    procedure TMyClass.SetMyPoint(Value:TPoint);
    begin
      AnPoint := Value;
    end;
    

    Here's the deal. If you write this code, at runtime it will do what it seems to be doing:

    var X:TMyClass;
    x.AnPoint.Left := 7;
    

    But this code will not work the same:

    var X:TMyClass;
    x.MyPoint.Left := 7;
    

    Because that code is equivalent to:

    var X:TMyClass;
    var tmp:TPoint;
    
    tmp := X.GetMyPoint;
    tmp.Left := 7;
    

    The way to fix this is to do something like this:

    var X:TMyClass;
    var P:TPoint;
    
    P := X.MyPoint;
    P.Left := 7;
    X.MyPoint := P;
    

    Moving on, you want to do the same with RTTI. You may get RTTI for both the "AnPoint:TPoint" field and for the "MyPoint:TPoint" field. Because using RTTI you're essentially using a function to get the value, you'll need do use the "Make local copy, change, write back" technique with both (the same kind of code as for the X.MyPoint example).

    When doing it with RTTI we'll always start from the "root" (a TExampleClass instance, or a TMyClass instance) and use nothing but a series of Rtti GetValue and SetValue methods to get the value of the deep field or set the value of the same deep field.

    We'll assume we have the following:

    AnPointFieldRtti: TRttiField; // This is RTTI for the AnPoint field in the TMyClass class
    LeftFieldRtti: TRttiField; // This is RTTI for the Left field of the TPoint record
    

    We want to emulate this:

    var X:TMyClass;
    begin
      X.AnPoint.Left := 7;
    end;
    

    We'll brake that into steps, we're aiming for this:

    var X:TMyClass;
        V:TPoint;
    begin
      V := X.AnPoint;
      V.Left := 7;
      X.AnPoint := V;
    end;
    

    Because we want to do it with RTTI, and we want it to work with anything, we will not use the "TPoint" type. So as expected we first do this:

    var X:TMyClass;
        V:TValue; // This will hide a TPoint value, but we'll pretend we don't know
    begin
      V := AnPointFieldRtti.GetValue(X);
    end;
    

    For the next step we'll use the GetReferenceToRawData to get a pointer to the TPoint record hidden in the V:TValue (you know, the one we pretend we know nothing about - except the fact it's a RECORD). Once we get a pointer to that record, we can call the SetValue method to move that "7" inside the record.

    LeftFieldRtti.SetValue(V.GetReferenceToRawData, 7);
    

    This is allmost it. Now we just need to move the TValue back into X:TMyClass:

    AnPointFieldRtti.SetValue(X, V)
    

    From head-to-tail it would look like this:

    var X:TMyClass;
        V:TPoint;
    begin
      V := AnPointFieldRtti.GetValue(X);
      LeftFieldRtti.SetValue(V.GetReferenceToRawData, 7);
      AnPointFieldRtti.SetValue(X, V);
    end;
    

    This can obviously be expanded to handle structures of any depth. Just remember that you need to do it step-by-step: The first GetValue uses the "root" instance, then the next GetValue uses an Instance that's extracted from the previous GetValue result. For records we may use TValue.GetReferenceToRawData, for objects we can use TValue.AsObject!

    The next tricky bit is doing this in a generic way, so you can implement your bi-directional tree-like structure. For that, I'd recommend storing the path from "root" to your field in the form of an TRttiMember array (casting will then be used to find the actual runtype type, so we can call GetValue and SetValue). An node would look something like this:

    TMemberNode = class
      private
        FMember : array of TRttiMember; // path from root
        RootInstance:Pointer;
      public
        function GetValue:TValue;
        procedure SetValue(Value:TValue);
    end;
    

    The implementation of GetValue is very simple:

    function TMemberNode.GetValue:TValue;
    var i:Integer;    
    begin
      Result := FMember[0].GetValue(RootInstance);
      for i:=1 to High(FMember) do
        if FMember[i-1].FieldType.IsRecord then
          Result := FMember[i].GetValue(Result.GetReferenceToRawData)
        else
          Result := FMember[i].GetValue(Result.AsObject);
    end;
    

    The implementation of SetValue would be a tiny little bit more involved. Because of those (pesky?) records we'll need to do everything the GetValue routine does (because we need the Instance pointer for the very last FMember element), then we'll be able to call SetValue, but we might need to call SetValue for it's parent, and then for it's parent's parent, and so on... This obviously means we need to KEEP all the intermediary TValue's intact, just in case we need them. So here we go:

    procedure TMemberNode.SetValue(Value:TValue);
    var Values:array of TValue;
        i:Integer;
    begin
      if Length(FMember) = 1 then
        FMember[0].SetValue(RootInstance, Value) // this is the trivial case
      else
        begin
          // We've got an strucutred case! Let the fun begin.
          SetLength(Values, Length(FMember)-1); // We don't need space for the last FMember
    
          // Initialization. The first is being read from the RootInstance
          Values[0] := FMember[0].GetValue(RootInstance);
    
          // Starting from the second path element, but stoping short of the last
          // path element, we read the next value
          for i:=1 to Length(FMember)-2 do // we'll stop before the last FMember element
            if FMember[i-1].FieldType.IsRecord then
              Values[i] := FMember[i].GetValue(Values[i-1].GetReferenceToRawData)
            else
              Values[i] := FMember[i].GetValue(Values[i-1].AsObject);
    
          // We now know the instance to use for the last element in the path
          // so we can start calling SetValue.
          if FMember[High(FMember)-1].FieldType.IsRecord then
            FMember[High(FMember)].SetValue(Values[High(FMember)-1].GetReferenceToRawData, Value)
          else
            FMember[High(FMember)].SetValue(Values[High(FMember)-1].AsObject, Value);
    
          // Any records along the way? Since we're dealing with classes or records, if
          // something is not a record then it's a instance. If we reach a "instance" then
          // we can stop processing.
          i := High(FMember)-1;
          while (i >= 0) and FMember[i].FieldType.IsRecord do
          begin
            if i = 0 then
              FMember[0].SetValue(RootInstance, Values[0])
            else
              if FMember[i-1].FieldType.IsRecord then
                FMember[i].SetValue(FMember[i-1].GetReferenceToRawData, Values[i])
              else
                FMember[i].SetValue(FMember[i-1].AsObject, Values[i]);
            // Up one level (closer to the root):
            Dec(i)
          end;
        end;
    end;
    

    ... And this should be it. Now some warnings:

    • DON'T expect this to compile! I actually wrote every single bit of code in this post in the web browser. For technical reasons I had access to the Rtti.pas source file to look up method and field names, but I don't have access to an compiler.
    • I'd be VERY careful with this code, especially if PROPERTIES are involved. A property can be implemented without an backing field, the setter procedure might not do what you expect. You might run into circular references!

提交回复
热议问题