Persekitaran RujukanPenutupan = Persekitaran Rujukan Fungsi Perkataan bahasa Go tidak boleh ditemui dalam definisi di atas Pelajar pintar mesti tahu bahawa penutupan tidak ada kaitan dengan bahasa Ia bukan unik untuk JavaScript atau Go, tetapi Bahasa pengaturcaraan berfungsi. ya, anda membacanya dengan betul, mana-mana bahasa yang menyokong pengaturcaraan berfungsi menyokong penutupan, Go dan JavaScript adalah dua daripadanya Versi semasa Java juga menyokong penutupan , Tetapi sesetengah orang mungkin berpendapat bahawa ia bukan yang sempurna. penutupan, butiran dibincangkan dalam artikel.
1.3 Cara menulis penutup
1.3.1 Pertama lihat penutupan
Yang berikut ialah Sekeping kod penutupan: import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
var sum = func() int {
fmt.Println("求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
return sum
}
Hasil keluaran: 先获取函数,不求结果
等待一会
求结果...
结果: 15
Dapat dilihat bahawa kaedah di dalam boleh merujuk kepada parameter dan pembolehubah tempatan bagi luaran fungsi sum()
, apabila lazySum()
mengembalikan fungsi lazySum()
, parameter dan pembolehubah yang berkaitan disimpan dalam fungsi yang dikembalikan dan boleh dipanggil kemudian. sum()
Fungsi di atas mungkin melangkah lebih jauh untuk mencerminkan fungsi yang digabungkan dan keadaan sekelilingnya Kami menambah nombor : count
import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", sumFunc())
}func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
} return result
} return sum
}
Apakah yang dikeluarkan oleh kod di atas? Adakah bilangan kali berubah? count
jelas merupakan pembolehubah setempat bagi fungsi luar, tetapi dalam rujukan fungsi ingatan (penggabungan), fungsi dalam didedahkan seperti berikut: count
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
第 2 次求结果...
结果: 15
第 3 次求结果...
结果: 15
Fungsi dalaman terdedah dan dirujuk oleh tempat
selain daripada fungsi count
luar, membentuk penutupan.
- Sesetengah orang mungkin mempunyai soalan pada ketika ini Dikatakan bahawa
dicipta sekali dan dilaksanakan 3 kali Tetapi apakah yang akan berlaku jika 3 eksekusi semuanya adalah ciptaan yang berbeza? Eksperimen: -
Keputusan pelaksanaan adalah seperti berikut, setiap pelaksanaan adalah kali pertama:
Seperti yang dapat dilihat daripada keputusan pelaksanaan di atas: lazySum()
import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
sumFunc1 := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc1())
sumFunc2 := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc2())
}func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
} return result
} return sum
}
Apabila penutupan dibuat, salinan pembolehubah luaran yang dirujuk telah pun dibuat, iaitu, tidak kira sama ada ia dipanggil secara berasingan
. 先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
Teruskan mengemukakan soalan, **Jika fungsi mengembalikan dua fungsi, adakah ini satu penutupan atau dua penutupan? **Mari kita amalkan di bawah:
Kembalikan dua fungsi pada satu masa, satu untuk mengira hasil jumlah dan satu untuk mengira hasil: count
Hasil berjalan adalah seperti berikut:
Ia boleh dilihat daripada keputusan di atas bahawa apabila fungsi mengembalikan fungsi, tidak kira berapa banyak nilai pulangan (fungsi) yang ada, ia adalah penutupan Jika fungsi dikembalikan menggunakan pembolehubah fungsi luaran, ia akan terikat kepada Bersama, ia mempengaruhi satu sama lain: import "fmt"
func main() {
sumFunc, productSFunc := lazyCalculate([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
fmt.Println("结果:", productSFunc())
}func lazyCalculate(arr []int) (func() int, func() int) {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求加和...")
result := 0
for _, v := range arr {
result = result + v
} return result
} var product = func() int {
count++
fmt.Println("第", count, "次求乘积...")
result := 0
for _, v := range arr {
result = result * v
} return result
} return sum, product
}
Penutupan mengikat keadaan sekeliling Saya faham bahawa fungsi pada masa ini mempunyai keadaan , membenarkan fungsi mempunyai semua keupayaan objek Fungsi Mempunyai status. 先获取函数,不求结果
等待一会
第 1 次求加和...
结果: 15
第 2 次求乘积...
结果: 0
1.3.2 Penunjuk dan nilai dalam penutup Dalam contoh di atas, semua nilai yang digunakan dalam penutupan kami adalah nilai berangka penunjuk, ia akan menjadi Bagaimana?
Keputusan yang dijalankan adalah seperti berikut:
Dapat dilihat bahawa jika ia adalah penunjuk, nilai alamat yang sepadan dengan penunjuk diubah suai dalam penutupan , yang juga akan menjejaskan nilai di luar penutupan. Ini sebenarnya sangat mudah difahami. Tiada rujukan lulus dalam Go, hanya lulus nilai Apabila kita lulus penunjuk, nilai di sini adalah nilai penunjuk (yang boleh difahami sebagai alamat nilai). Apabila parameter fungsi kita ialah penunjuk, parameter tersebut akan menyalin alamat penunjuk dan menghantarnya sebagai parameter Kerana intipatinya masih merupakan alamat, apabila ia diubah suai secara dalaman, ia masih boleh memberi kesan di luar. import "fmt"
func main() {
i := 0
testFunc := test(&i)
testFunc()
fmt.Printf("outer i = %d\n", i)
}func test(i *int) func() {
*i = *i + 1
fmt.Printf("test inner i = %d\n", *i) return func() {
*i = *i + 1
fmt.Printf("func inner i = %d\n", *i)
}
}
Data dalam penutupan sebenarnya mempunyai alamat yang sama Percubaan berikut boleh membuktikannya:
test inner i = 1
func inner i = 2
outer i = 2
Oleh itu, boleh disimpulkan bahawa jika penutupan merujuk kepada data penuding persekitaran luaran, Ia hanya menyalin data alamat penuding, dan bukannya menyalin data sebenar (==Tinggalkan soalan dahulu: bila masa salinan==):
1.3. 2 Penangguhan penutupan
func main() {
i := 0
testFunc := test(&i)
testFunc()
fmt.Printf("outer i address %v\n", &i)
}
func test(i *int) func() {
*i = *i + 1
fmt.Printf("test inner i address %v\n", i)
return func() {
*i = *i + 1
fmt.Printf("func inner i address %v\n", i)
}
}
Contoh di atas nampaknya memberitahu kita bahawa apabila penutupan dibuat, data telah disalin, tetapi adakah ini benar-benar berlaku?
test inner i address 0xc0003fab98
func inner i address 0xc0003fab98
outer i address 0xc0003fab98
Berikut ialah kesinambungan percubaan sebelumnya:
Selepas kami membuat penutupan, kami menukar data dan kemudian melaksanakan penutupan jawapannya mestilah ia benar-benar mempengaruhi pelaksanaan penutupan, kerana Semuanya adalah penunjuk, menunjuk kepada data yang sama:
Katakan kita menukar cara penulisan dan biarkan pembolehubah dalam persekitaran luaran penutupan diubah suai selepas mengisytiharkan penutupan fungsi: func main() {
i := 0
testFunc := test(&i)
i = i + 100
fmt.Printf("outer i before testFunc %d\n", i)
testFunc()
fmt.Printf("outer i after testFunc %d\n", i)
}func test(i *int) func() {
*i = *i + 1
fmt.Printf("test inner i = %d\n", *i)
return func() {
*i = *i + 1
fmt.Printf("func inner i = %d\n", *i)
}
}
Hasil pelaksanaan sebenar, akan menjadi nilai yang diubah suai:
test inner i = 1
outer i before testFunc 101
func inner i = 102
outer i after testFunc 102
Ini juga membuktikan bahawa sebenarnya, penutupan tidak akan diisytiharkan selepas hukuman
Ikat import "fmt"
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
count = count + 100
return sum
}
persekitaran luaran pada penutupan, tetapi hanya ikat apabila fungsi mengembalikan fungsi penutupan Ini adalah ikatan tertunda count
.
如果还没看明白没关系,我们再来一个例子:
func main() {
funcs := testFunc(100)
for _, v := range funcs {
v()
}
}
func testFunc(x int) []func() {
var funcs []func()
values := []int{1, 2, 3}
for _, val := range values {
funcs = append(funcs, func() {
fmt.Printf("testFunc val = %d\n", x+val)
})
}
return funcs
}
上面的例子,我们闭包返回的是函数数组,本意我们想入每一个 val
都不一样,但是实际上 val
都是一个值,==也就是执行到return funcs
的时候(或者真正执行闭包函数的时候)才绑定的 val
值==(关于这一点,后面还有个Demo可以证明),此时 val
的值是最后一个 3
,最终输出结果都是 103
:
testFunc val = 103
testFunc val = 103
testFunc val = 103
以上两个例子,都是闭包延迟绑定的问题导致,这也可以说是 feature,到这里可能不少同学还是对闭包绑定外部变量的时机有疑惑,到底是返回闭包函数的时候绑定的呢?还是真正执行闭包函数的时候才绑定的呢?
下面的例子可以有效的解答:
import (
"fmt"
"time"
)
func main() {
sumFunc := lazySum([]int{1, 2, 3, 4, 5})
fmt.Println("等待一会")
fmt.Println("结果:", sumFunc())
time.Sleep(time.Duration(3) * time.Second)
fmt.Println("结果:", sumFunc())
}
func lazySum(arr []int) func() int {
fmt.Println("先获取函数,不求结果")
count := 0
var sum = func() int {
count++
fmt.Println("第", count, "次求结果...")
result := 0
for _, v := range arr {
result = result + v
}
return result
}
go func() {
time.Sleep(time.Duration(1) * time.Second)
count = count + 100
fmt.Println("go func 修改后的变量 count:", count)
}()
return sum
}
输出结果如下:
先获取函数,不求结果
等待一会
第 1 次求结果...
结果: 15
go func 修改后的变量 count: 101
第 102 次求结果...
结果: 15
第二次执行闭包函数的时候,明显 count
被里面的 go func()
修改了,也就是调用的时候,才真正的获取最新的外部环境,但是在声明的时候,就会把环境预留保存下来。
其实本质上,Go Routine的匿名函数的延迟绑定就是闭包的延迟绑定,上面的例子中,go func(){}
获取到的就是最新的值,而不是原始值0
。
总结一下上面的验证点:
- 闭包每次返回都是一个新的实例,每个实例都有一份自己的环境。
- 同一个实例多次执行,会使用相同的环境。
- 闭包如果逃逸的是指针,会相互影响,因为绑定的是指针,相同指针的内容修改会相互影响。
- 闭包并不是在声明时绑定的值,声明后只是预留了外部环境(逃逸分析),真正执行闭包函数时,会获取最新的外部环境的值(也称为延迟绑定)。
- Go Routine的匿名函数的延迟绑定本质上就是闭包的延迟绑定。
2、闭包的好处与坏处?
2.1 好处
纯函数没有状态,而闭包则是让函数轻松拥有了状态。但是凡事都有两面性,一旦拥有状态,多次调用,可能会出现不一样的结果,就像是前面测试的 case 中一样。那么问题来了:
Q:如果不支持闭包的话,我们想要函数拥有状态,需要怎么做呢?
A: 需要使用全局变量,让所有函数共享同一份变量。
但是我们都知道全局变量有以下的一些特点(在不同的场景,优点会变成缺点):
- 常驻于内存之中,只要程序不停会一直在内存中。
- 污染全局,大家都可以访问,共享的同时不知道谁会改这个变量。
闭包可以一定程度优化这个问题:
- 不需要使用全局变量,外部函数局部变量在闭包的时候会创建一份,生命周期与函数生命周期一致,闭包函数不再被引用的时候,就可以回收了。
- 闭包暴露的局部变量,外界无法直接访问,只能通过函数操作,可以避免滥用。
除了以上的好处,像在 JavaScript 中,没有原生支持私有方法,可以靠闭包来模拟私有方法,因为闭包都有自己的词法环境。
2.2 坏处
函数拥有状态,如果处理不当,会导致闭包中的变量被误改,但这是编码者应该考虑的问题,是预期中的场景。
闭包中如果随意创建,引用被持有,则无法销毁,同时闭包内的局部变量也无法销毁,过度使用闭包会占有更多的内存,导致性能下降。一般而言,能共享一份闭包(共享闭包局部变量数据),不需要多次创建闭包函数,是比较优雅的方式。
3、闭包怎么实现的?
从上面的实验中,我们可以知道,闭包实际上就是外部环境的逃逸,跟随着闭包函数一起暴露出去。
我们用以下的程序进行分析:
import "fmt"
func testFunc(i int) func() int {
i = i * 2
testFunc := func() int {
i++
return i
}
i = i * 2
return testFunc
}
func main() {
test := testFunc(1)
fmt.Println(test())
}
执行结果如下:
5
先看看逃逸分析,用下面的命令行可以查看:
go build --gcflags=-m main.go
可以看到 变量 i
被移到堆中,也就是本来是局部变量,但是发生逃逸之后,从栈里面放到堆里面,同样的 test()
函数由于是闭包函数,也逃逸到堆上。
下面我们用命令行来看看汇编代码:
go tool compile -N -l -S main.go
生成代码比较长,我截取一部分:
"".testFunc STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0
0x0000 00000 (main.go:5) TEXT "".testFunc(SB), ABIInternal, $56-8
0x0000 00000 (main.go:5) CMPQ SP, 16(R14)
0x0004 00004 (main.go:5) PCDATA $0, $-2
0x0004 00004 (main.go:5) JLS 198
0x000a 00010 (main.go:5) PCDATA $0, $-1
0x000a 00010 (main.go:5) SUBQ $56, SP
0x000e 00014 (main.go:5) MOVQ BP, 48(SP)
0x0013 00019 (main.go:5) LEAQ 48(SP), BP
0x0018 00024 (main.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0018 00024 (main.go:5) FUNCDATA $1, gclocals·d571c0f6cf0af59df28f76498f639cf2(SB)
0x0018 00024 (main.go:5) FUNCDATA $5, "".testFunc.arginfo1(SB)
0x0018 00024 (main.go:5) MOVQ AX, "".i+64(SP)
0x001d 00029 (main.go:5) MOVQ $0, "".~r0+16(SP)
0x0026 00038 (main.go:5) LEAQ type.int(SB), AX
0x002d 00045 (main.go:5) PCDATA $1, $0
0x002d 00045 (main.go:5) CALL runtime.newobject(SB)
0x0032 00050 (main.go:5) MOVQ AX, "".&i+40(SP)
0x0037 00055 (main.go:5) MOVQ "".i+64(SP), CX
0x003c 00060 (main.go:5) MOVQ CX, (AX)
0x003f 00063 (main.go:6) MOVQ "".&i+40(SP), CX
0x0044 00068 (main.go:6) MOVQ "".&i+40(SP), DX
0x0049 00073 (main.go:6) MOVQ (DX), DX
0x004c 00076 (main.go:6) SHLQ $1, DX
0x004f 00079 (main.go:6) MOVQ DX, (CX)
0x0052 00082 (main.go:7) LEAQ type.noalg.struct { F uintptr; "".i *int }(SB), AX
0x0059 00089 (main.go:7) PCDATA $1, $1
0x0059 00089 (main.go:7) CALL runtime.newobject(SB)
0x005e 00094 (main.go:7) MOVQ AX, ""..autotmp_3+32(SP)
0x0063 00099 (main.go:7) LEAQ "".testFunc.func1(SB), CX
0x006a 00106 (main.go:7) MOVQ CX, (AX)
0x006d 00109 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x0072 00114 (main.go:7) TESTB AL, (CX)
0x0074 00116 (main.go:7) MOVQ "".&i+40(SP), DX
0x0079 00121 (main.go:7) LEAQ 8(CX), DI
0x007d 00125 (main.go:7) PCDATA $0, $-2
0x007d 00125 (main.go:7) CMPL runtime.writeBarrier(SB), $0
0x0084 00132 (main.go:7) JEQ 136
0x0086 00134 (main.go:7) JMP 142
0x0088 00136 (main.go:7) MOVQ DX, 8(CX)
0x008c 00140 (main.go:7) JMP 149
0x008e 00142 (main.go:7) CALL runtime.gcWriteBarrierDX(SB)
0x0093 00147 (main.go:7) JMP 149
0x0095 00149 (main.go:7) PCDATA $0, $-1
0x0095 00149 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x009a 00154 (main.go:7) MOVQ CX, "".testFunc+24(SP)
0x009f 00159 (main.go:11) MOVQ "".&i+40(SP), CX
0x00a4 00164 (main.go:11) MOVQ "".&i+40(SP), DX
0x00a9 00169 (main.go:11) MOVQ (DX), DX
0x00ac 00172 (main.go:11) SHLQ $1, DX
0x00af 00175 (main.go:11) MOVQ DX, (CX)
0x00b2 00178 (main.go:12) MOVQ "".testFunc+24(SP), AX
0x00b7 00183 (main.go:12) MOVQ AX, "".~r0+16(SP)
0x00bc 00188 (main.go:12) MOVQ 48(SP), BP
0x00c1 00193 (main.go:12) ADDQ $56, SP
0x00c5 00197 (main.go:12) RET
0x00c6 00198 (main.go:12) NOP
0x00c6 00198 (main.go:5) PCDATA $1, $-1
0x00c6 00198 (main.go:5) PCDATA $0, $-2
0x00c6 00198 (main.go:5) MOVQ AX, 8(SP)
0x00cb 00203 (main.go:5) CALL runtime.morestack_noctxt(SB)
0x00d0 00208 (main.go:5) MOVQ 8(SP), AX
0x00d5 00213 (main.go:5) PCDATA $0, $-1
0x00d5 00213 (main.go:5) JMP 0
可以看到闭包函数实际上底层也是用结构体new
创建出来的:
menggunakan i
pada timbunan:
Iaitu, apabila mengembalikan fungsi, ia sebenarnya mengembalikan struktur, Struktur merekodkan persekitaran rujukan fungsi.
4. Mari kita bincang secara ringkas
4.1 Adakah Java menyokong penutupan?
Terdapat banyak pendapat di Internet, walaupun pada masa ini Java tidak menyokong fungsi pulangan sebagai parameter pulangan, Java pada dasarnya melaksanakan konsep penutupan, dan kaedah yang digunakan ialah Bentuk. daripada kelas dalam, kerana ia adalah kelas dalam, adalah bersamaan dengan mempunyai persekitaran rujukan, yang dianggap sebagai penutupan yang tidak lengkap.
Pada masa ini terdapat sekatan tertentu Contohnya, hanya yang diisytiharkan oleh final
atau nilai yang ditakrifkan dengan jelas boleh diluluskan:
Terdapat jawapan yang berkaitan pada Stack Overflow: stackoverflow. .com /questions/5…
4.2 Apakah masa depan pengaturcaraan berfungsi?
Berikut ialah kandungan daripada Wiki:
Pengaturcaraan fungsional telah lama popular di kalangan akademik tetapi mempunyai beberapa aplikasi industri. Sebab utama bagi situasi ini ialah pengaturcaraan berfungsi sering dianggap menggunakan CPU dan sumber memori secara serius [18] Ini kerana isu kecekapan tidak dipertimbangkan dalam pelaksanaan awal bahasa pengaturcaraan berfungsi, dan ia telah berlaku berorientasikan fungsi. Ciri pengaturcaraan formula, seperti memastikan ketelusan rujukan, dsb., memerlukan struktur data dan algoritma yang unik. [19]
Walau bagaimanapun, baru-baru ini beberapa bahasa pengaturcaraan berfungsi telah digunakan dalam sistem komersial atau perindustrian [20], contohnya:
-
Erlang, yang dibangunkan pada akhir 1980-an oleh syarikat Sweden Ericsson, pada asalnya digunakan untuk melaksanakan sistem telekomunikasi toleran kesalahan. Sejak itu ia telah digunakan sebagai bahasa popular untuk mencipta satu siri aplikasi oleh syarikat seperti Nortel, Facebook, Facebook, Électricité de France dan WhatsApp. [21][22]Skim, yang digunakan untuk awal Apple MacintoshAsas untuk beberapa aplikasi pada komputer, dan baru-baru ini telah digunakan dalam arah seperti perisian simulasi latihan dan kawalan teleskop.
-
OCaml, yang dilancarkan pada pertengahan 1990-an, telah menemui aplikasi komersial dalam bidang seperti analisis kewangan, pengesahan pemandu, pengaturcaraan robot industri dan analisis statik perisian terbenam. Haskell, walaupun pada asalnya ia digunakan sebagai bahasa penyelidikan, ia juga telah digunakan oleh beberapa syarikat dalam sistem aeroangkasa, reka bentuk perkakasan, pengaturcaraan rangkaian dan bidang lain.
Bahasa pengaturcaraan berfungsi lain yang digunakan dalam industri termasuk pelbagai paradigma Scala[23], F# dan Terdapat Bahasa Wolfram, Bincang Biasa, ML Standard dan Clojure, dsb.
Dari sudut pandangan peribadi saya, saya tidak optimistik tentang pengaturcaraan berfungsi tulen, tetapi saya percaya bahawa hampir setiap keperluan pengaturcaraan lanjutan akan mempunyai idea pengaturcaraan berfungsi pada masa hadapan maju ke Java yang menerima pengaturcaraan berfungsi. Daripada bahasa yang saya tahu, ciri pengaturcaraan berfungsi dalam Go dan JavaScript sangat disukai oleh pembangun (sudah tentu, jika anda menulis pepijat, anda akan membencinya).
Sebab mengapa ia tiba-tiba menjadi popular baru-baru ini juga kerana dunia terus berkembang dan ingatan semakin besar dan semakin besar Batasan faktor ini hampir dibebaskan.
Saya percaya bahawa dunia ini berwarna-warni Adalah mustahil untuk satu perkara untuk menguasai dunia boleh menjadi tuan pada masa hadapan, dan akhirnya sejarah akan Menapis mereka yang akhirnya selaras dengan pembangunan masyarakat manusia.
Untuk lebih banyak pengetahuan berkaitan pengaturcaraan, sila lawati: Video Pengaturcaraan! !