回調函數(callback)是什麼?

網上搜到的資源講起來好凌亂
http://blog.csdn.net/scarlettsp/article/details/4043624


你到一個商店買東西,剛好你要的東西沒有貨,於是你在店員那裡留下了你的電話,過了幾天店裡有貨了,店員就打了你的電話,然後你接到電話後就到店裡去取了貨。在這個例子里,你的電話號碼就叫回調函數,你把電話留給店員就叫登記回調函數,店裡後來有貨了叫做觸發了回調關聯的事件,店員給你打電話叫做調用回調函數,你到店裡去取貨叫做響應回調事件。回答完畢。


什麼是回調函數?

我們繞點遠路來回答這個問題。

編程分為兩類:系統編程(system programming)和應用編程(application programming)。所謂系統編程,簡單來說,就是編寫;而應用編程就是利用寫好的各種庫來編寫具某種功用的程序,也就是應用。系統程序員會給自己寫的庫留下一些介面,即API(application programming interface,應用編程介面),以供應用程序員使用。所以在抽象層的圖示里,庫位於應用的底下。

當程序跑起來時,一般情況下,應用程序(application program)會時常通過API調用庫里所預先備好的函數。但是有些庫函數(library function)卻要求應用先傳給它一個函數,好在合適的時候調用,以完成目標任務。這個被傳入的、後又被調用的函數就稱為回調函數(callback function)。

打個比方,有一家旅館提供叫醒服務,但是要求旅客自己決定叫醒的方法。可以是打客房電話,也可以是派服務員去敲門,睡得死怕耽誤事的,還可以要求往自己頭上澆盆水。這裡,「叫醒」這個行為是旅館提供的,相當於庫函數,但是叫醒的方式是由旅客決定並告訴旅館的,也就是回調函數。而旅客告訴旅館怎麼叫醒自己的動作,也就是把回調函數傳入庫函數的動作,稱為登記回調函數(to register a callback function)。如下圖所示(圖片來源:維基百科):

可以看到,回調函數通常和應用處於同一抽象層(因為傳入什麼樣的回調函數是在應用級別決定的)。而回調就成了一個高層調用底層,底層再過頭來調用高層的過程。(我認為)這應該是回調最早的應用之處,也是其得名如此的原因。

回調機制的優勢

從上面的例子可以看出,回調機制提供了非常大的靈活性。請注意,從現在開始,我們把圖中的庫函數改稱為中間函數了,這是因為回調並不僅僅用在應用和庫之間。任何時候,只要想獲得類似於上面情況的靈活性,都可以利用回調。

這種靈活性是怎麼實現的呢?乍看起來,回調似乎只是函數間的調用,但仔細一琢磨,可以發現兩者之間的一個關鍵的不同:在回調中,我們利用某種方式,把回調函數像參數一樣傳入中間函數。可以這麼理解,在傳入一個回調函數之前,中間函數是不完整的。換句話說,程序可以在運行時,通過登記不同的回調函數,來決定、改變中間函數的行為。這就比簡單的函數調用要靈活太多了。請看下面這段Python寫成的回調的簡單示例:

`even.py`

#回調函數1
#生成一個2k形式的偶數
def double(x):
return x * 2

#回調函數2
#生成一個4k形式的偶數
def quadruple(x):
return x * 4

`callback_demo.py`

from even import *

#中間函數
#接受一個生成偶數的函數作為參數
#返回一個奇數
def getOddNumber(k, getEvenNumber):
return 1 + getEvenNumber(k)

#起始函數,這裡是程序的主函數
def main():
k = 1
#當需要生成一個2k+1形式的奇數時
i = getOddNumber(k, double)
print(i)
#當需要一個4k+1形式的奇數時
i = getOddNumber(k, quadruple)
print(i)
#當需要一個8k+1形式的奇數時
i = getOddNumber(k, lambda x: x * 8)
print(i)

if __name__ == "__main__":
main()

運行`callback_demp.py`,輸出如下:

3
5
9

上面的代碼里,給`getOddNumber`傳入不同的回調函數,它的表現也不同,這就是回調機制的優勢所在。值得一提的是,上面的第三個回調函數是一個匿名函數。

易被忽略的第三方

通過上面的論述可知,中間函數和回調函數是回調的兩個必要部分,不過人們往往忽略了回調里的第三位要角,就是中間函數的調用者。絕大多數情況下,這個調用者可以和程序的主函數等同起來,但為了表示區別,我這裡把它稱為起始函數(如上面的代碼中注釋所示)。

之所以特意強調這個第三方,是因為我在網上讀相關文章時得到一種印象,很多人把它簡單地理解為兩個個體之間的來回調用。譬如,很多中文網頁在解釋「回調」(callback)時,都會提到這麼一句話:「If you call me, I will call you back.」我沒有查到這句英文的出處。我個人揣測,很多人把起始函數和回調函數看作為一體,大概有兩個原因:第一,可能是「回調」這一名字的誤導;第二,給中間函數傳入什麼樣的回調函數,是在起始函數里決定的。實際上,回調並不是「你我」兩方的互動,而是ABC的三方聯動。有了這個清楚的概念,在自己的代碼里實現回調時才不容易混淆出錯。

另外,回調實際上有兩種:阻塞式回調和延遲式回調。兩者的區別在於:阻塞式回調里,回調函數的調用一定發生在起始函數返回之前;而延遲式回調里,回調函數的調用有可能是在起始函數返回之後。這裡不打算對這兩個概率做更深入的討論,之所以把它們提出來,也是為了說明強調起始函數的重要性。網上的很多文章,提到這兩個概念時,只是籠統地說阻塞式回調發生在主調函數返回之前,卻沒有明確這個主調函數到底是起始函數還是中間函數,不免讓人糊塗,所以這裡特意說明一下。另外還請注意,本文中所舉的示例均為阻塞式回調。延遲式回調通常牽扯到多線程,我自己還沒有完全搞明白,所以這裡就不多說了。


回調函數是你寫一個函數,讓預先寫好的系統來調用。你去調用系統的函數,是直調。讓系統調用你的函數,就是回調。但假如滿足於這種一句話結論,是不會真正明白的。

回調函數可以看成,讓別人做事,傳進去的額外信息。


比如 A 讓 B 做事,根據粒度不同,可以理解成 A 函數調用 B 函數,或者 A 類使用 B 類,或者 A 組件使用 B 組件等等。反正就是 A 叫 B 做事。


當 B 做這件事情的時候,自身的需要的信息不夠,而 A 又有。就需要 A 從外面傳進來,或者 B 做著做著再向外面申請。對於 B 來說,一種被動得到信息,一種是主動去得到信息,有人給這兩種方式術語,叫信息的 push,和信息的 pull。


A 調用 B,A 需要向 B 傳參數。如簡單的函數:

int max(int a, int b);

要使用這函數,得到兩者最大的值, 外面就要傳進來 a, b。這個很好理解。


void qsort(void *, size_t, size_t, int (*)(const void *, const void *));

而這個函數用於排序,最後一個參數就是回調函數,似乎就比較難以理解了。這是因為人為割裂了代碼和數據。


我們暫停一下,看看計算機中比較詭異的地方,也就是代碼(code)和數據(data)的統一。這是一個檻,如果不跨過這檻,很多概念就不清楚。我們常常說計算機程序分成 code 和 data 兩部分。很多人會理解成,code 是會運行的,是動態的,data 是給 code 使用,是靜態的,這是兩種完全不同的東西。


其實 code 只是對行為的一種描述,比如有個機器人可以開燈,關燈,掃地。如果跟機器人約定好,0 表示開燈,1 表示關燈,2 表示掃地。我發出指令串,0 1 2,就可以控制機器人開燈,關燈,掃地。再約定用二進位表示,兩位一個指令,就有一個數字串,000111,這個時候 000111 這串數字就描述了機器人的一系列動作,這個就是從一方面理解是 code,它可以控制機器人的行為。但另一方面,它可以傳遞,可以記錄,可以修改,也就是數據。只要大家都協商好,code 就可以編碼成 data, 將 data 解釋運行的時候,也變成了 code。


code 和 data 可以不用區分,統一稱為信息。既然 int max(int a, int b) 中 int,double 等表示普通 data 的東西可以傳遞進去,自然表示 code 的函數也可以傳進去了。有些語言確實是不區分的,它的 function(表示code)跟 int, double 的地位是一樣的。這種語言就為函數是第一類值。


而有些語言是不能存儲函數,不能動態創建函數,不能動態銷毀函數。只能存儲一個指向函數的指針,這種語言稱為函數是第二類值。

另外有些語言不單可以傳遞函數,函數裡面又用到一些外部信息(包括code, data)。那些語言可以將函數跟函數所用到的信息一起傳遞存儲。這種將函數和它所用的信息作為一個整體,就為閉包。


過了這個檻,將代碼和數據統一起來,很多難以理解的概念就會清晰很多。


現在我們再回頭看看回調函數。回調函數也就是是 A 讓 B 做事,B 做著做著,信息不夠,不知道怎麼做了,就再讓外面處理。


比如上述排序例子,A 讓 B 排序,B 會做排序,但排序需要知道哪個比哪個大,這點 B 自己不知道,就需要 A 告訴它。而這種判斷大小本身是一種動作,既然 C 語言中不可以傳進第一值的函數,就設計成傳遞第二值的函數指針,這個函數指針就是 A 傳向 B 的信息,用來表示一個行為。這裡本來 A 調用 B 的,結果 B 又調用了 A 告訴它的信息,也就叫 callback。


再比如 A 讓 B 監聽系統的某個消息,比如敲了哪個鍵。跟著 B 監聽到了,但它不知道怎麼去處理這個消息,就給外面關心這個消息,又知道怎麼去處理這個消息的人去處理,這個處理過程本身是個行為,既然這個語言不可以傳遞函數,又只能傳一個函數指針了。假如我將函數指針存儲下來,以後就可以隨時調用。代碼和數據都是信息,數據可以存儲下來,用來表示行為的函數自然也可以存儲下來。


跟著有些人有會引申成,什麼註冊啊,通知啊等等等。假如 B 做監聽,C, D, E, F, G, H 告訴 B 自己有興趣知道這消息,那 B 監聽到了就去告訴 C,D,E,F,G等人了,這樣通知多人了,就叫廣播。


理解後進行思考,根本不用關心術語。術語只是為了溝通,別人要告訴你,或者你去告訴人,使用的一套約定的詞語。同一個東西往往有不同術語。


再將回調的概念泛化,比如某人同時關心 A, B, C, D, E, F 事件,並且這些事件是一組的,比如敲鍵盤,滑鼠移動,滑鼠點擊等一組。將一組事件結合起來。在有些語言就映射成介面,介面有 N 個函數。有些語言就映射成一個結構,裡面放著 N 個函數指針。跟著就不是將單個函數指針傳進去,而是將介面,或者函數指針的結構傳進去。根據不同的用途,有些人叫它為代理,監聽者,觀察者等等。


實際上也是將某種行為存儲下來,以後有需要再進行調用。跟回調函數在更深層次是沒有區別的。


回調方法介紹之中國好室友篇(Java示例)

前言

在Java社區的各種開源工具中,回調方法的使用俯拾即是。所以熟悉回調方法無疑能加速自己對開源輪子的掌握。
網上搜了一些文章,奈何對回調方法的介紹大多隻停留在什麼是回調方法的程度上。本篇文章嘗試從回調方法怎麼來的、為什麼要使用回調方法以及在實際項目中如何使用等方面來介紹下。

場景

場景選擇的得當與否,很影響讀者的繼續閱讀的興趣甚至理解的主動性(長期作為互聯網技術博文讀者的我,深有感觸)。
好場景私以為是:熟悉且簡單。

本例小心翼翼選擇的場景是:寫作業。(hope you like)

自己寫

註:寫作業這個動作至少交代三個方面:誰,什麼動作(寫),寫什麼。
下面先從(有個學生,寫,作業)開始。

# 1. 有個學生
Student student = new Student();
# 2. 該學生有寫作業這個動作需要執行
student.doHomeWork(someHomeWork);
# 3. 注意到這個寫作業這個動作是需要得到入參「作業」的後才能進行的。所以給這個學生new了個簡單的題目做。
String aHomeWork = "1+1=?";
student.doHomeWork(aHomeWork);

至此,完成寫作業的動作。


完整代碼

public class Student {

public void doHomeWork(String homeWork) {
System.out.println("作業本");
if("1+1=?".equals(homeWork)) {
System.out.println("作業:"+homeWork+" 答案:"+"2");
} else {
System.out.println("作業:"+homeWork+" 答案:"+"不知道~~");
}
}

public static void main(String[] args) {
Student student = new Student();

String aHomeWork = "1+1=?";
student.doHomeWork(aHomeWork);
}
}

程序執行

作業本
作業:1+1=? 答案:2

我們一定要把焦點聚焦到,」寫作業「這個需求上面。
該學生寫作業的方法是現成的,但是需要有作業作為入參,怎麼獲取作業才是完成動作的關鍵。希望這點能深深印入我們的腦海。


讓室友幫忙解答

上面的例子中該同學自己調用自己的方法,把收到的homework直接寫了。
但是現實可能會出現各種各樣的問題導致該同學不能(xiang)自己來做。比如他想玩遊戲或者有約會。所以他拜託了他的好室友(roommate)來幫忙寫下。該怎麼實現呢。

#1. 因為室友幫忙寫,所以在doHomeWork動作裡面,就不需要有邏輯判斷的代碼,因為舍友會直接把答案寫進來。改成:
student.doHomeWork(aHomeWork, theAnswer);
#上句中做作業的動作支持傳入「作業」和「答案」,有了這兩個,就說明能做好作業了。
#其中aHomeWork作業是已知的,但是theAnswer這個答案卻是室友提供的。
#室友怎麼才能提供答案呢,最簡單是,室友這個對象直接提供一個傳入作業,傳出答案的方法,這樣該同學就可以直接調用了。
RoomMate roomMate = new RoomMate();
String theAnswer = roomMate.getAnswer(aHomeWork);
student.doHomeWork(aHomeWork, theAnswer);

完整代碼

public class Student {

public void doHomeWork(String homeWork, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+homeWork+" 答案:"+ answer);
} else {
System.out.println("作業:"+homeWork+" 答案:"+ "(空白)");
}

}

public static void main(String[] args) {
Student student = new Student();

String aHomeWork = "1+1=?";

RoomMate roomMate = new RoomMate();
String theAnswer = roomMate.getAnswer(aHomeWork);
student.doHomeWork(aHomeWork, theAnswer);
}
}

public class RoomMate {

public String getAnswer(String homework) {
if("1+1=?".equals(homework)) {
return "2";
} else {
return null;
}
}
}

程序執行

作業本
作業:1+1=? 答案:2

怒,說好的回調方法呢~~

因為到目前為止,不需要使用回調方法。
技術總是伴隨新的需求出現的。

好,給你新的需求。
注意重點來了
我們回顧下這兩行代碼

#室友寫好作業
String theAnswer = roomMate.getAnswer(aHomeWork);
#該同學直接抄答案,完成作業
student.doHomeWork(aHomeWork, theAnswer);

該同學想了想,你給了答案有屁用,還得要我自己謄寫到作業本上面去(執行自己的做作業方法)。
你就不能直接調用我的做作業方法幫我把答案寫好,把作業做完得了。

讓室友直接把作業寫了

經不住該同學的軟磨硬泡,「中國好室友」答應了。怎麼實現呢。
再回顧下做作業的全過程

#待解決的作業
String aHomeWork = "1+1=?";
#室友寫出答案
String theAnswer = roomMate.getAnswer(aHomeWork);
#該同學調用,自己把答案寫到作業本。(也即是這個步驟不給調用了)
student.doHomeWork(aHomeWork, theAnswer);
#做作業必須得調用這個方法,而根據需求這個方法必須由室友去調用。那很顯然,該室友得保持一個該同學的引用,才能正常調用啊。
#燈燈燈~
#室友說,那你在調用getAnswer方法的時候,除了傳入作業,還需要把自己的引用放裡面。這樣我做完了,直接調用你的做作業方法就行了。
roomMate.getAnswer(aHomeWork,student);

完整代碼

public class Student {

public void doHomeWork(String homeWork, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+homeWork+" 答案:"+ answer);
} else {
System.out.println("作業:"+homeWork+" 答案:"+ "(空白)");
}

}

public static void main(String[] args) {
Student student = new Student();

String aHomeWork = "1+1=?";

RoomMate roomMate = new RoomMate();
roomMate.getAnswer(aHomeWork,student);
}
}

public class RoomMate {

public void getAnswer(String homework, Student student) {
if("1+1=?".equals(homework)) {
student.doHomeWork(homework, "2");
} else {
student.doHomeWork(homework, "(空白)");
}
}
}

執行程序

作業本
作業:1+1=? 答案:2

回調方法

在上述「讓室友直接把作業寫了」的例子中,其實已經體現了回調的意思。
場景的核心在於這位學生要把作業給做了。
簡單點描述:這位學生告訴室友要做什麼作業,並把自己的引用也給了室友。該室友得到作業,做完後直接引用該學生並調用其做作業的方法,完成代寫作業的任務。
稍微複雜點描述:
該學生做作業的方法有兩個入參,一個是作業題目(已知),一個是作業答案(未知)。
室友為了幫助他寫作業提供了一個方法,該方法有兩個入參,一個是作業題目,一個是該學生的引用(解出答案得知道往哪寫)。
程序執行時,該學生只要調用室友的代寫作業方法就行了。一旦室友得到答案,因為有該學生的引用,所以直接找到對應方法,幫助其完成作業。
再複雜點描述:
學生調用室友的替寫作業方法,註冊了題目和自己的引用。室友的替寫作業方法被調用,則會根據題目完成作業後,再回調該同學寫作業方法,完成作業。
再抽象點描述:
類A調用類B的方法b(傳入相關信息),類B的方法在執行完後,會將結果寫到(再回調)類A的方法a,完成動作。(其實方法a就是傳說中的回調方法啦)
最抽象的描述:
調用,回調。


介面方式的回調方法

常常使用回調方法的同學可能會說,我從來也沒見過直接把對象的引用寫到第一次調用方法裡面的。
嗯,是的,下面就來填上述例子留下的「天坑」(實際項目中常見到)。

問題:在調用方法中直接傳對象引用進去有什麼不好?
只說一點,只是讓別人代寫個方法,犯得著把自己全部暴露給別人嗎。萬一這個別人是競爭對手的介面咋辦。這就是傳說中的後面代碼嗎(/tx)。
總之這樣做是非常不安全的。

因此,最常規的《調用,回調》實現,是(你已經猜到了)使用介面作為引用(說的不嚴謹)傳入調用的方法裡面。

我承認,怎麼將思路跳轉到使用介面的花了我好長時間。
我們再看RoomMate類的getAnswer方法。

public class RoomMate {

public void getAnswer(String homework, Student student) {
if("1+1=?".equals(homework)) {
student.doHomeWork(homework, "2");
} else {
student.doHomeWork(homework, "(空白)");
}
}
}

關鍵在於,該方法的用途是來解決某學生提出的某個問題。答案是通過學生的doHomeWork方法回調傳回去的。那假設有個工人也有問題,這位室友該怎麼解決呢。再開個方法,專門接收工人的引用作為傳參?當然不用,只要你這個引用包含了doHomeWork()方法,那麼不論你是工人、警察還是環衛工人,直接調用getAnswer()方法就能解決你提的問題。
至此我們的思路達到了:所有的對象要有同一個方法,所以自熱而然就引出了介面概念。只要這些對象都實現了某個介面就行了,這個介面的作用,僅僅是用來規定那個做作業的方法長什麼樣。這樣工人實現了該介面,那麼就有了默認繼承的做作業方法。工人再把自己的引用拋給該室友的時候,這個室友就不需要改動任何代碼,直接接觸答案,完成任務了。

創建一個做作業的介面,專門規定,需要哪些東西(問題和答案)就能做作業.

public interface DoHomeWork {
void doHomeWork(String question, String answer);
}

改動下中國好室友的解答方法。任意一個實現了DoHomeWork 介面的someone,都擁有doHomeWork(String question,String answer)的方法。這個方法就是上面已經提到的「回調方法」。someone先調用下好室友的getAnswer()方法,把問題和自己傳進來(此為調用),好室友把問題解答出之後,調用默認提供的方法,寫完作業。
思考下,因為是以介面作為參數類型的約定,在普通對象upcast向上轉型之後將只暴露介面描述的那個方法,別人獲取到這個引用,也只能使用這個(回調)方法。至此,遺留的重大安全隱患重要解決。

完整代碼

public class RoomMate {

public void getAnswer(String homework, DoHomeWork someone) {
if("1+1=?".equals(homework)) {
someone.doHomeWork(homework, "2");
} else {
someone.doHomeWork(homework, "(空白)");
}
}
}

package org.futeng.designpattern.callback.test1;

public class Worker implements DoHomeWork {

@Override
public void doHomeWork(String question, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+question+" 答案:"+ answer);
} else {
System.out.println("作業:"+question+" 答案:"+ "(空白)");
}
}

public static void main(String[] args) {
Worker worker = new Worker();
String question = "1+1=?";

new RoomMate().getAnswer(question, worker);

}
}

執行程序

作業本
作業:1+1=? 答案:2

至此,調用+回調的文章是不是寫完了呢。
咳咳,還木有。大家喝點茶再忍耐下。(我都寫一天了 - -)


常規使用之匿名內部類

作為平凡的屁民,實用主義是我們堅持的生存法則。
所以凡事用不到的技術都可以不學,凡事學了卻不用的技術等於白學。

我們之前已經定性,中國好室友RoomMate類擁有接受任何人任何問題挑戰的潛質。
自從好室友出名之後,有個不知道什麼工作(類型)的人也來問問題。反正只要實現了回調介面,好室友都能調用你默認繼承的回調方法,那就放馬過來吧。

package org.futeng.designpattern.callback.test1;

public class RoomMate {

public void getAnswer(String homework, DoHomeWork someone) {
if("1+1=?".equals(homework)) {
someone.doHomeWork(homework, "2");
} else {
someone.doHomeWork(homework, "(空白)");
}
}

public static void main(String[] args) {

RoomMate roomMate = new RoomMate();

roomMate.getAnswer("1+1=?", new DoHomeWork() {

@Override
public void doHomeWork(String question, String answer) {
System.out.println("問題:"+question+" 答案:"+answer);
}
});
}
}

看到稍顯奇怪的roomMate.getAnswer("1+1=?", new DoHomeWork() {的哪一行,其實這裡new的是DoHomeWork介面的一個匿名內部類。這裡我想大家應該自己動腦想想,調用+反調,這個過程是怎麼實現的了。
至於是否使用匿名內部類是根據具體使用場景決定的。普通類不夠直接,匿名內部類的語法似乎也不夠友好。

開源工具中對回調方法的使用

上述匿名內部類的示例才是開源工具中常見到的使用方式。

調用roomMate解答問題的方法(傳進去自己的引用),roomMate解決問題,回調引用裡面包含的回調方法,完成動作。
roomMate就是個工具類,「調用」這個方法你傳進去兩個參數(更多也是一個道理),什麼問題,問題解決了放哪,就行了。該「調用」方法根據問題找到答案,就將結果寫到你指定放置的位置(作為回調方法的入參)。

試想下,「中國好室友」接收的問題是SQL語句,接收的放置位置是我的引用。你解決問題(執行完SQL),將答案(SQL的反饋結果)直接寫入我的回調方法裡面。回調方法裡面可能包括一個個的欄位賦值。但是在調用層面隱藏了很多細節的處理。這是回調方法的一個小小的優勢。再換句話說,不需要拿到執行完SQL之後的返回結果一個個來賦值。

SQL的例子

public static List& queryPerson() {
QueryRunner queryRunner = new QueryRunner(DataSourceSupport.getDataSource());

return queryRunner.query(" select t.name, t.age from person t ", new ResultSetHandler&&>(){

List list = new ArrayList&();
@Override
public List& handle(ResultSet rs) throws SQLException {

while(rs.next()) {
Person person = new Person();
person.setName(rs.getString(0));
person.setAge(rs.getInt(1));
list.add(person);
}
return list;
}
});
}

回調方法的優勢

回調方法最大的優勢在於,非同步回調,這樣是其最被廣為使用的原因。
下面將沿用「中國好室友」 來對回調方法做非同步實現。

回調介面不用變

public interface DoHomeWork {
void doHomeWork(String question, String answer);
}

為了體現非同步的意思,我們給好室友設置了個較難的問題,希望好室友能多好點時間思考。

Student student = new Student();
String homework = "當x趨向於0,sin(x)/x =?";
#給學生新建個ask方法,該方法中另開一個線程,來等待回調方法的結果反饋。
student.ask(homework, new RoomMate());
#ask方法如下
public void ask(final String homework, final RoomMate roomMate) {
new Thread(new Runnable() {

@Override
public void run() {
roomMate.getAnswer(homework, Student.this);
}
}).start();

goHome();
}
#新開的線程純粹用來等待好室友來寫完作用。由於在好室友類中設置了3秒的等待時間,所以可以看到goHome方法將先執行。
#意味著該學生在告知好室友做作用後,就可以做自己的事情去了,不需要同步阻塞去等待結果。
#一旦好室友完成作用,寫入作業本,該現場也就結束運行了。

完整代碼

public class Student implements DoHomeWork{

@Override
public void doHomeWork(String question, String answer) {
System.out.println("作業本");
if(answer != null) {
System.out.println("作業:"+question+" 答案:"+ answer);
} else {
System.out.println("作業:"+question+" 答案:"+ "(空白)");
}
}

public void ask(final String homework, final RoomMate roomMate) {
new Thread(new Runnable() {

@Override
public void run() {
roomMate.getAnswer(homework, Student.this);
}
}).start();

goHome();
}

public void goHome(){
System.out.println("我回家了……好室友,幫我寫下作業。");
}

public static void main(String[] args) {
Student student = new Student();
String homework = "當x趨向於0,sin(x)/x =?";
student.ask(homework, new RoomMate());

}
}

public class RoomMate {

public void getAnswer(String homework, DoHomeWork someone) {
if ("1+1=?".equals(homework)) {
someone.doHomeWork(homework, "2");
} else if("當x趨向於0,sin(x)/x =?".equals(homework)) {

System.out.print("思考:");
for(int i=1; i&<=3; i++) { System.out.print(i+"秒 "); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(); someone.doHomeWork(homework, "1"); } else { someone.doHomeWork(homework, "(空白)"); } } }

完結

至此回調方法的介紹告一段落。

趁著是高考日,表示要跟考生感同身受一下。特意花了整整一個白天的時間寫就這篇文章。全文一蹴而就並沒有更多時間來修改,可能隱藏著諸多錯誤,行文也可能不甚通順,還請各位指正和海涵。


2014/06/08 19:24


一般寫程序是你調用系統的API,如果把關係反過來,你寫一個函數,讓系統調用你的函數,那就是回調了,那個被系統調用的函數就是回調函數。


這裡講的是非同步回調(謝謝評論中 @羊羊羊 提醒),或者是遠程過程調用(RPC)語義上的回調。

小明有一道數學題不懂,打電話給同學小軍,小軍聽完題後感覺不能馬上回答,解題得花些時間。

這時候他們有幾種選擇:

  • 選擇1,電話不掛(也可以掛了),小軍解題,每過一會兒,小明就(打電話)問,怎麼樣了?問了十幾分鐘,小軍才解答出來,告訴小明。
  • 選擇2,電話掛了,小明把自己的電話告訴小軍,小軍做完後給小明打電話告訴小明答案。
  • 選擇3,電話掛了,但小軍不願意花電話費給小明講題,於是約定響鈴三聲,小明別接,然後再打回來。
  • 選擇4,電話掛了,小軍告訴小明自己的博客地址,解完了會寫在博客上,小明做完其他作業後再去看。

第一種方法,小明在小軍解題期間不能放下電話,只能不斷詢問。

第二種方法,小明在小軍解題期間可以做其他作業,當小軍打電話來時,又得放下其他作業。小明給小軍留下電話號碼,讓他撥回來,就被稱作回調(call back)

第三種方法,小明可以做其他作業,當小軍打電話來響鈴時,就知道小軍已經解完題了,可以馬上,或在合適的時間去打電話給小軍。這種就不叫回調了。

第四種方法,小明可以先做完其他作業後去小明的博客看就行了。


我覺得樓上很多關於回調的解釋。只是描述了回調的一些應用場景 並沒有講清楚回調究竟是什麼
比如很多人點贊的那個等店裡面有貨店員打電話通知你的那個例子 我覺得callback只是實現observer的這一手段而已
引用stack overflow 上大神的描述 其實callback 很簡單也很純粹
A "callback" is any function that is called by another function which takes the first function as a parameter. (在一個函數中調用另外一個函數就是callback)

function callback() {
alert("I am in the callback!");
}

function work(func) {
alert("I am calling the callback!");
func();
}

work(callback);

這就是一個很簡單的callback
callback 作為一個變數傳入函數work 中 在work 中被調用

然後來說一下callback 經常的使用場景

A lot of the time, a "callback" is a function that is called when something happens. That something can be called an "event" in programmer-speak.(很多時候 callback 都是用來執行事件驅動的任務 比如有貨了通知我 | 你到家了再叫我做飯 等等之類的 )


舉個簡單的例子 對一個文件的讀入
如果不用callback

fileObject = open(file)
# 我們必須等到文件打開才能進行讀入 在這之前我們不能做其他事情
fileObject.write("We are writing to the file.")

使用callback

# 我們把writeToFile 為一個function 傳入open writeToFile就是一個callback function
fileObject = open(file, writeToFile)
# 我們不需要一直等著這個文件打開
# 文件打開的時候 他就會執行callback 也就是writeToFile(具體實現 可以用promise) 在這之前

我們可以做其他事情

----------------------------------------------------------分割線----------------------------------------------------------------
好吧。。如果有人還是看不懂上面的例子。。我就把上面的代碼用js 實現一下

function openFile(filePath,callback){
alert("start opening file in"+filePath);
callback();//when finished,execute callback()
}

function writeToFile(){
alert("i"m now writing file");
}

openFile("c://test.csv",writeToFile);

如果我們在調用openFile 這個例子 我們先會收到 start opening file in c://test.csv 然後會收到 i"m now writing file 這不需要你call 它。。在你open file 結束之後就會被自動執行了。。。

不知道能不能回答提問的人的那個dont call it , it will call you 的提問?


回調函數:

函數可以理解為一個功能體,執行它可以完成一個任務。
回調函數本質上就是一個函數,只是執行時間和執行主體與普通的函數稍有區別。

-----舉個例子的分割線------------
你去食堂打飯,你喜歡吃小炒熱飯菜,所以你去了一個小炒窗口。
你跟老闆說了要×××蓋飯,老闆說:你是100號,喊到你的號你就來拿菜。
然後你在旁邊跟同學吹牛、或者看手機、或者干點你想乾的任何事情。。。
然後你聽到老闆喊100號並且把菜放到窗口,你走到窗口,拿到你的菜。

這裡面就有典型的非同步操作、回調函數的概念。

-----下面很煩的分割線------------
好吧,先搞清楚一些問題再回頭分析

回調函數在什麼場景有用?
我要在特定時候執行一個任務,至於是什麼時候我自己都不知道。比如某一時間到了或者某一事件發生或者某一中斷觸發。

回調函數怎麼起作用?
把我要執行的這個任務寫成一個函數,將這個函數和某一時間或者事件或者中斷建立關聯。當這個關聯完成的時候,這個函數華麗的從普通函數變身成為回調函數。

回調函數什麼時候執行?
當該回調函數關心的那個時間或者事件或者中斷觸發的時候,回調函數將被執行。
一般是觸發這個時間、事件或中斷的程序主體(通常是個函數或者對象)觀察到有一個關注這個東東的回調函數的時候,這個主體負責調用這個回調函數。

回調函數有什麼好處?
最大的好處是你的程序變成非同步了。也就是你不必再調用這個函數的時候一直等待這個時間的到達、事件的發生或中斷的發生(萬一一直不發生,你的程序會怎麼樣?)。再此期間你可以做做別的事情,或者四處逛逛。當回調函數被執行時,你的程序重新得到執行的機會,此時你可以繼續做必要的事情了。

回調函數有什麼問題嗎?
既然有人問,當然就會有點問題。一個是學習成本會比普通函數高,需要有一定的抽象思維能力,需要對應用場景的理解。另一個是回調函數很多情況下會附帶有跨線程操作甚至於跨進程的操作,這些都是非同步帶來的成本。


-----回調分析的分割線------------
你去食堂打飯,你喜歡吃小炒熱飯菜,所以你去了一個小炒窗口。
你跟老闆說了要×××蓋飯,老闆說:你是100號,喊到你的號你就來拿菜。
然後你在旁邊跟同學吹牛、或者看手機、或者干點你想乾的任何事情。。。
然後你聽到老闆喊100號並且把菜放到窗口,你走到窗口,拿到你的菜。

這裡面有幾個函數:
老闆的部分:
1、老闆提供一個點餐的函數 boss.Order(string 菜名,double 錢)
2、老闆有個做飯的函數,此函數耗時較長boss.Cook()
3、老闆提供一個事件,當boss.cook()執行完時,該事件被觸發,boss.OnCookFinish;

你的部分:
1、你需要有一個函數去訂餐,也就是你的函數中需要執行類似於boss.Order("紅燒肉蓋澆飯",20),比如是me.Hungry()
2、你需要有一個函數作為回調函數去關注boss.OnCookFinish事件,這樣當老闆做好飯,你就可以知道是不是你的好了。
由於老闆的事件發生的時候中會喊編號並且吧菜放到窗口,所以你的回調函數需要能夠接受1個編號和1個菜作為參數。
比如me.AcceptFood(int currNumber,object food)

所以整個程序的流程其實是這樣的。
me.Hungry(){
boss.Order("紅燒肉蓋澆飯",20);
boss.OnCookFinish+=me.AcceptFood;//此處表面,AcceptFood這個回調函數關心OnCookFinish事件,並且變成這個事件的回調函數
//此時這個函數執行完,不再等待
}

boss.Order("紅燒肉蓋澆飯",20){
//收錢
//配菜 前2個耗時較短
boss.Cook();//此處一般會開新線程執行cook動作
}

boss.Cook(){
//cooking~~~~~~~~~~
//完成了,下面將要觸發事件,系統將檢查這個事件是否有回調函數關心,有的話逐個回調。
OnCookFinish(100號,紅燒肉蓋澆飯);
}

至此案例基本完成了一個完整的任務流程。


======最終總結的分割線==========
回調函數在非同步處理過程中的一個必要手段。目的是讓me不需要等boss的長時間操作,可以在這段時間做點別的事情。


------關於硬體中斷------------
硬體中斷也會有對應的函數做處理,所以這個函數從概念上來說也就是個回調函數。
無非前文討論的是軟體層面的,硬體中斷對應的函數是OS層面甚至於硬體層面的。
沒什麼本質的區別。


  • 非回調函數,輪詢的方法:http://jsfiddle.net/island205/6Xccd/,溫度計5秒更新自己顯示溫度,公告板每兩秒查看下溫度計的溫度,如果溫度變了就更新公告板上的溫度顯示
  • 回調函數,委託的方法:http://jsfiddle.net/island205/Hek3K/,公告板把檢查的介面暴露給了溫度計,委託溫度計每次更新溫度時直接進行更新

所謂的回調函數就是所謂的委託,委託的使用,使得公告板不用關心溫度計是否已經更新,只需要將更新的介面暴露給溫度計即可。
事件的原理就是委託,補個例子:http://jsfiddle.net/island205/aANQK/,公告板向溫度計註冊事件說,你刷新的時候就通知我,且把刷新的溫度發給我阿,而這個通知就是溫度計調用公告板給的函數onRefresh,即callback來完成。
大體上就是這樣,例子有不當的地方,歡迎大家指正修改。


好萊塢準則:
Don"t call me; I"ll call you!


隨便拖動了下,感覺有些答案很長。這樣真的不好。所以,我嘗試給出比較簡短的描述性定義(只看粗體部分就夠了,如果覺得我沒描述清楚的,可以繼續看後面的一堆)。

f1 直接或間接的通知給 f2 一個函數 f3,在 f1 調用 f2 的過程中,f2 調用了 f3。

這裡:f3 即回調函數。
----------------------------------------------------------------------------------
(備註:下文敘述中:A = f1, B = f2, C = f3)

請仔細品味 call back 的英文字面意思,有助於理解!A -&> B 的過程中包含了 B -&> C 。因此這就叫做 call back!

(以上過程,可以用 UML 圖表示,畫兩個垂直先下的箭頭分別代表 A 和 B函數,然後再加上橫向的箭頭表示調用,這樣就會顯得更清楚,這裡我就不給出圖了,感興趣的可以自己畫一下)。

補充說明:

A,B 通常是位於不同模塊中的函數。

即 A,C 通常屬於一個模塊 m1,B 屬於另一個模塊 m2。

模塊m1,m2通常是由不同的人,不同的公司開發的。即回調函數是模塊間合作的一種常見工具,手段,用法。可以讓兩個模塊更面向介面,防止兩者的過強耦合(難以拆分)。

一些回調函數的實例例如,
TestAbortProc(測試用戶是否取消了一個耗時過程),UpdateProgress(提供更新進度的反饋)。

----------------------------------------------------------------------------
A把C直接和間接通知到B,我們舉兩個例子來說明;

(1)直接的方式比較直觀,通常C是一個參數,被傳遞到B。

例如 WINAPI 中的枚舉窗口,枚舉字體,枚舉顯示器,創建一個線程,都是這樣的方式,可以很直觀的看到到回調函數這個參數。

。net的同學可能會疑惑,為什麼C++中是這樣的方式實現的,而不像。net 里那樣把大量結果直接用一個集合都返回來結果多方便啊,代碼多簡潔啊。所以,這就體現了c++的哲學,高效,不僅僅是時間高效,空間也要高效。這種通過一個循環來返回多個結果的方式,可以有效的提高空間效率(節約內存!)。

(2)間接的方式,比較少見一些。例如窗口過程,就是間接性的。

窗口過程,首先在 RegisterClass 的時候被告知給系統(系統註冊該Class的時候,窗口過程,窗口背景畫刷,菜單,圖標等信息都被告知給系統), 然後CreateWindow 的時候傳遞的是 Class"s Name 或者 atom (一個數字ID)(系統自然知道這個窗口對應的的窗口過程),然後在你的 UI 線程調用 DispatchMessage (即消息循環的循環體)的時候,引起對你定義的窗口過程的回調!因此窗口過程也是 CALLBACK 回調。

以及,當你調用 SendMessage 的時候,在同進程內的情況下,等同於直接調用對應的窗口過程。


一句話:傳一個函數指針做為參數
另外:寫過代碼你就明白,死扣概念不行。


就是把自己交給別的函數,讓它執行到一半來調用自己的函數。


可以傳進高階函數的closure


服裝店兩個業務(1)在店面選衣服,顧客提供尺碼和顏色。(2)定製衣服,顧客提供材料類型,尺碼,顏色以及製作方法(如用毛線織立領,袖子針織條紋,胸前綉上名字)。顧客去店面選衣服類似普通函數調用;定製衣服類似回調,首先顧客調用「定製衣服"方法,然後店家在製作的時候使用顧客提供的「製作方法」就是回調。
用c語言表達就是:
clothes 選衣服(int size,color c){。。。;return cloth;}
void 製作方法(int size,材料 l,style s){。。。}
clothes 定製(int size,材料 l,style s,function 製作方法){。。。製作方法(size,l,s);return cloth;}
製作方法就是回調函數。
總的來說,調用函數的時候需要給函數提供一些變數於是有了形參。為了尋求更高的靈活性保留於具體客戶無關的部分,將函數中與具體客戶相關的部分抽離出去由客戶完成自己的個性化部分,然後以回調的形式再傳遞給函數,函數在自己內部使用回調函數。
拙見,不足之處請海涵。


回調函數的意思是「把函數A作為參數傳遞給另一個函數B」。

維基百科的解釋:
a callback is a piece of executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at some convenient time.

為什麼要這樣做呢?為什麼不把定義好的函數放在頭文件中呢?

因為可以作為參數的函數A的名稱和實現可以被抽象出來,也就是在寫函數B的時候,不需要管函數A叫什麼名字,怎麼實現,只要函數A的返回類型是一定的就可以了(泛型的話連這個也不需要)。

比如某人寫了一個畫圖的庫,把具體怎麼畫留給使用庫的人定義。使用庫的人就可以自定義畫圖函數的名稱和實現方式。

例子(C++):

非泛型
定義庫(library.h)

#include &
void fill_screen(int (*some_func)(void)) {printf("%d
",some_func());}

使用庫的時候:

#include "library.h"

int draw_this_way() {return 0;}
int draw_that_way() {return 1;}
int draw_yet_another_way() {return 2;}

int main() {
fill_screen(draw_this_way);
fill_screen(draw_that_way);
fill_screen(draw_yet_another_way);
return 0;
}

編譯:

g++ callback.cpp -o callback

結果:

$./callback
0
1
2

泛型
定義庫(library.h):

# include &
template&
void fill_screen(ANY_TYPE (*some_func)(void)) { std::cout &<&< some_func() &<&< std::endl;}

使用庫的時候:

#include "library.h"

int draw_this_way() {return 0;}
char draw_that_way() {return "a";}
bool draw_yet_another_way() {return true;}

int main() {
fill_screen(draw_this_way);
fill_screen(draw_that_way);
fill_screen(draw_yet_another_way);
return 0;
}

編譯:

g++ callback.cpp -o callback

結果:

$./callback
0
a
1

補充:

委託和回調
委託是一種設計模式。意思是A類把自己的工作交給B類完成。簡單的委託相當與在A的方法中調用了B的方法。

使用委託可以把庫的定義和具體使用分開。比如資料庫的基本操作是CRUD,但是每個不同資料庫具體api不同,對於使用者而言不可能逐個掌握所有資料庫的api,所以出現了api的wrapper,用戶只用掌握一套CRUD的api就可以使用所有資料庫了。

在某些不能直接將函數作為參數傳遞的語言(比如java)中,委託是實現回調功能的一種方式。
具體而言,庫的作者定義一個介面,在他的方法中調用這個介面,但是把介面的實現留給具體使用的人。在使用的時候,使用者新建一個匿名介面,並且重載要調用的介面方法。 這個過程相當與庫的作者將工作通過介面「委託」給了使用者。

這個委託的過程相對比較複雜,打個生活中的比方:「領導不但把工作推脫給下屬,還要對他們具體怎麼干指手畫腳。」

這裡的領導是庫的使用者,下屬是庫的定義,推脫是指調用下屬的工作函數,指手畫腳是指對工作函數調用的介面給予具體定義。

以下是java的例子。

定義庫 (DrawLibrary.java)

public class DrawLibrary {
public interface DrawInterface {
int draw();
}

public static void fillScreen(DrawInterface someObj) {
System.out.println(someObj.draw());
}
}

使用庫(Graphics.java)

public class Graphics {
public static void drawThisWay() {
DrawLibrary.fillScreen( new DrawLibrary.DrawInterface() {
@Override
public int draw() {
return 0;
}
});
}
public static void drawThatWay() {
DrawLibrary.fillScreen( new DrawLibrary.DrawInterface() {
@Override
public int draw() {
return 1;
}
});
}
public static void drawYetAnotherWay() {
DrawLibrary.fillScreen( new DrawLibrary.DrawInterface() {
@Override
public int draw() {
return 2;
}
});
}

public static void main(String[] args) {
drawThisWay();
drawThatWay();
drawYetAnotherWay();
}
}

編譯:

$javac Graphics.java
$java Graphics

結果:

0
1
2

這種設計模式經常在事件驅動的設計中看到,比如監聽按鈕是否被按下等等。


有本python 網路編程的書,介紹了twisted庫 及其 email 編程。上面說,所謂回調函數,其運行方式是,「你不用調用我,等著我來調用你」。


模塊A和B配合做一件事情。

模塊A清楚在什麼時間和地點做這件事情,但不清楚怎麼去做;
模塊B相反,清楚怎麼做,但不知道什麼時間和地點去做。

於是B把「怎麼做」封裝成了函數;
A在合適的時間和地點調用此函數。

這個函數就是回調函數。


是一種約定


是設計模式裡面template method pattern當template method的數量無限趨近於一的時候的極限


推薦閱讀:

對於一個編程基礎不是很好的學生來說,學習數據挖掘、機器學習之類的並以後從事這樣的工作靠譜嗎?
為什麼有些驗證碼看起來很容易但是沒人做自動識別的?
用 Linux 真的能學到很多平台無關的東西嗎?
國內人寫代碼的水平跟美國的差距在哪?
為什麼說讀代碼比寫代碼難?

TAG:編程語言 | 編程 | 回調函數(Callback) | 事件處理 |