自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

徹底理解閉包實(shí)現(xiàn)原理

開發(fā) 前端
這篇文章的主要目的就是從編譯器的角度來(lái)分析閉包,徹底搞懂閉包的實(shí)現(xiàn)原理。

前言

閉包對(duì)于一個(gè)長(zhǎng)期寫 Java 的開發(fā)者來(lái)說估計(jì)鮮有耳聞,我在寫 Python 和 Go 之前也是沒怎么了解,光這名字感覺就有點(diǎn)"神秘莫測(cè)",這篇文章的主要目的就是從編譯器的角度來(lái)分析閉包,徹底搞懂閉包的實(shí)現(xiàn)原理。

函數(shù)一等公民

一門語(yǔ)言在實(shí)現(xiàn)閉包之前首先要具有的特性就是:First class function 函數(shù)是第一公民。

簡(jiǎn)單來(lái)說就是函數(shù)可以像一個(gè)普通的值一樣在函數(shù)中傳遞,也能對(duì)變量賦值。

先來(lái)看看在 Go 里是如何編寫的:

package main
import "fmt"
var varExternal int

func f1() func(int) int {
varInner := 20
innerFun := func(a int) int {
fmt.Println(a)
varExternal++
varInner++
return varInner
}
return innerFun
}
func main() {
varExternal = 10
f2 := f1()
for i := 0; i < 2; i++ {
fmt.Printf("varInner=%d, varExternal=%d \n", f2(i), varExternal)
}
fmt.Println("======")

f3 := f1()
for i := 0; i < 2; i++ {
fmt.Printf("varInner=%d, varExternal=%d \n", f3(i), varExternal)
}
}
// Output:
0
varInner=21, varExternal=11
1
varInner=22, varExternal=12
======
0
varInner=21, varExternal=13
1
varInner=22, varExternal=14

這里體現(xiàn)了閉包的兩個(gè)重要特性,第一個(gè)自然就是函數(shù)可以作為值返回,同時(shí)也能賦值給變量。

第二個(gè)就是在閉包函數(shù) f1() 對(duì)閉包變量 varInner 的訪問,每個(gè)閉包函數(shù)的引用都會(huì)在自己的函數(shù)內(nèi)部保存一份閉包變量 varInner,這樣在調(diào)用過程中就不會(huì)互相影響。

從打印的結(jié)果中也能看出這個(gè)特性。

作用域

閉包之所以不太好理解的主要原因是它不太符合自覺。

圖片

本質(zhì)上就是作用域的關(guān)系,當(dāng)我們調(diào)用 f1()? 函數(shù)的時(shí)候,會(huì)在棧中分配變量 varInner?,正常情況下調(diào)用完畢后 f1? 的棧會(huì)彈出,里面的變量 varInner 自然也會(huì)銷毀才對(duì)。

但在后續(xù)的 f2()? 和 f3()? 調(diào)用的時(shí),卻依然能訪問到 varInner,就這點(diǎn)不符合我們對(duì)函數(shù)調(diào)用的直覺。

但其實(shí)換個(gè)角度來(lái)看,對(duì) innerFun? 來(lái)說,他能訪問到 varExternal? 和 varInner? 變量,最外層的 varExternal 就不用說了,一定是可以訪問的。

但對(duì)于 varInner 來(lái)說就不一定了,這里得分為兩種情況;重點(diǎn)得看該語(yǔ)言是靜態(tài)/動(dòng)態(tài)作用域。

就靜態(tài)作用域來(lái)說,每個(gè)符號(hào)在編譯器就確定好了樹狀關(guān)系,運(yùn)行時(shí)不會(huì)發(fā)生變化;也就是說 varInner? 對(duì)于 innerFun 這個(gè)函數(shù)來(lái)說在編譯期已經(jīng)確定可以訪問了,在運(yùn)行時(shí)自然也是可以訪問的。

但對(duì)于動(dòng)態(tài)作用域來(lái)說,完全是在運(yùn)行時(shí)才確定訪問的變量是哪一個(gè)。

恰好 Go? 就是一個(gè)靜態(tài)作用域的語(yǔ)言,所以返回的 innerFun? 函數(shù)可以一直訪問到 varInner 變量。

實(shí)現(xiàn)閉包

但 Go 是如何做到在 f1() 函數(shù)退出之后依然能訪問到 f1() 中的變量呢?

這里我們不妨大膽假設(shè)一下:

首先在編譯期掃描出哪些是閉包變量,也就是這里的 varInner,需要將他保存到函數(shù) innerFun() 中。

f2 := f1()
f2()

運(yùn)行時(shí)需要判斷出 f2 是一個(gè)函數(shù),而不是一個(gè)變量,同時(shí)得知道它所包含的函數(shù)體是 innerFun() 所定義的。

接著便是執(zhí)行函數(shù)體的 statement 即可。

而當(dāng) f3 := f1() 重新賦值給 f3 時(shí),在 f2 中累加的 varInner 變量將不會(huì)影響到 f3,這就得需要在給 f3 賦值的重新賦值一份閉包變量到  f3 中,這樣便能達(dá)到互不影響的效果。

閉包掃描

GScript 本身也是支持閉包的,所以把 Go 的代碼翻譯過來(lái)便長(zhǎng)這樣:

int varExternal =10;
func int(int) f1(){
int varInner = 20;
int innerFun(int a){
println(a);
int c=100;
varExternal++;
varInner++;
return varInner;
}
return innerFun;
}
func int(int)= f1();
for(int i=0;i<2;i++){
println("varInner=" + f2(i) + ", varExternal=" + varExternal);
}
println("=======");
func int(int)= f1();
for(int i=0;i<2;i++){
println("varInner=" + f3(i) + ", varExternal=" + varExternal);
}
// Output:
0
varInner=21, varExternal=11
1
varInner=22, varExternal=12
=======
0
varInner=21, varExternal=13
1
varInner=22, varExternal=14

可以看到運(yùn)行結(jié)果和 Go 的一樣,所以我們來(lái)看看 GScript 是如何實(shí)現(xiàn)的便也能理解 Go 的原理了。

先來(lái)看看第一步掃描閉包變量:

圖片

allVariable := c.allVariable(function)查詢所有的變量,包括父 scope 的變量。

scopeVariable := c.currentScopeVariable(function)查詢當(dāng)前 scope 包含下級(jí)所有 scope 中的變量,這樣一減之后就能知道閉包變量了,然后將所有的閉包變量存放進(jìn)閉包函數(shù)中。

閉包賦值

之后在 return innerFun 處,將閉包變量的數(shù)據(jù)賦值到變量中。

圖片

閉包函數(shù)調(diào)用

func int(int)= f1();
func int(int)= f1();

在這里每一次賦值時(shí),都會(huì)把 f1() 返回函數(shù)復(fù)制到變量 f2/f3 中,這樣兩者所包含的閉包變量就不會(huì)互相影響。

圖片

在調(diào)用函數(shù)變量時(shí),判斷到該變量是一個(gè)函數(shù),則直接返回函數(shù)。

之后直接調(diào)用該函數(shù)即可。

函數(shù)式編程

接下來(lái)便可以利用 First class function 來(lái)試試函數(shù)式編程:


class Test{
int value=0;
Test(int v){
value=v;
}
int map(func int(int){
return f(value);
}
}
int square(int v){
return v*v;
}
int add(int v){
return v++;
}
int add2(int v){
v=v+2;
return v;
}
Test t =Test(100);
func int(int)= square;
func int(int)= add;
func int(int)= add2;
println(t.map(s));
assertEqual(t.map(s),10000);

println(t.map(a));
assertEqual(t.map(a),101);

println(t.map(a2));
assertEqual(t.map(a2),102);

這個(gè)有點(diǎn)類似于 Java 中流的 map 函數(shù),將函數(shù)作為值傳遞進(jìn)去,后續(xù)支持匿名函數(shù)后會(huì)更像是函數(shù)式編程,現(xiàn)在必須得先定義一個(gè)函數(shù)變量再進(jìn)行傳遞。

除此之外在 GScript 中的 http 標(biāo)準(zhǔn)庫(kù)也利用了函數(shù)是一等公民的特性:

// 標(biāo)準(zhǔn)庫(kù):Bind route
httpHandle(string method, string path, func (HttpContext) handle){
HttpContext ctx = HttpContext();
handle(ctx);
}

在綁定路由時(shí),handle 便是一個(gè)函數(shù),使用的時(shí)候直接傳遞業(yè)務(wù)邏輯的 handle 即可:

func (HttpContext) handle (HttpContext ctx){
Person p = Person();
p.name = "abc";
println("p.name=" + p.name);
println("ctx=" + ctx);
ctx.JSON(200, p);
}
httpHandle("get", "/p", handle);

總結(jié)

總的來(lái)說閉包具有以下特性:

  • 函數(shù)需要作為一等公民。
  • 編譯期掃描出所有的閉包變量。
  • 在返回閉包函數(shù)時(shí),為閉包變量賦值。
  • 每次創(chuàng)建新的函數(shù)變量時(shí),需要將閉包數(shù)據(jù)復(fù)制進(jìn)去,這樣閉包變量才不會(huì)互相影響。
  • 調(diào)用函數(shù)變量時(shí),需要判斷為函數(shù),而不是變量。

圖片

可以在 Playground 中體驗(yàn)閉包函數(shù)打印裴波那切數(shù)列的運(yùn)用。

本文相關(guān)資源鏈接

  • GScript 源碼:https://github.com/crossoverJie/gscript。
  • Playground 源碼:https://github.com/crossoverJie/gscript-homepage。
責(zé)任編輯:姜華 來(lái)源: crossoverJie
相關(guān)推薦

2016-10-27 19:26:47

Javascript閉包

2016-09-18 20:53:16

JavaScript閉包前端

2017-05-22 16:08:30

前端開發(fā)javascript閉包

2010-07-26 11:27:58

Perl閉包

2011-03-02 12:33:00

JavaScript

2022-05-06 16:18:00

Block和 C++OC 類lambda

2011-08-05 09:33:30

Func局部變量作用域

2021-10-26 13:18:52

Go底層函數(shù)

2024-01-22 09:51:32

Swift閉包表達(dá)式尾隨閉包

2022-05-04 10:38:58

React閉包組件

2021-09-04 07:29:57

Android

2020-10-14 09:11:44

IO 多路復(fù)用實(shí)現(xiàn)機(jī)

2022-05-05 08:31:48

useRefuseEffecthook

2020-02-12 16:58:15

JavaScript前端技術(shù)

2021-05-13 08:55:33

Android架構(gòu)功能

2021-02-21 16:21:19

JavaScript閉包前端

2022-05-11 07:38:45

SpringWebFlux

2020-05-07 18:39:15

數(shù)據(jù)庫(kù)閉源開源

2020-08-10 15:24:05

Snowflake算法開源

2024-11-26 00:45:29

free區(qū)域字段
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)