1. 程式人生 > 實用技巧 >回撥函式

回撥函式

作者:no.body
連結:https://www.zhihu.com/question/19801131/answer/27459821
來源:知乎
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

什麼是回撥函式?

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

程式設計分為兩類:系統程式設計(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的三方聯動。有了這個清楚的概念,在自己的程式碼裡實現回撥時才不容易混淆出錯。

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