Поля доступа 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;

Для легкого доступа участников я хочу к buil дерево членских узлов, которое обеспечивает интерфейс для того, чтобы получить и установить значения, получая атрибуты, сериализируя/десериализовывая значения и так далее.

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

Таким образом, самая важная вещь получает/устанавливает значения, который сделан - как указано прежде - при помощи функций GetValue и SetValue TRttiField.

Таким образом, каков Экземпляр для участников FPoint? Скажем, Родителем является Узел для класса TExample, где экземпляр известен, и участник является полем, затем Экземпляр был бы:

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

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

Для члена FAnotherClass Экземпляр был бы:

FInstance := Parent.Value.AsObject;  

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

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

7
задан Community 23 May 2017 в 12:14
поделиться

3 ответа

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

13
ответ дан 6 December 2019 в 11:46
поделиться

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

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

0
ответ дан 6 December 2019 в 11:46
поделиться

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

Пример:

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

Следующий сложный момент - сделать это общим способом, чтобы вы могли реализовать двунаправленную древовидную структуру. Для этого я бы рекомендовал хранить путь от "корня" до вашего поля в виде массива TRttiMember (затем будет использовано приведение для нахождения фактического типа 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 (потому что нам нужен указатель Instance для самого последнего элемента FMember), затем мы сможем вызвать 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 может сделать не то, что вы ожидаете. Вы можете столкнуться с круговыми ссылками!
4
ответ дан 6 December 2019 в 11:46
поделиться
Другие вопросы по тегам:

Похожие вопросы: