一文搞懂:詞法做用域、動態做用域、回調函數、閉包

無論什麼語言,咱們總要學習做用域(或生命週期)的概念,好比常見的稱呼:全局變量、包變量、模塊變量、本地變量、局部變量等等。無論如何稱呼這些做用域的範圍,實現它們的目的都同樣:python

  • (1)爲了不名稱衝突;
  • (2)爲了限定變量的生命週期(本文以變量名說事,其它的名稱在規則上是同樣的)。

可是不一樣語言的做用域規則不同,雖然學個簡單的基礎就足夠應用,由於咱們有編程規範:(1)儘可能避免名稱衝突;(2)加上相似於local的修飾符儘可能縮小生效範圍;(3)放進代碼塊,等等。可是真正去細心驗證做用域的生效機制卻並不是易事(我學Python的時候,花了很長時間細細驗證,學perl的時候又花了很長時間細細驗證),但能夠確定的是,理解本文的詞法做用域規則(Lexical scoping)和動態做用域規則(dynamic scoping),對學習任何語言的做用域規則都有很大幫助,這兩個規則是各類語言都宏觀通用的。shell

很簡單的一段bash下的代碼:編程

x=1
function g(){ echo "g: $x" ; x=2; }
function f(){ local x=3 ; g; echo "f: $x"; } # 輸出2仍是3
f           # 輸出1仍是3?
echo $x     # 輸出1仍是2?

對於bash來講,上面輸出的分別是3(g函數中echo)、2(f函數中的echo)和1(最後一行echo)。可是一樣語義的代碼在其它語言下獲得的結果安全

可能就不同(分別輸出一、3和2,例如perl中將local替換爲my)。bash

這牽扯到兩種貫穿全部程序語言的做用域概念:詞法做用域(相似於C語言中static)和動態做用域。詞法做用域和"詞法"這個詞真的沒什麼關係,反而更應該稱之爲"文本段做用域"。要區別它們,只須要回答"函數out_func中嵌套的內層函數in_func可否看見out_func中的環境"。多線程

對於上面的bash代碼來講,假如這段代碼是適用於全部語言的僞代碼:閉包

  • 對於詞法做用域的語言,執行f時會調用g,g將沒法訪問f文本段的變量,詞法做用域認爲g並非f的一部分,而是跳出f的,由於g的定義文本段是在全局範圍內的,因此它是全局文本段的一部分。若是函數g的定義文本段是在f內部,則g屬於f文本段的一部分
    • 因此g不知道f文本段中local x=3的設置,因而g的echo會輸出全局變量x=1,而後設置x=2,由於它沒有加上做用域修飾符,而g又是全局內的函數,因此x將修改全局做用域的x值,使得最後的echo輸出2,而f中的echo則輸出它本身文本段中的local x=3。因此整個流程輸出1 3 2
  • 對於動態做用域的語言,執行f時會調用g,g將能夠訪問f文本中的變量,動態做用域認爲g是f文本段的一部分,是f中的嵌套函數
    • 因此g能看到local x=3的設置,因此g的echo會輸出3。g中設置x=2後,僅僅只是在f的內層嵌套函數中設置,因此x=2對g文本段和f文本段(由於g是f的一部分)均可見,但對f文本段外部不可見,因此f中的echo輸出2,最後一行的echo輸出1。因此整個流程輸出3 2 1
  • 總結來講:
    • 詞法做用域是關聯在編譯期間的,對於函數來講就是函數的定義文本段的位置決定這個函數所屬的範圍
    • 動態做用域是關聯在程序執行期間的,對於函數來講就是函數執行的位置決定這個函數所屬的範圍

因爲bash實現的是動態做用域規則。因此,輸出的是3 2 1。對於perl來講,my修飾符實現詞法做用域規則,local修飾符實現動態做用域規則。app

例如,使用my修飾符的perl程序:異步

#!/usr/bin/perl

$x=1;
sub g { print "g: $x\n"; $x=2; }
sub f { my $x=3; g(); print "f: $x\n"; }  # 詞法做用域
f(); 
print "$x\n";

執行結果:編程語言

[fairy@fairy:/perlapp]$ perl scope2.pl 
g: 1
f: 3
2

使用local修飾符的perl程序:

#!/usr/bin/perl

$x=1;
sub g { print "g: $x\n"; $x=2; }
sub f { local $x=3; g(); print "f: $x\n"; }  # 動態做用域
f(); 
print "$x\n";

執行結果:

[fairy@fairy:/perlapp]$ perl scope2.pl 
g: 3
f: 2
1

有些語言只支持一種做用域規則,特別是那些比較現代化的語言,而有些語言支持兩種做用域規則(正如perl語言,my實現詞法變量做用域規則,local實現動態做用域規則)。相對來講,詞法做用域規則比較好控制整個流程,還能借此實現更豐富的功能(如最典型的"閉包"以及高階函數),而動態做用域因爲讓變量生命週期"沒有任何深度"(回想一下shell腳本對函數和做用域的控制,很是傻瓜化),比較少應用上,甚至有些語言根本不支持動態做用域。

閉包和回調函數

理解閉包、回調函數不可不知的術語

1.引用(reference):數據對象和它們的名稱

前文所說的可見、不可見、變量是否存在等概念,都是針對變量名(或其它名稱,如函數名、列表名、hash名)而言的,和變量的值無關。名稱和值的關係是引用(或指向)關係,賦值的行爲就是將值所在的數據對象的引用(指針)交給名稱,讓名稱指向這個內存中的這個數據值對象。以下圖:

2.一級函數(first-class functions)和高階函數(high-order functions)

有些語言認爲函數就是一種類型,稱之爲函數類型,就像變量同樣。這種類型的語言能夠:

  1. 將函數賦值給某個變量,那麼這個變量就是這個函數體的另外一個引用,就像是第二個函數名稱同樣。經過這個函數引用變量,能夠找到函數體,而後調用執行。
    • 例如perl中$ref_func=\&myfunc表示將函數myfunc的引用賦值給$ref_func,那麼$ref_func也指向這個函數。
  2. 將函數做爲另外一個函數的參數。例如兩個函數名爲myfunc和func1,那麼myfunc(func1)就將func1做爲myfunc的參數。
    • 這種行爲通常用於myfunc函數中對知足某些邏輯的東西執行func1函數。
    • 舉個簡單的例子,unix下的find命令,將find看做是一個函數,它用於查找指定路徑下符合條件的文件名,將-print-exec {}\;選項實現的功能看做是其它的函數(請無視它是否真的是函數),這些選項對應的函數是find函數的參數,每當find函數找到符合條件的文件名時,就執行-print函數輸出這個文件名
  3. 函數的返回值也能夠是另外一個函數。例如myfunc函數的定義語句爲function myfunc(){ ...return func1 }

其實,實現上面三種功能的函數稱之爲一級函數或高階函數,其中高階函數至少要實現上面的2和3。一級函數和高階函數並無區分的必要,但若是必定要區分,那麼:

  • 一級函數更像是一種術語概念,它將函數看成一種值看待,能夠將其賦值出去、做爲參數傳遞出去以及做爲返回值,對於計算機程序語言而言,它更多的是用來描述某種語言是否支持一級函數;
  • 高階函數是一種函數類型,就像回調函數同樣,當某個函數符合高階函數的特性,就能夠將其稱之爲這是一個高階函數。

3.自由變量(free variable)和約束變量(bound variable)

這是一組數學界的術語。

在計算機程序語言中,自由變量是指函數中的一種特殊變量,這種變量既不在本函數中定義,也不是本函數的參數。換句話說,多是外層函數中定義的但卻在內層函數中使用的,因此自由變量經常和"非本地變量"(non-local variable,熟悉Python的人確定知道)互用。例如:

function func1(x){
    var z;
    function func2(y){
        return x+y+z     # x和z既不是func2內部定義的,也不是func2的參數,因此x和z都是自由變量
    }
    return func1
}

自由變量和約束變量對應。所謂約束變量,是指這個變量以前是自由變量,但以後會對它進行賦值,將自由變量綁定到一個值上以後,這個變量就成爲約束變量或者稱爲綁定變量。

例如:

function func1(x){
    var m=20     # 對func2來講,這是自由變量,對其賦值,因此m變成了bound variable
    var z
    function func2(y){
        z=10       # 對自由變量z賦值,z變成bound variable
        return m+x+y+z     # m、x和z都是自由變量
    }
    return func1
}

ref_func=func1(3)       # 對x賦值,x變成bound variable

回調函數

回調函數一開始是C裏面的概念,它表示的是一個函數:

  • 能夠訪問另外一個函數
  • 當這個函數執行完了,會執行另外一個函數

也就是說,將一個函數(B)做爲參數傳遞給另外一個函數(A),但A執行完後,再自動調用B。因此這種回調函數的概念也稱爲"call after"。

可是如今回調函數已經足夠通用化了。通用化的回調函數定義爲:將函數B做爲另外一個函數A的參數,執行到函數A中某個地方的時候去調用B。和原來的概念相比,再也不是函數A結束後再調用,而是咱們本身定義在哪一個地方調用。

例如,Perl中的File::Find模塊中的find函數,經過這個函數加上回調函數,能夠實現和unix find命令相同的功能。例如,搜索某個目錄下的文件,而後print輸出這個文件名,即find /path xxx -print

#!/usr/bin/perl
use File::Find;

sub print_path {         # 定義一個函數,用於輸出路徑名稱
    print "$File::Find::name\n";
}

$callback = \&print_path;  # 建立一個函數引用,名爲$callback,因此perl是一種支持一級函數的語言

find( $callback,"/tmp" );  # 查找/tmp下的文件,每查找到一個文件,就執行一次$callback函數

這裏傳遞給find函數的$callback就是一個回調函數。幾個關鍵點:

  • $callback做爲參數傳遞給另外一個find()函數(因此find()函數是一個高階函數)
  • 在find()函數中,每查找到一個文件,就調用一次這個$callback函數。固然,若是find是咱們本身寫的程序,就能夠由咱們本身定義在什麼地方去調用$callback
  • $callback不是咱們主動調用的,而是由find()函數在某些狀況下(每查找到一個文件)去調用的

回調就像對函數進行填空答題同樣,根據咱們填入的內容去複用填入的函數從而實現某一方面的細節,而普通函數則是定義了就只能機械式地複用函數自己。

之因此稱爲回調函數,是由於這個函數並不是由咱們主觀地、直接地去調用,而是將函數做爲一個參數,經過被調用者間接去調用這個函數參數。本質上,回調函數和通常的函數沒有什麼區別,可能只是由於咱們定義一個函數,卻歷來沒有直接調用它,這一點感受上有點奇怪,因此有人稱之爲"回調函數",用來統稱這種間接的調用關係。

回調函數能夠被多線程異步執行。

完全搞懂閉包

計算機中的閉包概念是從數學世界引入的,在計算機程序語言中,它也稱爲詞法閉包、函數閉包。

閉包簡單的、通用的定義是指:函數引用一個詞法變量,在函數或語句塊結束後(變量的名稱消失),詞法變量仍然對引用它的函數有效。在下一節還有關於閉包更嚴格的定義(來自wiki)。

看一個python示例:函數f中嵌套了函數g,並返回函數g

def f(x):
    def g(y):
        return x + y
    return g  # 返回一個閉包:有名稱的函數(高階函數的特性)

# 將執行函數時返回的閉包函數賦值給變量(高階函數的特性)
a = f(1)

# 調用存儲在變量中閉包函數
print (a(5))

# 無需將閉包存儲進臨時變量,直接一次性調用閉包函數
print( f(1)(5) )   # f(1)是閉包函數,由於沒有將其賦值給變量,因此f(1)稱爲"匿名閉包"

上面的a是一個閉包,它是函數g()的一個實例。f()的參數x能夠被g訪問,在f()返回g函數後,f()就退出了,隨之消失的是變量名x(注意是變量名稱x,變量的值在這裏還不必定會消失)。當將閉包f(1)賦值給a後,原來x指向的數據對象(即數值1)仍被a指向的閉包函數引用着,因此x對應的值1在x消失後仍保存在內存中,只有當名爲a的閉包被消除後,原來x指向的數值1纔會消失。

閉包特性1:對於返回的每一個閉包g()來講,不一樣的g()引用不一樣的x對應的數據對象。換句話說,變量x對應的數據對象對每一個閉包來講都是相互獨立的

例以下面獲得兩個閉包,這兩個閉包中持有的自由變量雖然都引用相等的數值1,但兩個數值是不一樣數據對象,這兩個閉包也是相互獨立的:

a=f(1)
b=f(1)

閉包特性2:對於某個閉包函數來講,只要這不是一個匿名閉包,那麼閉包函數能夠一直訪問x對應的數據對象,即便名稱x已經消失

可是

a=f(1)      # 有名稱的閉包a,將一直引用數值對象1
a(3)        # 調用閉包函數a,將返回1+3=4,其中1是被a引用着的對象,即便a(3)執行完了也不放開
a(3)        # 再次調用函數a,將返回4,其中1和上面一條語句的1是同一個數據對象
f(1)(3)     # 調用匿名的閉包函數,數據對象1在f(1)(3)執行完就消失
f(1)(3)     # 調用匿名的閉包函數,和上面的匿名閉包是相互獨立的

最重要的特性就在於上面執行的兩次a(3):將詞法變量的生命週期延長,但卻足夠安全

看下面perl程序中的閉包函數,能夠更直觀地看到結果。

sub how_many {       # 定義函數
    my $count=2;     # 詞法變量$count
    return sub {print ++$count,"\n"};  # 返回一個匿名函數,這是一個匿名閉包
}

$ref=how_many();    # 將閉包賦值給變量$ref

how_many()->();     # (1)調用匿名閉包:輸出3
how_many()->();     # (2)調用匿名閉包:輸出3
$ref->();           # (3)調用命名閉包:輸出3
$ref->();           # (4)再次調用命名閉包:輸出4

上面將閉包賦值給$ref,經過$ref去調用這個閉包,則即便how_many中的$count在how_many()執行完就消失了,但$ref指向的閉包函數仍然在引用這個變量,因此屢次調用$ref會不斷修改$count的值,因此上面(3)和(4)先輸出3,而後輸出改變後的4。而上面(1)和(2)的輸出都是3,由於兩個how_many()函數返回的是獨立的匿名閉包,在語句執行完後數據對象3就消失了。

閉包更嚴格的定義

注意,嚴格定義的閉包和前面通俗定義的閉包結果上是不同的,通俗意義上的閉包並不必定符合嚴格意義上的閉包。

關於閉包更嚴格的定義,是一段誰都看不懂的說明(來自wiki)。以下,幾個關鍵詞我加粗顯示了,由於重要。

閉包是一種在支持一級函的編程語言中可以將詞法做用域中的變量名稱進行綁定的技術。在操做上,閉包是一種用於保存函數和環境的記錄。這個環境記錄了一些關聯性的映射,將函數的每一個自由變量與建立閉包時所綁定名稱的值或引用相關聯經過閉包,就算是在做用域外部調用函數,也容許函數經過閉包拷貝他們的值或經過引用的方式去訪問那些已經被捕獲的變量

我知道這段話誰都看不懂,因此簡而言之一下:一個函數實例和一個環境結合起來就是閉包。這個所謂的環境,決定了這個函數的特殊性,決定了閉包的特性。

仍是上面的python示例:函數f中嵌套了函數g,並返回函數g

def f(x):
    def g(y):
        return x + y
    return g  # 返回一個閉包:有名稱的函數

# 將執行函數時返回的閉包函數賦值給變量
a = f(1)

上面的a是一個閉包,它是函數g()的一個實例。f()的參數x能夠被g訪問,對於g()來講,這個x不是g()內部定義的,也不是g()的參數,因此這個x對於g來講是一個自由變量(free variable)。雖然g()中持有了自由變量,可是g()函數自身不是閉包函數,只有在g持有的自由變量x和傳遞給f()函數的x的值(即f(1)中的1)進行綁定的時候,纔會從g()建立一個閉包函數,這表示閉包函數開始引用這個自由變量,而且這個閉包一直持有這個變量的引用,即便f()已經執行完畢了。而後在f()中return這個閉包函數,由於這個閉包函數綁定了(引用)自由變量x,這就是閉包函數所在的環境。

環境對閉包來講很是重要,是區別普通函數和閉包的關鍵。若是返回的每一個閉包不是獨立持有屬於本身的自由變量,而是全部閉包都持有徹底相同的自由變量,那麼閉包雖然仍可稱爲閉包,但和普通函數卻沒有區別了。例如:

def f(x):
    x=3
    def g(y):
        return x + y
    return g

a = f(1)
b = f(3)

在上面的示例中,x雖然是自由變量,但卻在g()的定義以前就綁定了值(前文介紹過,它稱爲bound variable),使得閉包a和閉包b持有的再也不是自由變量,而是值對象徹底相同的綁定變量,其值對象爲3,a和b這個時候其實沒有任何區別(雖然它們是不一樣對象)。換句話說,有了閉包a就徹底沒有必要再定義另外一個功能上徹底相同的閉包b。

在函數複用性的角度上來講,這裏的a和普通函數沒有任何區別,都只是簡單地複用了函數體。而真正嚴格意義上的閉包,除了複用函數體,還複用它所在的環境。

可是這樣一種狀況,對於通俗定義的閉包來講,返回的g()也是一個閉包,但在嚴格定義的閉包中,這已經不算是閉包。

再看一個示例:將自由變量x放在g()函數定義文本段的後面。

def f(y):
    return x+y

x=1

def g(z):
    x=3
    return f(z)

print(g(1))   # 輸出2,而不是4

首先要說明的是,python在沒有給任何做用域修飾符的時候實現的詞法做用域規則,因此上面return f(z)中的f()看見的是全局變量x(由於f()定義在全局文本段中),而不是g()中的x=3。

回到閉包問題上。上面f()持有一個自由變量x,這個f(z)的文本定義段是在全局文本段中,它綁定的自由變量x是全局變量(聲明並初始化爲空或0),可是這個變量以後賦值爲1了。對於g()中返回的每一個f()所在的環境來講,它持有的自由變量x一開始都是不肯定的,可是後來都肯定爲1了。這種狀況也不能稱之爲閉包,由於閉包是在f()對自由變量進行綁定時建立的,而這個時候x已是固定的值對象了。

回調函數、閉包和匿名函數

回調函數、閉包和匿名函數其實沒有必然的關係,但由於不少書上都將匿名函數和回調函數、閉包放在一塊兒解釋,讓人誤覺得回調函數、閉包須要經過匿名函數實現。實際上,匿名函數只是一個有函數定義文本段,卻沒有名稱的函數,而閉包則是一個函數的實例加上一個環境(嚴格意義上的定義)。

對於閉包和匿名函數來講,仍然以python爲例:

def f(x):
    def g(y):
        return x + y
    return g    # 返回一個閉包:有名稱的函數

def h(x):
    return lambda y: x + y  # 返回一個閉包:匿名函數

# 將執行函數時返回的閉包函數賦值給變量
a = f(1)
b = h(1)

# 調用存儲在變量中閉包函數
print (a(5))
print (b(5))

對於回調函數和匿名函數來講,仍然以perl的find函數爲例:

#!/usr/bin/perl
use File::Find;

$callback = sub {
    print "$File::Find::name\n";
};  # 建立一個匿名函數以及它的引用

find( $callback,"/tmp" );  # 查找/tmp下的文件,每查找到一個文件,就執行一次$callback函數

匿名函數讓閉包的實現更簡潔,因此不少時候返回的閉包函數就是一個匿名函數實例。

相關文章
相關標籤/搜索