Rtti доступ к полям и свойствам в сложных структурах данных

как уже обсуждалось в Rtti манипуляция данными и согласованность в Delphi 2010 согласованность между исходными данными и значениями 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;

поэтому самое главное-получить / установить значения, что делается, как указано выше, с помощью функций GetValue и SetValue TRttiField.

Итак, что такое экземпляр для членов FPoint? Предположим, Parent-это узел для класса TExample, где экземпляр известен и member-это поле, тогда экземпляр будет:

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

но что, если я хочу знать экземпляр для свойства записи? В этом случае смещения нет. Итак, есть ли лучшее решение для получения указателя на данные?

для члена FAnotherClass экземпляр будет:

FInstance := Parent.Value.AsObject;  

до сих пор решение работает, и манипуляция данными может быть выполнена с помощью rtti или исходных типов, без потери информации.

но все становится сложнее, когда работа с массивами. Особенно второй массив точек. Как я могу получить экземпляр для членов точек в этом случае?

3 ответов


TRttiField.GetValue где тип поля является типом значения, вы получаете копию. Это по замыслу. TValue.MakeWithoutCopy предназначен для управления ссылками на такие вещи, как интерфейсы и строки; это не для избежания этого поведения копирования. TValue намеренно не предназначен для имитации Variantповедение ByRef, где вы можете получить ссылки на (например) объекты стека внутри TValue, увеличивая риск несвежих указателей. Это также будет противоречить интуиции; когда вы говорите GetValue, вы должны ожидать значение, а не ссылка.

вероятно, самый эффективный способ манипулировать значениями типов значений, когда они хранятся внутри других структур, - это отступить и добавить еще один уровень косвенности: путем вычисления смещений, а не работы с TValue непосредственно для всех промежуточных введенных значений шагов по пути к элементу.

это может быть инкапсулировано довольно тривиально. Я провел последний час или около того пишу немного TLocation запись, которая использует 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. Чтобы сделать его немного проще в использовании и немного веселее для меня писать, я также написал парсер - the 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 экземпляров, а чем разрушительные само-обновления, или не-плоский индексировать массива могут быть поддержаны, ЕТК.


вы касаетесь нескольких концепций и проблем с этим вопросом. Прежде всего, вы смешали некоторые типы записей и некоторые свойства, и я хотел бы сначала разобраться с этим. Затем я дам вам краткую информацию о том, как читать "левые" и "Верхние" поля записи, когда эта запись является частью поля в классе... Тогда я дам вам советы о том, как сделать эту работу в общем. Я, наверное, объясню немного больше, чем требуется, но сейчас полночь, и я не могу. спать!

пример:

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. Вы можете получить RTTI для поля "AnPoint:TPoint" и для поля" MyPoint:TPoint". Поскольку с помощью RTTI вы по существу используете функцию для получения значения, вам нужно будет использовать метод "сделать локальную копию, изменить, записать обратно" с обоими (тот же код, что и для примера X. MyPoint).

при выполнении этого с RTTI мы всегда будем начинать с "root" (экземпляр 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, чтобы получить указатель на запись TPoint скрытый в V: TValue (вы знаете, тот, о котором мы притворяемся, что ничего не знаем, кроме того факта, что это запись). Как только мы получим указатель на эту запись, мы можем вызвать метод 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 использует экземпляр "root", затем следующий GetValue использует экземпляр, извлеченный из предыдущего результата GetValue. Для записи мы можем использовать TValue.GetReferenceToRawData, для объектов мы можем использовать TValue.AsObject!

следующий сложный бит делает это общим способом, поэтому вы можете реализовать свою двунаправленную древовидную структуру. Для этого я бы рекомендовал сохранить путь от "root" до вашего поля в виде Trttimember array (затем литье будет использоваться для поиска фактического типа runtype, поэтому мы можем вызвать 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 element), тогда мы сможем вызвать 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;

... И это должно быть оно. Теперь некоторые предупреждения:

  • не ожидайте, что это будет компилироваться! Я фактически написал каждый бит кода в этом сообщении в веб-браузере. Для технических причины, по которым у меня был доступ к Rtti.исходный файл pas для поиска имен методов и полей, но у меня нет доступа к компилятору.
  • я был бы очень осторожен с этим кодом, особенно если речь идет о свойствах. Свойство может быть реализовано без резервного поля, процедура setter может не делать то, что вы ожидаете. Вы можете столкнуться с циклическими ссылками!

вы, похоже, неправильно понимаете, как работает указатель экземпляра. Вы не храните указатель на поле, вы храните указатель на класс или запись, что это поле. Ссылки на объекты уже являются указателями, поэтому там не требуется кастинг. Для записей необходимо получить указатель на них с символом@.

как только у вас есть указатель и объект TRttiField, который ссылается на это поле, вы можете вызвать SetValue или GetValue на TRttiField и передать указатель экземпляра, и он заботится обо всех вычислениях смещения для вас.

в конкретном случае массивов GetValue он даст вам TValue, который представляет массив. Вы можете проверить это, позвонив TValue.IsArray Если вы хотите. Когда у вас есть TValue, представляющий массив, вы можете получить длину массива с помощью TValue.GetArrayLength и получить отдельные элементы с TValue.GetArrayElement.

EDIT: вот как обращаться с членами записи в класс.

записи тоже являются типами, и у них есть 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. Это даст вам указатель на поле, без необходимости беспокоиться о вычислении смещений и указателей на целые числа.