7

「 Delphi 2010 での Rtti データの操作と整合性」ですでに説明したように、元のデータと rtti 値の間の整合性は、TRttiField とインスタンス ポインタのペアを使用してメンバーにアクセスすることによって実現できます。これは、基本的なメンバー型 (整数や文字列など) のみを持つ単純なクラスの場合は非常に簡単です。しかし、構造化されたフィールド タイプがある場合はどうなるでしょうか。

次に例を示します。

TIntArray = array [0..1] of Integer;

TPointArray = array [0..1] of Point;

TExampleClass = class
  private
    FPoint : TPoint;
    FAnotherClass : TAnotherClass;
    FIntArray : TIntArray;
    FPointArray : TPointArray;
  public  
    property Point : TPoint read FPoint write FPoint; 
    //.... and so on
end;

メンバーに簡単にアクセスするために、値の取得と設定、属性の取得、値のシリアル化/逆シリアル化などのインターフェイスを提供するメンバーノードのツリーを構築したいと考えています。

TMemberNode = class
  private
    FMember : TRttiMember;
    FParent : TMemberNode;
    FInstance : Pointer;
  public
    property Value : TValue read GetValue write SetValue; //uses FInstance
end;

したがって、最も重要なことは、値を取得/設定することです。これは、前に述べたように、TRttiField の GetValue および SetValue 関数を使用して行われます。

では、FPoint メンバーのインスタンスとは何でしょうか? Parent が TExample クラスのノードであり、インスタンスが既知でメンバーがフィールドであるとします。その場合、Instance は次のようになります。

FInstance := Pointer (Integer (Parent.Instance) + TRttiField (FMember).Offset);

しかし、レコード プロパティのインスタンスを知りたい場合はどうすればよいでしょうか。この場合、オフセットはありません。データへのポインターを取得するためのより良い解決策はありますか?

FAnotherClass メンバーの場合、インスタンスは次のようになります。

FInstance := Parent.Value.AsObject;  

これまでのところ、ソリューションは機能しており、情報を失うことなく、rtti または元の型を使用してデータ操作を行うことができます。

しかし、配列を操作する場合、事態はさらに難しくなります。特にポイントの 2 番目の配列。この場合、ポイントのメンバーのインスタンスを取得するにはどうすればよいですか?

4

3 に答える 3

13

TRttiField.GetValueフィールドのタイプが値のタイプである場合、コピーが取得されます。これは仕様によるものです。TValue.MakeWithoutCopyインターフェイスや文字列などの参照カウントを管理するためのものです。このコピー動作を回避するためではありません。TValueは意図的にVariantの ByRef 動作を模倣するようには設計されていません。この場合、(たとえば) 内のスタック オブジェクトへの参照がTValue発生する可能性があり、古いポインターのリスクが高まります。また、直感に反します。と言うときGetValueは、参照ではなく値を期待する必要があります。

値型の値が他の構造内に格納されている場合に、値型の値を操作する最も効率的な方法は、一歩下がって別のレベルの間接性を追加することTValueです。アイテムへのパスに沿ったすべての中間値型のステップを直接操作するのではなく、オフセットを計算します。 .

これはかなり簡単にカプセル化できます。TLocation私は過去 1 時間ほどかけて、 RTTI を使用してこれを行う小さな記録を書き上げました。

type
  TLocation = record
    Addr: Pointer;
    Typ: TRttiType;
    class function FromValue(C: TRttiContext; const AValue: TValue): TLocation; static;
    function GetValue: TValue;
    procedure SetValue(const AValue: TValue);
    function Follow(const APath: string): TLocation;
    procedure Dereference;
    procedure Index(n: Integer);
    procedure FieldRef(const name: string);
  end;

function GetPathLocation(const APath: string; ARoot: TLocation): TLocation; forward;

{ TLocation }

type
  PPByte = ^PByte;

procedure TLocation.Dereference;
begin
  if not (Typ is TRttiPointerType) then
    raise Exception.CreateFmt('^ applied to non-pointer type %s', [Typ.Name]);
  Addr := PPointer(Addr)^;
  Typ := TRttiPointerType(Typ).ReferredType;
end;

procedure TLocation.FieldRef(const name: string);
var
  f: TRttiField;
begin
  if Typ is TRttiRecordType then
  begin
    f := Typ.GetField(name);
    Addr := PByte(Addr) + f.Offset;
    Typ := f.FieldType;
  end
  else if Typ is TRttiInstanceType then
  begin
    f := Typ.GetField(name);
    Addr := PPByte(Addr)^ + f.Offset;
    Typ := f.FieldType;
  end
  else
    raise Exception.CreateFmt('. applied to type %s, which is not a record or class',
      [Typ.Name]);
end;

function TLocation.Follow(const APath: string): TLocation;
begin
  Result := GetPathLocation(APath, Self);
end;

class function TLocation.FromValue(C: TRttiContext; const AValue: TValue): TLocation;
begin
  Result.Typ := C.GetType(AValue.TypeInfo);
  Result.Addr := AValue.GetReferenceToRawData;
end;

function TLocation.GetValue: TValue;
begin
  TValue.Make(Addr, Typ.Handle, Result);
end;

procedure TLocation.Index(n: Integer);
var
  sa: TRttiArrayType;
  da: TRttiDynamicArrayType;
begin
  if Typ is TRttiArrayType then
  begin
    // extending this to work with multi-dimensional arrays and non-zero
    // based arrays is left as an exercise for the reader ... :)
    sa := TRttiArrayType(Typ);
    Addr := PByte(Addr) + sa.ElementType.TypeSize * n;
    Typ := sa.ElementType;
  end
  else if Typ is TRttiDynamicArrayType then
  begin
    da := TRttiDynamicArrayType(Typ);
    Addr := PPByte(Addr)^ + da.ElementType.TypeSize * n;
    Typ := da.ElementType;
  end
  else
    raise Exception.CreateFmt('[] applied to non-array type %s', [Typ.Name]);
end;

procedure TLocation.SetValue(const AValue: TValue);
begin
  AValue.Cast(Typ.Handle).ExtractRawData(Addr);
end;

このタイプは、RTTI を使用して値内の場所をナビゲートするために使用できます。少し使いやすくし、書くのが少し楽しくなるように、パーサーも作成しました -Followメソッド:

function GetPathLocation(const APath: string; ARoot: TLocation): TLocation;

  { Lexer }

  function SkipWhite(p: PChar): PChar;
  begin
    while IsWhiteSpace(p^) do
      Inc(p);
    Result := p;
  end;

  function ScanName(p: PChar; out s: string): PChar;
  begin
    Result := p;
    while IsLetterOrDigit(Result^) do
      Inc(Result);
    SetString(s, p, Result - p);
  end;

  function ScanNumber(p: PChar; out n: Integer): PChar;
  var
    v: Integer;
  begin
    v := 0;
    while (p >= '0') and (p <= '9') do
    begin
      v := v * 10 + Ord(p^) - Ord('0');
      Inc(p);
    end;
    n := v;
    Result := p;
  end;

const
  tkEof = #0;
  tkNumber = #1;
  tkName = #2;
  tkDot = '.';
  tkLBracket = '[';
  tkRBracket = ']';

var
  cp: PChar;
  currToken: Char;
  nameToken: string;
  numToken: Integer;

  function NextToken: Char;
    function SetToken(p: PChar): PChar;
    begin
      currToken := p^;
      Result := p + 1;
    end;
  var
    p: PChar;
  begin
    p := cp;
    p := SkipWhite(p);
    if p^ = #0 then
    begin
      cp := p;
      currToken := tkEof;
      Exit(currToken);
    end;

    case p^ of
      '0'..'9':
      begin
        cp := ScanNumber(p, numToken);
        currToken := tkNumber;
      end;

      '^', '[', ']', '.': cp := SetToken(p);

    else
      cp := ScanName(p, nameToken);
      if nameToken = '' then
        raise Exception.Create('Invalid path - expected a name');
      currToken := tkName;
    end;

    Result := currToken;
  end;

  function Describe(tok: Char): string;
  begin
    case tok of
      tkEof: Result := 'end of string';
      tkNumber: Result := 'number';
      tkName: Result := 'name';
    else
      Result := '''' + tok + '''';
    end;
  end;

  procedure Expect(tok: Char);
  begin
    if tok <> currToken then
      raise Exception.CreateFmt('Expected %s but got %s', 
        [Describe(tok), Describe(currToken)]);
  end;

  { Semantic actions are methods on TLocation }
var
  loc: TLocation;

  { Driver and parser }

begin
  cp := PChar(APath);
  NextToken;

  loc := ARoot;

  // Syntax:
  // path ::= ( '.' <name> | '[' <num> ']' | '^' )+ ;;

  // Semantics:

  // '<name>' are field names, '[]' is array indexing, '^' is pointer
  // indirection.

  // Parser continuously calculates the address of the value in question, 
  // starting from the root.

  // When we see a name, we look that up as a field on the current type,
  // then add its offset to our current location if the current location is 
  // a value type, or indirect (PPointer(x)^) the current location before 
  // adding the offset if the current location is a reference type. If not
  // a record or class type, then it's an error.

  // When we see an indexing, we expect the current location to be an array
  // and we update the location to the address of the element inside the array.
  // All dimensions are flattened (multiplied out) and zero-based.

  // When we see indirection, we expect the current location to be a pointer,
  // and dereference it.

  while True do
  begin
    case currToken of
      tkEof: Break;

      '.':
      begin
        NextToken;
        Expect(tkName);
        loc.FieldRef(nameToken);
        NextToken;
      end;

      '[':
      begin
        NextToken;
        Expect(tkNumber);
        loc.Index(numToken);
        NextToken;
        Expect(']');
        NextToken;
      end;

      '^':
      begin
        loc.Dereference;
        NextToken;
      end;

    else
      raise Exception.Create('Invalid path syntax: expected ".", "[" or "^"');
    end;
  end;

  Result := loc;
end;

型の例と、Pそれを操作するルーチン ( ) を次に示します。

type
  TPoint = record
    X, Y: Integer;
  end;
  TArr = array[0..9] of TPoint;

  TFoo = class
  private
    FArr: TArr;
    constructor Create;
    function ToString: string; override;
  end;

{ TFoo }

constructor TFoo.Create;
var
  i: Integer;
begin
  for i := Low(FArr) to High(FArr) do
  begin
    FArr[i].X := i;
    FArr[i].Y := -i;
  end;
end;

function TFoo.ToString: string;
var
  i: Integer;
begin
  Result := '';
  for i := Low(FArr) to High(FArr) do
    Result := Result + Format('(%d, %d) ', [FArr[i].X, FArr[i].Y]);
end;

procedure P;
var
  obj: TFoo;
  loc: TLocation;
  ctx: TRttiContext;
begin
  obj := TFoo.Create;
  Writeln(obj.ToString);

  ctx := TRttiContext.Create;

  loc := TLocation.FromValue(ctx, obj);
  Writeln(loc.Follow('.FArr[2].X').GetValue.ToString);
  Writeln(obj.FArr[2].X);

  loc.Follow('.FArr[2].X').SetValue(42);
  Writeln(obj.FArr[2].X); // observe value changed

  // alternate syntax, not using path parser, but location destructive updates
  loc.FieldRef('FArr');
  loc.Index(2);
  loc.FieldRef('X');
  loc.SetValue(24);
  Writeln(obj.FArr[2].X); // observe value changed again

  Writeln(obj.ToString);
end;

この原則は、他の型や Delphi 式の構文に拡張することも、破壊的な自己更新ではなくTLocation新しいインスタンスを返すように変更することも、フラットでない配列のインデックス付けをサポートすることもできます。TLocation

于 2010-05-11T12:43:05.670 に答える
4

この質問では、いくつかの概念と問題に触れています。まず、いくつかのレコード タイプといくつかのプロパティを混在させました。これを最初に処理したいと思います。次に、レコードがクラスのフィールドの一部である場合に、レコードの「左」フィールドと「上」フィールドを読み取る方法について簡単な情報を提供します...次に、作成方法について提案しますこれは一般的に機能します。必要以上に説明するかもしれませんが、ここは真夜中で眠れません!

例:

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;

これが取引です。このコードを書くと、実行時に実行しているように見えることを実行します。

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

しかし、このコードは同じようには機能しません:

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

そのコードは次と同等であるためです。

var X:TMyClass;
var tmp:TPoint;

tmp := X.GetMyPoint;
tmp.Left := 7;

これを修正する方法は、次のようにすることです。

var X:TMyClass;
var P:TPoint;

P := X.MyPoint;
P.Left := 7;
X.MyPoint := P;

次に、RTTI で同じことを行いたいとします。「AnPoint:TPoint」フィールドと「MyPoint:TPoint」フィールドの両方で RTTI を取得できます。RTTI を使用すると、本質的に関数を使用して値を取得するため、両方で「ローカル コピーを作成し、変更し、書き戻す」手法を使用する必要があります (X.MyPoint の例と同じ種類のコード)。

RTTI でそれを行う場合、常に「ルート」(TExampleClass インスタンスまたは TMyClass インスタンス)から開始し、一連の Rtti GetValue および SetValue メソッド以外は何も使用せずに、ディープ フィールドの値を取得したり、値を設定したりします。同じディープフィールド。

次のものがあると仮定します。

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

これをエミュレートしたい:

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

私たちはそれをステップに分けて、これを目指しています:

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

RTTI で実行したいので、何にでも機能するようにしたいので、「TPoint」タイプは使用しません。予想どおり、最初にこれを行います。

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;

次のステップでは、GetReferenceToRawData を使用して、V:TValue に隠されている TPoint レコードへのポインタを取得します (レコードであるという事実を除けば、何も知らないふりをしているレコードです)。そのレコードへのポインターを取得したら、SetValue メソッドを呼び出して、その "7" をレコード内に移動できます。

LeftFieldRtti.SetValue(V.GetReferenceToRawData, 7);

これで大体です。TValue を X:TMyClass に戻すだけです。

AnPointFieldRtti.SetValue(X, V)

頭から尾まで、次のようになります。

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

これは明らかに、任意の深さの構造を処理するように拡張できます。段階的に行う必要があることを覚えておいてください。最初の GetValue は「ルート」インスタンスを使用し、次の GetValue は前の GetValue 結果から抽出されたインスタンスを使用します。レコードには TValue.GetReferenceToRawData を使用でき、オブジェクトには TValue.AsObject を使用できます。

次のトリッキーなビットは、これを一般的な方法で行うことです。これにより、双方向のツリーのような構造を実装できます。そのためには、「ルート」からフィールドへのパスを TRttiMember 配列の形式で保存することをお勧めします (実際のランタイプ タイプを見つけるためにキャストが使用されるため、GetValue と SetValue を呼び出すことができます)。ノードは次のようになります。

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

GetValue の実装は非常に単純です。

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;

SetValue の実装は、もう少し複雑です。これらの (厄介な?) レコードのために、GetValue ルーチンが行うすべてのことを行う必要があります (最後の FMember 要素のインスタンス ポインターが必要なため)。その後、SetValue を呼び出すことができますが、その親の SetValue、次にその親の親の SetValue など...これは明らかに、必要な場合に備えて、すべての中間 TValue をそのまま保持する必要があることを意味します。だからここに行きます:

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;

...そして、これはそれである必要があります。ここでいくつかの警告:

  • これがコンパイルされるとは思わないでください! 私は実際、この投稿のすべてのコードを Web ブラウザーで書きました。技術的な理由から、Rtti.pas ソース ファイルにアクセスしてメソッドとフィールド名を検索することはできましたが、コンパイラにはアクセスできませんでした。
  • 特にプロパティが関係している場合は、このコードに非常に注意してください。プロパティはバッキング フィールドなしで実装できます。setter プロシージャは期待どおりに動作しない可能性があります。循環参照に遭遇するかもしれません!
于 2010-05-10T21:50:31.827 に答える
0

インスタンスポインタの仕組みを誤解しているようです。フィールドへのポインターを保存するのではなく、クラスまたはフィールドであるレコードへのポインターを保存します。オブジェクト参照は既にポインターであるため、キャストは必要ありません。レコードの場合、@ 記号を使用してレコードへのポインターを取得する必要があります。

ポインターと、そのフィールドを参照する TRttiField オブジェクトを取得したら、TRttiField で SetValue または GetValue を呼び出し、インスタンス ポインターを渡すと、すべてのオフセット計算が処理されます。

配列の特定のケースでは、GetValue は配列を表す TValue を提供します。必要に応じて、電話してこれをテストできTValue.IsArrayます。配列を表す TValue がある場合、 で配列の長さをTValue.GetArrayLength取得し、 で個々の要素を取得できますTValue.GetArrayElement

編集:クラス内のレコード メンバーを処理する方法は次のとおりです。

レコードもタイプであり、独自の RTTI を持っています。次のように「GetValue、modify、SetValue」を実行せずにそれらを変更できます。

procedure ModifyPoint(example: TExampleClass; newXValue, newYValue: integer);
var
  context: TRttiContext;
  value: TValue;
  field: TRttiField;
  instance: pointer;
  recordType: TRttiRecordType;
begin
  field := context.GetType(TExampleClass).GetField('FPoint');
  //TValue that references the TPoint
  value := field.GetValue(example);
  //Extract the instance pointer to the TPoint within your object
  instance := value.GetReferenceToRawData;
  //RTTI for the TPoint type
  recordType := context.GetType(value.TypeInfo) as TRttiRecordType;
  //Access the individual members of the TPoint
  recordType.GetField('X').SetValue(instance, newXValue);
  recordType.GetField('Y').SetValue(instance, newYValue);
end;

あなたが知らなかった部分はTValue.GetReferenceToRawDataのようです。これにより、オフセットの計算や整数へのポインターのキャストについて心配する必要なく、フィールドへのポインターが得られます。

于 2010-05-10T14:03:26.360 に答える