本文內容包括: 1 什麼是回調 2 過程語言中的回調(C) 2.2 參數路由規則 3 回調在分散式運算中的應用(CORBA) 關於作者 軟體模組之間總是存在著一定的介面,從調用方式上,可以把他們分為三類:同步調用、回調和非同步呼叫。同步調用是一種阻塞式調用,調用方要等待對方執行完畢才返回,它是一種單向調用;回調是一種雙向調用模式,也就是說,被呼叫者在介面被調用時也會調用對方的介面;非同步呼叫是一種類似訊息或事件的機制,不過它的調用方向剛好相反,介面的服務在收到某種訊息或發生某種事件時,會主動通知客戶方(即調用客戶方的介面)。回調和非同步呼叫的關係非常緊密,通常我們使用回調來實現非同步訊息的註冊,通過非同步呼叫來實現訊息的通知。同步調用是三者當中最簡單的,而回調又常常是非同步呼叫的基礎,因此,下面我們著重討論回調機制在不同軟體架構中的實現。 1 什麼是回調 軟體模組之間總是存在著一定的介面,從調用方式上,可以把他們分為三類:同步調用、回調和非同步呼叫。同步調用是一種阻塞式調用,調用方要等待對方執行完畢才返回,它是一種單向調用;回調是一種雙向調用模式,也就是說,被呼叫者在介面被調用時也會調用對方的介面;非同步呼叫是一種類似訊息或事件的機制,不過它的調用方向剛好相反,介面的服務在收到某種訊息或發生某種事件時,會主動通知客戶方(即調用客戶方的介面)。回調和非同步呼叫的關係非常緊密,通常我們使用回調來實現非同步訊息的註冊,通過非同步呼叫來實現訊息的通知。同步調用是三者當中最簡單的,而回調又常常是非同步呼叫的基礎,因此,下面我們著重討論回調機制在不同軟體架構中的實現。
對於不同類型的語言(如結構化語言和對象語言)、平台(Win32、JDK)或構架(CORBA、DCOM、WebService),客戶和服務的互動除了同步方式以外,都需要具備一定的非同步通知機制,讓服務方(或介面提供方)在某些情況下能夠主動通知客戶,而回調是實現非同步一個最簡捷的途徑。 對於一般的結構化語言,可以通過回呼函數來實現回調。回呼函數也是一個函數或過程,不過它是一個由調用方自己實現,供被呼叫者使用的特殊函數。 在物件導向的語言中,回調則是通過介面或抽象類別來實現的,我們把實現這種介面的類成為回調類,回調類的對象成為回調對象。對於象C++或Object Pascal這些相容了過程特性的對象語言,不僅提供了回調對象、回調方法等特性,也能相容過程語言的回呼函數機制。 Windows平台的訊息機制也可以看作是回調的一種應用,我們通過系統提供的介面註冊訊息處理函數(即回呼函數),從而實現接收、處理訊息的目的。由於Windows平台的API是用C語言來構建的,我們可以認為它也是回呼函數的一個特例。 對於分布式組件代理體系CORBA,非同步處理有多種方式,如回調、事件服務、通知服務等。事件服務和通知服務是CORBA用來處理非同步訊息的標準服務,他們主要負責訊息的處理、派發、維護等工作。對一些簡單的非同步處理過程,我們可以通過回調機制來實現。 下面我們集中比較具有代表性的語言(C、Object Pascal)和架構(CORBA)來分析回調的實現方式、具體作用等。 2 過程語言中的回調(C) 2.1 函數指標 回調在C語言中是通過函數指標來實現的,通過將回呼函數的地址傳給被調函數從而實現回調。因此,要實現回調,必須首先定義函數指標,請看下面的例子: void Func(char *s);// 函數原型 void (*pFunc) (char *);//函數指標 可以看出,函數的定義和函數指標的定義非常類似。 一般的化,為了簡化函數指標類型的變數定義,提高程式的可讀性,我們需要把函數指標類型自訂一下。 typedef void(*pcb)(char *); 回呼函數可以象普通函數一樣被程式調用,但是只有它被當作參數傳遞給被調函數時才能稱作回呼函數。 被調函數的例子:
void GetCallBack(pcb callback) { /*do something*/ } 使用者在調用上面的函數時,需要自己實現一個pcb類型的回呼函數: void fCallback(char *s) { /* do something */ } 然後,就可以直接把fCallback當作一個變數傳遞給GetCallBack, GetCallBack(fCallback); |
如果賦了不同的值給該參數,那麼調用者將調用不同地址的函數。賦值可以發生在運行時,這樣使你能實現動態綁定。 2.2 參數路由規則 到目前為止,我們只討論了函數指標及回調而沒有去注意ANSI C/C++的編譯器規範。許多編譯器有幾種調用規範。如在Visual C++中,可以在函數類型前加_cdecl,_stdcall或者_pascal來表示其調用規範(預設為_cdecl)。C++ Builder也支援_fastcall調用規範。調用規範影響編譯器產生的給定函數名,參數傳遞的順序(從右至左或從左至右),堆棧清理責任(調用者或者被調用者)以及參數傳遞機制(堆棧,CPU寄存器等)。 將調用規範看成是函數類型的一部分是很重要的;不能用不相容的調用規範將地址賦值給函數指標。例如:
// 被調用函數是以int為參數,以int為傳回值 __stdcall int callee(int); // 調用函數以函數指標為參數 void caller( __cdecl int(*ptr)(int)); // 在p中企圖儲存被調用函數地址的非法操作 __cdecl int(*p)(int) = callee; // 出錯 |
指標p和callee()的類型不相容,因為它們有不同的調用規範。因此不能將被調用者的地址賦值給指標p,儘管兩者有相同的傳回值和參數列 2.3 應用舉例 C語言的標準庫函數中很多地方就採用了回呼函數來讓使用者定製處理過程。如常用的快速排序函數、二分搜尋函數等。 快速排序函數原型:
void qsort(void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *)); 二分搜尋函數原型: void *bsearch(const void *key, const void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *)); |
其中fcmp就是一個回呼函數的變數。 下面給出一個具體的例子:
#include <stdio.h> #include <stdlib.h> int sort_function( const void *a, const void *b); int list[5] = { 54, 21, 11, 67, 22 }; int main(void) { int x; qsort((void *)list, 5, sizeof(list[0]), sort_function); for (x = 0; x < 5; x++) printf("%i/n", list[x]); return 0; } int sort_function( const void *a, const void *b) { return *(int*)a-*(int*)b; } |
2.4 物件導向語言中的回調(Delphi) Dephi與C++一樣,為了保持與過程語言Pascal的相容性,它在引入物件導向機制的同時,保留了以前的結構化特性。因此,對回調的實現,也有兩種截然不同的模式,一種是結構化的函數回調模式,一種是物件導向的介面模式。 2.4.1 回呼函數 回呼函數類型定義:
type TCalcFunc=function (a:integer;b:integer):integer; |
按照回呼函數的格式自訂函數的實現,如
function Add(a:integer;b:integer):integer begin result:=a+b; end; function Sub(a:integer;b:integer):integer begin result:=a-b; end; |
回調的使用
| function Calc(calc:TcalcFunc;a:integer;b:integer):integer |
下面,我們就可以在我們的程式裡按照需要調用這兩個函數了
c:=calc(add,a,b);//c=a+b c:=calc(sub,a,b);//c=a-b |
2.4.2 回調對象 什麼叫回調對象呢,它具體用在哪些場合?首先,讓我們把它與回呼函數對比一下,回呼函數是一個定義了函數的原型,函數體則交由第三方來實現的一種Live App模式。要實現一個回呼函數,我們必須明確知道幾點:該函數需要那些參數,返回什麼類型的值。同樣,一個回調對象也是一個定義了對象介面,但是沒有具體實現的抽象類別(即介面)。要實現一個回調對象,我們必須知道:它需要實現哪些方法,每個方法中有哪些參數,該方法需要放回什麼值。 因此,在回調對象這種應用模式中,我們會用到介面。介面可以理解成一個定義好了但是沒有實現的類,它只能通過繼承的方式被別的類實現。Delphi中的介面和COM介面類似,所有的介面都繼承與IInterface(等同於IUnknow),並且要實現三個基本的方法QueryInterface, _AddRef, 和_Release。 定義一個介面
type IShape=interface(IInterface) procedure Draw; end |
實現回調類
type TRect=class(TObject,IShape) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public procedure Draw; end; type TRound=class(TObject,IShape) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public procedure Draw; end; |
使用回調對象
procedure MyDraw(shape:IShape); var shape:IShape; begin shape.Draw; end; |
如果傳入的對象為TRect,那麼畫矩形;如果為TRound,那麼就為圓形。使用者也可以按照自己的意圖來實現IShape介面,畫出自己的圖形:
MyDraw(Trect.Create); MyDraw(Tround.Create); |
2.4.3 回調方法 回調方法(Callback Method)可以看作是回調對象的一部分,Delphi對windows訊息的封裝就採用了回調方法這個概念。在有些場合,我們不需要按照給定的要求實現整個對象,而只要實現其中的一個方法就可以了,這是我們就會用到回調方法。 回調方法的定義如下:
TNotifyEvent = procedure(Sender: TObject) of object; TMyEvent=procedure(Sender:Tobject;EventId:Integer) of object; |
TNotifyEvent 是Delphi中最常用的回調方法,表單、控制項的很多事件,如單擊事件、關閉事件等都是採用了TnotifyEvent。回調方法的變數一般通過事件屬性的方式來定義,如TCustomForm的建立事件的定義:
| property OnCreate: TNotifyEvent read FOnCreate write FOnCreate stored IsForm; |
我們通過給事件屬性變數賦值就可以定製事件處理器。 使用者定義對象(包含回調方法的對象):
type TCallback=Class procedure ClickFunc(sender:TObject); end; procedure Tcallback.ClickFunc(sender:TObject); begin showmessage('the caller is clicked!'); end; |
表單對象:
type TCustomFrm=class(TForm) public procedure RegisterClickFunc(cb:procedure(sender:Tobject) of object); end; procedure TcustomFrm..RegisterClickFunc(cb:TNotifyEvent); begin self.OnClick=cb; end; |
使用方法:
var frm:TcustomFrm; begin frm:=TcustomFrm.Create(Application); frm.RegisterClickFunc(Tcallback.Create().ClickFunc); end; |
3 回調在分散式運算中的應用(CORBA) 3.1 回調介面模型 CORBA的訊息傳遞機制有很多種,比如回調介面、事件服務和通知服務等。回調介面的原理很簡單,CORBA客戶和伺服器都具有雙重角色,即充當伺服器也是客戶客戶。 回調介面的反向調用與正向調用往往是同時進行的,如果服務端多次調用該回調介面,那麼這個回調介面就變成非同步介面了。因此,回調介面在CORBA中常常充當事件註冊的用途,用戶端調用該註冊函數時,客戶函數就是回呼函數,在此後的調用中,由於不需要用戶端的主動參與,該函數就是實現了一種非同步機制。 從CORBA規範我們知道,一個CORBA介面在服務端和用戶端有不同的表現形式,在用戶端一般使用樁(Stub)檔案,服務端則用到架構(Skeleton)檔案,介面的規格採用IDL來定義。而回呼函數的引入,使得服務端和用戶端都需要實現一定的樁和架構。下面是回調介面的實現模型: 3.1.1 範例
下面給出了一個使用回調的介面檔案,服務端需要實現Server介面的架構,用戶端需要實現CallBack的架構:
module cb { interface CallBack; interface Server; interface CallBack { void OnEvent(in long Source,in long msg); }; interface Server { long RegisterCB(in CallBack cb); void UnRegisterCB(in long hCb); }; }; |
用戶端首先通過同步方式調用服務端的介面RegistCB,用來註冊回調介面CallBack。服務端收到該請求以後,就會保留該介面引用,如果發生某種事件需要向用戶端通知的時候就通過該引用調用客戶方的OnEvent函數,以便對方及時處理。 關於作者 陳家朋,系統架構師和技術顧問,目前擔任杭州邁可行通訊技術有限公司MPS2000業務交換平台的首席設計人員。擅長架構設計、提供技術解決方案等工作,熟知電腦和通訊領域的各種前沿技術。可以通過 japen@vip.sina.com跟他聯絡。 |