Как уже обсуждено в манипулировании данными 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 или исходных типов, не теряя информацию.
Но вещи становятся более твердыми при работе с массивами. Особенно второй массив Точек. Как я могу получить экземпляр для членов точек в этом случае?
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
экземпляры, а не деструктивные самообновления, или может поддерживаться не плоская индексация массивов, и т.д.
Похоже, вы неправильно понимаете принцип работы указателя экземпляра. Вы не храните указатель на поле, вы храните указатель на класс или запись, полем которой оно является. Ссылки на объекты уже являются указателями, поэтому приведение не требуется. Для записей необходимо получить указатель на них с помощью символа @.
Когда у вас есть указатель и объект 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. Это даст вам указатель на поле, без необходимости беспокоиться о вычислении смещений и приведении указателей к целым числам.
В этом вопросе вы затрагиваете несколько концепций и проблем. Прежде всего, вы смешали некоторые типы записей и некоторые свойства, и я бы хотел сначала разобраться с этим. Затем я дам вам краткую информацию о том, как читать поля "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;
... И это должно быть все. Теперь несколько предупреждений: