正如在
Rtti data manipulation and consistency in Delphi 2010中已经讨论的那样,通过使用一对TRttiField和一个实例指针访问成员,可以达到原始数据和rtti值之间的一致性.在仅具有基本成员类型(例如整数或字符串)的简单类的情况下,这将是非常容易的.
但是,如果我们有结构化的字段类型呢?
但是,如果我们有结构化的字段类型呢?
这是一个例子:
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;
为了方便成员访问,我想构建一个成员节点树,它提供了获取和设置值,获取属性,序列化/反序列化值等的接口.
TMemberNode = class private FMember : TRttiMember; FParent : TMemberNode; FInstance : Pointer; public property Value : TValue read GetValue write SetValue; //uses FInstance end;
因此,最重要的是获取/设置值,这是通过使用TRttiField的GetValue和SetValue函数完成的 – 如前所述.
那么FPoint成员的实例是什么?假设Parent是TExample类的Node,其中实例是已知的,成员是一个字段,那么Instance将是:
FInstance := Pointer (Integer (Parent.Instance) + TRttiField (FMember).Offset);
但是,如果我想知道记录属性的实例呢?在这种情况下没有偏移.那么有一个更好的解决方案来获取指向数据的指针吗?
对于FAnotherClass成员,实例将是:
FInstance := Parent.Value.AsObject;
到目前为止,该解决方案仍然有效,并且可以使用rtti或原始类型完成数据操作,而不会丢失信息.
但是在处理数组时事情会变得更难.特别是第二个点数组.在这种情况下,如何获得积分成员的实例?
解决方法
TRttiField.GetValue,其中字段的类型是值类型,可以获得副本.这是设计的. TValue.MakeWithoutCopy用于管理接口和字符串等内容的引用计数;它不是为了避免这种复制行为. TValue故意不是为了模仿Variant的ByRef行为而设计的,在这种行为中,您最终可能会引用(例如)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实例而不是破坏性自我更新,或者可以支持非平面数组索引等.