この質問では、いくつかの概念と問題に触れています。まず、いくつかのレコード タイプといくつかのプロパティを混在させました。これを最初に処理したいと思います。次に、レコードがクラスのフィールドの一部である場合に、レコードの「左」フィールドと「上」フィールドを読み取る方法について簡単な情報を提供します...次に、作成方法について提案しますこれは一般的に機能します。必要以上に説明するかもしれませんが、ここは真夜中で眠れません!
例:
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 要素のインスタンス ポインターが必要なため)。その後、SetValue を呼び出すことができますが、その親の 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 プロシージャは期待どおりに動作しない可能性があります。循環参照に遭遇するかもしれません!