python质数列_现代化程序开发笔记(7)——闭包

本系列文章以我的个人博客的搭建为线索(GitHub 仓库:Evian-Zhang/evian-blog),记录我在现代化程序设计中的一些笔记。在这篇文章中,我将讨论的是现代语言中的闭包。

函数起名之痛

在现代语言中,函数是一等公民的思想几乎贯彻在了每一个编程语言中,函数应该和变量一样,能够自由地作为参数和返回值在函数间传递。比方说,一个游戏的开发者在写代码的时候发现,有好几种怪物的代码都极其类似,但只有其进行打斗的样子不同。因此,开发者将这几个怪物的代码封装成同一个函数monster,那么怎样区别这几种怪物打斗的形态呢?只能传递其打斗的函数进去了。

拿Swift为例,在没有闭包之前,开发者可以这么写:

// monster 1

func monster1_fight(with hero: Hero) { }

// monster 2

func monster2_fight(with hero: Hero) { }

func foo() {

let monster1 = monster(monster1_fight)

let monster2 = monster(monster2_fight)

}

将各自打斗的函数传递给monster就好了。

后来,为了调试的需要,开发者还写了好多如monster1_fight_and_log, monster1_fight_without_trigger等奇奇怪怪的函数,大多都是在monster1_fight_and函数之前加上一些很简单的调试语句。

func monster1_fight(with hero: Hero) { }

func monster1_fight_and_log(with hero: &Hero) {

// log something...

monster1_fight(with: hero)

}

func foo() {

// let monster1 = monster(monster1_fight)

let monster1 = monster(monster1_fight_and_log)

}

众所周知,困扰全世界开发者的两大问题,一是缓存,二是命名。这些用来调试的函数往往都特别简单,而且有时候就只在某个地方用一次,但是程序员却都要给这些函数起名字,而且还要额外再新增函数,导致函数密密麻麻,调试的时候也不方便。

闭包此时就成了开发者的救星。有了闭包之后,开发者可以这么写:

func monster1_fight(with hero: Hero) { }

func foo() {

let monster1 = monster({ hero in

// log something...

monster1_fight(with: hero)

})

}

也就是说,我们不需要给这个函数命名了,直接在传递函数作参数时,新写一个函数,也就是把闭包作为匿名函数来使用,这也是闭包最初级的做法。

捕获

看上去闭包只是作为一个没有名字的函数出现的,那么它还有什么作用呢?事实上,在大多数语言中,闭包作为匿名函数的时候,它的作用往往都相同。但是,当遇到下面的情况时,各个语言的处理方式不同,也就产生了各种各样语言特色的闭包。

我们单看一个普通的函数

func normal_function() {

// do something

}

它的全部信息是否就是这么多呢?函数的名字,函数的参数,函数的函数体,函数的返回值等等。看上去似乎是的,函数的全部信息似乎就只在这三行里。然而,并不是这样。

我们知道,作为一个普通的函数,它可以操作全局变量:

var a = 1

func normal_function() {

a = a + 1

}

如果单看我们刚刚说的函数的三行,a这个变量的信息并没有包含在内,相反,它作为全局变量,出现在了函数的外部。但是,它也作为重要的一部分,组成了函数的全部信息。

因此,一个函数其实是由函数本身的记录以及其环境组成的。

那么,作为闭包,下面的情况可否出现呢?

func monster1_fight(with hero: Hero) { }

func foo() {

var a = 1

let monster1 = monster({ hero in

a = a + 1

// log something...

monster1_fight(with: hero)

})

}

作为正常人,这种简单的类比推导肯定是对的,闭包必然要支持这种操作,事实上,这种操作也是确实支持的,被称为闭包捕获外部的变量,外部变量作为环境的一部分,也应成为闭包的一部分。

但是一到捕获,各个语言就会有各自的麻烦了。比如说,我们有以下的伪代码:

function outer() {

variable a

return function {

a = a + 1

}

}

如果变量a是分配在栈上的,那么在函数outer返回之后,a已经被释放了,那么在返回的闭包里,如何改变a的值呢?此时a的值应该已经没有意义了。

如果变量a不是通过栈来释放的,而是通过引用计数来释放的,那么闭包应不应该增加对这个变量的引用计数器呢?

闭包捕获的变量的作用域又是什么呢?

这一切都是很麻烦的事,所以我们一个一个来看。

栈内释放问题

对于Rust这类把变量分配在栈上的语言,可以巧妙地通过所有权的设置来解决这个问题。在Rust中,把闭包分为三个trait:Fn, FnMut, FnOnce, 分别代表闭包捕获的是变量的不可变引用,可变引用,以及直接捕获所有权。通过捕获不可变引用或可变引用,局部变量a的所有权仍然在outer函数里,仍然会在离开outer函数体之后被析构。但是,由于返回的闭包捕获了一个生命周期仅在outer里的变量,所以编译器会拒绝将这样的闭包返回,产生编译错误。而通过捕获所有权,a的释放就完全交给了闭包自己来做,也就不会产生这样的问题。

引用计数问题

对于Swift这样的语言,闭包是默认使用强引用的,如果要使用弱引用,则需要在闭包前提前声明,比如说我们之前提到的一个例子:

let client = Client()

class Person {

var book: Book?

func fetch() {

client.fetch(completionHandler: { [weak self] book ->

guard let self = self else { return }

self.book = book

})

}

}

这里的[weak self]就是提前声明捕获的是弱引用。

而对于Python来说,由于没有强弱引用的问题,lambda(实际上只是函数的语法糖)只是最自然地捕获了变量,并增加了它的引用计数。

捕获对象作用域问题

这个问题是单独为JavaScript相关的。众所周知,JavaScript有令人惊奇的作用域推断手法,以MDN中的代码为例:

function showHelp(help) {

document.getElementById('help').innerHTML = help;

}

function setupHelp() {

var helpText = [

{'id': 'email', 'help': 'Your e-mail address'},

{'id': 'name', 'help': 'Your full name'},

{'id': 'age', 'help': 'Your age (you must be over 16)'}

];

for (var i = 0; i < helpText.length; i++) {

var item = helpText[i];

document.getElementById(item.id).onfocus = function() {

showHelp(item.help);

}

}

}

setupHelp();

我们会发现,这段脚本运行之后,每一个item当被focus的时候,竟然都显示的是age相关的help。这完全违背了我们的常理。这是为什么呢?这是因为,用var声明的item作用域其实不是for循环的循环体,而是setupHelp这个函数的作用域,这叫做变量提升。因此,这几个闭包所捕获的,实际都是同一个变量item,只不过它在每次循环的时候分别被赋值为不同的值,最后停留在了age对应的item上。

解决这个问题的方法很简单,使用let就行了:

function showHelp(help) {

document.getElementById('help').innerHTML = help;

}

function setupHelp() {

var helpText = [

{'id': 'email', 'help': 'Your e-mail address'},

{'id': 'name', 'help': 'Your full name'},

{'id': 'age', 'help': 'Your age (you must be over 16)'}

];

for (var i = 0; i < helpText.length; i++) {

let item = helpText[i];

document.getElementById(item.id).onfocus = function() {

showHelp(item.help);

}

}

}

setupHelp();

let声明的item被固定在了for循环的循环体内,所以就ok了。

第二种奇特的情况,就在于JavaScript的奇葩的this捕获了:

var foo = {

a: 1,

bar: function() {

this.a += 1;

console.log(this.a);

}

};

var a = 3;

foo.bar(); // prints: 2var bar = foo.bar;

this.bar(); // prints: 4

bar作为一个闭包,是foo这个对象的一个字段。然而,foo.bar()里,是把this作为foo, 而在全局的this.bar()里,this又成了全局的window或global. 这就是JavaScript匿名函数的一个特点:this永远捕获的是调用者。对于foo.bar()这个情况来说,是foo调用的bar, 所以在bar内部,this就指的是foo; 类似地,全局调用的话,bar内部的this指的自然就是全局的window或global了。

让这种看似十分混乱不确定的操作变得确定的方法也有,就是直接使用箭头函数:

var foo = {

a: 1,

bar: () => {

this.a += 1;

console.log(this.a);

}

};

var a = 3;

foo.bar(); // prints: 4var bar = foo.bar;

this.bar(); // prints: 4

箭头函数没有自己的this,它只会从自己的作用域链的上一层继承this。这里,bar在定义时,它的上一层就是foo, 而foo的this自然就是全局的window或global了,因此无论是调用foo.bar()还是this.bar(),它内部的this都是全局的window或global.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值