您正在触及这个问题的一些概念和问题。首先你已经混合了一些记录类型和一些属性,我想先处理它。然后,我会告诉你一些关于如何阅读记录的“左”和“顶”字段的简短信息,当该记录是班级中某个字段的一部分时......那么我会给你提供关于如何制作这项工作一般。我可能会稍微解释一下,但这是午夜,我无法入睡!
例子:
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进行操作时,我们总是从“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(要知道,一个我们可以假装什么都不知道关于 - 除了它是一个RECORD的事实)。一旦我们获得了一条指向该记录的指针,我们可以调用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的对象!
下一个棘手的问题是以通用的方式做到这一点,所以你可以实现你的双向树状结构。为此,我建议以TRttiMember数组的形式存储从“root”到您的字段的路径(然后将使用铸造来查找实际的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元素),那么我们将能够调用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-11 13:36:09
异议(1):这只适用于字段,因为这取决于在原始结构(记录/类)中取得字段地址的能力。只有Fields有实际的内存支持,属性不支持,所以它有点有限 - 我承认这不是一个大问题,特别是如果这只能在开发者控制下的一个特定应用程序中工作。 – 2010-05-12 05:33:02
...和我的+1是因为我发现了TValue.Make和TValue.ExtractRawData真的有多聪明!他们很聪明,因为他们正确处理托管类型(字符串,托管记录,接口)。 – 2010-05-12 06:04:23