您正在觸及這個問題的一些概念和問題。首先你已經混合了一些記錄類型和一些屬性,我想先處理它。然後,我會告訴你一些關於如何閱讀記錄的「左」和「頂」字段的簡短信息,當該記錄是班級中某個字段的一部分時......那麼我會給你提供關於如何製作這項工作一般。我可能會稍微解釋一下,但這是午夜,我無法入睡!
例子:
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