您正在触及这个问题的一些概念和问题。首先,您已经混合了一些记录类型和一些属性,我想先处理一下。然后我会给你一些关于如何读取记录的“左”和“顶部”字段的简短信息,当该记录是类中的一个字段的一部分时......然后我会给你一些关于如何制作的建议这项工作一般。我可能会解释得比需要的多一点,但是这里是午夜,我无法入睡!
例子:
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 元素的 Instance 指针),然后我们将能够调用 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 过程可能不会像您期望的那样。您可能会遇到循环引用!