四:接口的RTTI
接口也可以應(yīng)用RTTI,其原理和內(nèi)容都是和對(duì)象的相差無幾,在runtime type information中就提到了在接口聲明的時(shí)候加上編譯開關(guān){$M+}就可以為接口產(chǎn)生RTTI,由于接口中所有的方法和函數(shù)都是published的,所以接口RTTI中包含了接口的所有方法和函數(shù),同時(shí)只要是父接口聲明的時(shí)候有{$M+},則從其派生的接口中也都有RTTI,這點(diǎn)和對(duì)象是一致的,與對(duì)象TPersistent相對(duì)應(yīng)的是IInvokable 。而且,對(duì)于接口,并不需要{$METHODINFO ON},僅僅只是{$M+}就可以將所有的函數(shù)信息,包括返回值、參數(shù)等都編譯進(jìn)RTTI,這些信息的結(jié)構(gòu)和聲明在單元IntfInfo中。
下面這個(gè)方法是獲得接口的RTTI,呵呵,打印出來像不像代碼?
function TForm1.GetIntfRTTI(APInfo: Pointer): String;
var
IntfMetaData: TIntfMetaData;
I, J: Integer;
RoutinePrefix, Params: String;
StrList: TStringList;
begin
StrList:= TStringList.Create;
try
IntfInfo.GetIntfMetaData(APInfo, IntfMetaData);
with StrList do
begin
Clear;
Append(Format('Unit %s', [IntfMetaData.UnitName]));
Append(Format(' %s = interface(%s)', [IntfMetaData.Name,IntfMetaData.AncInfo^.Name]));
Append(Format(' [%s]', [GUIDToString(IntfMetaData.IID)]));
for I := 0 to Length(IntfMetaData.MDA)-1 do
begin
Params:= '';
for J := 0 to IntfMetaData.MDA[I].ParamCount-1 do
begin
Params:= Params+ IntfMetaData.MDA[I].Params[J].Name+':'+IntfMetaData.MDA[I].Params[J].Info^.Name;
if (J+1)<IntfMetaData.MDA[I].ParamCount then
Params:= Params+';';
end;
if IntfMetaData.MDA[I].ResultInfo<> nil then
RoutinePrefix:= 'function '+IntfMetaData.MDA[I].Name+'(%s):'+
IntfMetaData.MDA[I].ResultInfo^.Name+'; '+CallingConventionName[IntfMetaData.MDA[I].CC]
else
RoutinePrefix:= 'procedure '+
IntfMetaData.MDA[I].Name+'(%s);'+CallingConventionName[IntfMetaData.MDA[I].CC];
Append(' '+Format(RoutinePrefix, [Params]));
end;
Append(' end;');
end;
Result:= StrList.Text;
finally
StrList.Free;
end;
end;
該方法調(diào)用的形式如下:
MemoIRTTI.Lines.Text:= GetIntfRTTI(TypeInfo(ITest));
通過函數(shù)TypeInfo獲得接口的類型信息。
對(duì)于接口函數(shù),也有類似的方法來驅(qū)動(dòng),在單元Invoker中TInterfaceInvoker.Invoke方法(參數(shù):Obj,接口的實(shí)現(xiàn)對(duì)象;IntfMD,接口的運(yùn)行時(shí)信息,通過單元IntfInfo中函數(shù)GetIntfMetaData獲得;MethNum,所要驅(qū)動(dòng)函數(shù)在TIntfMetaData.MDA數(shù)組中的index,通過單元IntfInfo中函數(shù)GetMethNum獲得;Context,用以傳遞參數(shù)和返回值,其本身是一個(gè)列表,包含了實(shí)參地址和返回值地址)。
下面就是用Invoke來驅(qū)動(dòng)接口方法的代碼,接口ITest和實(shí)現(xiàn)類TTest如前所聲明:
驅(qū)動(dòng)函數(shù)ShowMsg,這個(gè)依然是最簡(jiǎn)單,沒有參數(shù),沒有返回值,但仍然需要?jiǎng)?chuàng)建對(duì)象TInvContext。注意,GetMethNum最后一個(gè)參數(shù)有默認(rèn)值,該參數(shù)的表示的意義是,當(dāng)函數(shù)有overload的時(shí)候,需要指明函數(shù)參數(shù)的個(gè)數(shù)用以確定需要獲得是哪一個(gè)重載函數(shù),若沒有overload,則為-1.
var
IntfMetaData: TIntfMetaData;
InvC: TInvContext;
MIndex: Integer;
begin
IntfInfo.GetIntfMetaData(TypeInfo(ITest), IntfMetaData, True);
InvC:= TInvContext.Create;
try
MIndex:= GetMethNum(IntfMetaData, 'ShowMsg');
InvC.SetMethodInfo(IntfMetaData.MDA[MIndex]);
FIntfInvoke.Invoke(FTest, IntfMetaData, MIndex, InvC);
finally
InvC.Free;
end;
end;
驅(qū)動(dòng)函數(shù)AddStr,這里有兩個(gè)參數(shù)和一個(gè)返回值,傳實(shí)參的時(shí)候,不需要填入隱式參數(shù)Self,TInvContext中填充的是包含實(shí)參以及返回值的變量的地址,填充序號(hào)從0開始。
var
IntfMetaData: TIntfMetaData;
InvC: TInvContext;
MIndex: Integer;
P1: String;
P2: Integer;
MResult: String;
begin
IntfInfo.GetIntfMetaData(TypeInfo(ITest), IntfMetaData, True);
InvC:= TInvContext.Create;
try
MIndex:= GetMethNum(IntfMetaData, 'AddStr');
InvC.SetMethodInfo(IntfMetaData.MDA[MIndex]);
P1:= 'BBB';
P2:= 3;
InvC.SetParamPointer(0, @P1);
InvC.SetParamPointer(1, @P2);
InvC.SetResultPointer(@MResult);
FIntfInvoke.Invoke(FTest, IntfMetaData, MIndex, InvC);
ShowMessage(MResult);
finally
InvC.Free;
end;
end;
驅(qū)動(dòng)函數(shù)IncNum,這里函數(shù)原型在聲明的時(shí)候,參數(shù)為var,但是在調(diào)用的時(shí)候,卻與其他的形式并沒有什么區(qū)別,其中原因在于由于實(shí)參填充的是變量的地址,至于實(shí)參變量是否需要被改寫,由函數(shù)invoke內(nèi)部判斷。
var
IntfMetaData: TIntfMetaData;
InvC: TInvContext;
MIndex: Integer;
P1: Integer;
begin
IntfInfo.GetIntfMetaData(TypeInfo(ITest), IntfMetaData, True);
InvC:= TInvContext.Create;
try
MIndex:= GetMethNum(IntfMetaData, 'IncNum');
InvC.SetMethodInfo(IntfMetaData.MDA[MIndex]);
P1:= 3;
InvC.SetParamPointer(0, @P1);
FIntfInvoke.Invoke(FTest, IntfMetaData, MIndex, InvC);
ShowMessage(IntToStr(P1));
finally
InvC.Free;
end;
end;
驅(qū)動(dòng)函數(shù)GetName,有意思的地方來了。還記得在ObjectInvoke中,不能驅(qū)動(dòng)參數(shù)為對(duì)象的這種函數(shù)嗎?但是在這里卻可以了,因?yàn)檫@里傳遞實(shí)參不再是使用Variant開放數(shù)組,就不受傳參類型的限制了。只是,這里參數(shù)類型檢查仍然不是很嚴(yán)謹(jǐn),盡管這個(gè)函數(shù)是VCL實(shí)現(xiàn),例如,對(duì)于函數(shù)GetName參數(shù)要求傳入的是TComponent類型,但是如果實(shí)參填入的是一個(gè)TPersistent的對(duì)象,仍然是可以通過類型檢查而調(diào)用到這個(gè)函數(shù)的,只是該函數(shù)內(nèi)部在訪問Component.Name屬性的時(shí)候,才會(huì)拋錯(cuò)。
var
IntfMetaData: TIntfMetaData;
InvC: TInvContext;
MIndex: Integer;
MResult: String;
begin
IntfInfo.GetIntfMetaData(TypeInfo(ITest), IntfMetaData, True);
InvC:= TInvContext.Create;
try
MIndex:= GetMethNum(IntfMetaData, 'GetName');
InvC.SetMethodInfo(IntfMetaData.MDA[MIndex]);
InvC.SetParamPointer(0, @Self);
InvC.SetResultPointer(@MResult);
FIntfInvoke.Invoke(FTest, IntfMetaData, MIndex, InvC);
ShowMessage(MResult);
finally
InvC.Free;
end;
end;
有一個(gè)小技巧,枚舉出程序中所有RegisterClass的類。
var
FFinder: TClassFinder;
begin
FFinder:= TClassFinder.Create();
try
Result:= FFinder.GetClasses(Proc);
finally
FFinder.Free;
end;
其中TClassFinder聲明在Classes單元中,Proc是回調(diào)函數(shù),類型為TGetClass = procedure (AClass:TPersistentClass) of object,在遍歷內(nèi)部的注冊(cè)列表的時(shí)候循環(huán)調(diào)用回調(diào)函數(shù),在外部實(shí)現(xiàn)回調(diào)函數(shù)的時(shí)候,判斷每次傳入進(jìn)來的類是否是所需要,如果需要?jiǎng)t記錄保存下來。
關(guān)于RTTI在動(dòng)態(tài)連接庫中的使用
DLL不能傳遞RTTI,所以在DLL之間、或DLL與exe之間傳遞對(duì)象的時(shí)候,不能顯示和使用對(duì)象的RTTI。如果需要在傳遞對(duì)象的時(shí)候同時(shí)擁有其RTTI,則需要使用package來替代DLL,因?yàn)閜ackage之間、package與exe之間是共享RTTI。
最后是一個(gè)關(guān)于代碼格式的問題,很多年前我和別人打過一個(gè)賭,下面兩種書寫格式:
if then begin
end
和
if then
begin
end
哪種更加規(guī)范。于是我在VCL中搜索,發(fā)現(xiàn)其實(shí)兩種書寫方式都有,但是明顯后者多得多,究其原因是在Menu->Tools->Editor
Options...->Source Options->Edit Code
Templates中聲明的代碼模板格式是后一種,在輸入的時(shí)候只需要鍵入ifb
組合鍵Ctrl+j 就可以生成了,習(xí)慣后,使用起來是不是很快捷呢?
以上說明皆是基于Delphi 6/7版本,后繼版本內(nèi)容略有差異,但原理大體相當(dāng)。