写点什么

谈谈闭包——以 Swift 为例

  • 2015-12-03
  • 本文字数:5082 字

    阅读完需:约 17 分钟

本文讨论闭包的相关概念,大部分代码使用 Swift 编写。Swift 对闭包有着良好的支持。这是因为,Swift 被设计成一门一定程度上支持函数式编程范式的编程语言。而函数式编程和闭包有着紧密的联系。本文着重讨论的也是函数式编程和闭包之间的关系。

变量,约束,环境和函数

在讨论闭包之前,需要先明晰一些简单的概念。

变量

计算机程序语言中必不可少的一部分是它需要提供一种通过名字去使用计算对象的方式。也就是,我们需要能为计算对象标识一个名字。名字标识符就是我们常说的变量,而它的值就是它所对应的那个对象。如果要在编程语言中使用这些变量,我们就需要有将值和变量名关联起来,和在需要时又可以将值提取出来的能力。这就意味着编程语言需要提供某种存储能力,将变量名和值的对应关系存储下来,以便需要时使用。

约束

将变量名关联于对应的值,就构成了一个约束。任何变量至多只能有一个约束。这很容易理解,因为使用变量名取数据时,你当然希望它指明的是明确而且唯一的值。这也是为何把变量名和值的对应关系称为约束的原因。

环境

一系列这种名字和值对应关系(约束)的存储,就可以称之为环境。环境对于程序语句是至关重要的,因为它确定了每个表达式的上下文。甚至,我们可以说环境决定了表达式的含义。因为,即便是确定像 (1 + 1)这么简单的语句的具体含义,也有赖于环境来确定+是表达加法的运算符号。我们可以假定程序的运行时拥有一个全局环境,这个环境里包含了所有关联于基本过程的符号的值。例如,符号+就在全局环境中被约束到基本的加法运算。

函数

函数,是大部分编程语言都存在的概念。它在不同语言中这个概念也存在着细微的区别。在面向对象编程语言中称之为“方法”,在函数式编程语言中称之为“过程”。无论被称为什么名字,它们都拥有的共同基本含义是:它是编程语言的一种基本的抽象手段,使我们可以将一组操作作为一个单元组合起来,并为这组操作命名。这样我们就可以通过一个简单的名字操作一组复杂的操作。而对于不同的编程语言中“函数”这一实体所存在的细微差别,我们会在后文中通过对“闭包”的探讨加以说明。

闭包

在说闭包之前,需要先清楚“自由变量”的概念。在某个作用域中,如果使用未在本作用域中声明的变量,对于此作用域来说,该变量就是一个自由变量。

闭包,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。另一种说法认为闭包并不是函数,而是由函数和与其相关的引用环境组合而成的实体。这是因为,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。而函数只会有一个实例。这两种定义对闭包的看法并没有不同,只是对函数的定义不同。前者对函数的定义更宽松,后者则更为严格。

重要通知:接下来 InfoQ 将会选择性地将部分优秀内容首发在微信公众号中,欢迎关注 InfoQ 微信公众号第一时间阅读精品内容。

Scheme 中的闭包

最早实现闭包的程序语言是 Scheme。Scheme 是一种函数式编程语言。从这也可以看出闭包和函数式编程语言联系紧密。我们用 Scheme 实现一个简单的自增器的例子,这个例子会在后文中以 Swift 版本再次出现:

复制代码
(define make_counter
(lambda ()
(let ((count 0))
(lambda ()
(set! count (+ 1 count))
count))))

这段代码做了如下事情:

  • lambda定义了一个过程(或者说函数)。
  • define将第一个lambda定义的过程命名为make_counter
  • let创建了一个作用域,这个作用域内,创建了变量count,并初始化为 0(在环境中添加了一条 count 到 0 的约束)。
  • 返回第二个lambda创建的过程。该过程将count加一后再赋给count(取得环境中 count 到某个值的约束,加一后更新这条约束),并将count作为返回值返回。

第二个lambda其实创建了一个我们常说的闭包。我们可以发现,Scheme 中的闭包并没有区别于其他过程的特殊语法标识,它们都用lambda定义。但在 Objective-C(或者说 C 语言,因为 Objective-C 的block就是来源于 C 语言的一种闭包扩展)和 Java 中,定义一个函数和定义一个闭包使用的语法并不相同。这是因为闭包性被认为 Scheme 的过程本应具有的特性。而 Objectiive-C 和 Java 中的闭包,都是在创建语言多时之后,迫于编程语言的发展趋势而添加的特性。由于面向对象编程对于函数的看法具有局限性,在 Objective-C 和 Java 这样比较纯粹的面向对象编程语言中,闭包的实现会比较困难,即使实现了,语法也难以优雅,看起来就很像一个补丁。

至于为何第二个过程是一个闭包,将会在下一小节使用 Swift 代码讲解。后文也将会看到,在闭包语法简洁性这一点上,Swift 更接近于 Scheme。

高阶函数

高阶函数,指可以将其他函数作为参数或者返回结果的函数。

Swift 中的函数都是高阶函数,这和 Scheme,Scala,Haskell 一样。与此对照的是,Java 中没有高阶函数(在 Java 7 支持闭包之前)。Java 中的方法没法单独存在,方法总是需要和类捆绑在一起。当需要将一个方法作为参数传递给另外一个方法时,你会发现必须以类作为载体来运送方法。这也是 Java 中监听器(Listener)的做法。

一般支持闭包的语言都是一定程度上支持函数式编程的编程语言。若非如此,则其闭包实现一般都晦涩复杂。这是因为高阶函数需要函数先成为闭包。而函数式编程语言的函数都是高阶函数。所以,函数式编程语言支持闭包就理所当然了。其实在很多函数式编程语言中,闭包的概念并不明显。因为,在函数式编程语言中,这是函数本身就应该具备的能力(绑定其引用的自由变量)。函数式编程中,函数也被视为对象,拥有多个实例也就理所当然。而在命令式编程语言中,对函数看法更为局限,视函数为语言的一种特殊构造(和类并不相同),它只是一些列语句的集合。在这种情况下,函数如果需要操作自由变量,程序员就需要自己保障自由变量是可以被访问的,并且存储着有意义的值。在 C 语言中,程序员有很大一部分精力用于关注这个问题(野指针)。

我们再来看看,为何高阶函数需要函数先成为闭包。举一个高阶函数的例子:

复制代码
func makeCounter() -> (() -> Int) {
var count = 0
func inc() -> Int {
count += 1
return count
}
return inc
}

在这个简单的例子中,我们定义了一个生产自增器的函数makeCounter。由makeCounter返回一个函数,这个函数就是一个自增器。每次调用自增器就会增加一。很明显,makeCounter 是高阶函数,因为它返回了另外一个函数。

makeCounter首先定义了一个变量var count = 0(在环境中添加了一条 count 到 0 的约束)。函数本身创造了一个作用域,这个函数内部定义变量count的作用域就是函数开始直到函数返回。也就是说,函数返回后,就不在变量count的作用域中了,变量的生命周期也就结束了。

然后,makeCounter中又定义了一个新函数incinc引用了自由变量count,并将count加一后再赋给count(取得环境中 count 到某个值的约束,加一后更新这条约束),最后将count作为返回值返回。而后makeCounter又将inc作为返回值返回。

在函数makeCounter返回后,由于count已经不在其作用域内,这看起来应该是无法正确执行的。所以,为了让程序正确执行,inc需要绑定其引用的自由变量,使得即使在makeCounter返回后,count也不会消失。用命令式编程的观点来看,也就是说,inc被返回时,就创造了一个特殊的函数,该函数带着它定义时引用的自由变量的上下文环境,这其实就是闭包

一等函数

一等函数,进一步扩展了函数的使用范围,使得函数成为语言中的“头等公民”。这意味函数可在任何其他语言结构(比如变量)出现的地方出现。一等函数是更严格的高阶函数。Swift 中的函数都是一等函数。

我们在上一小节发现,成为高阶函数,需要函数本身就具备闭包性。这一节,我们会发现更严格的一等函数和闭包的也仍然有着紧密的关系。

我们可以这样使用上例中的自增器:

复制代码
let inc1 = makeCounter()
inc1() // 1
inc1() // 2
let inc2 = makeCounter()
inc2() // 1
inc2() // 2

代码中,我们将函数赋给变量,这样我们可以通过变量来调用函数。运行结果使得我们可以发现,每次调用makeCounter其实是创建了一个新的对象,inc1,和inc2并不一样。它们有着自己的 count 值,之间并不共享。这说明,这些函数是一等函数,它们是对象,可以有多个实例,可以被赋给变量。这和 Swift 中如 Struct,Class 等其他语言构造并没有不同。

我们再对比一下 Java 中的“函数”(方法)就能发现明显的区别。Java 中的方法没法单独存在,方法总是需要和类捆绑在一起。Java 中的方法是类的一种附属构造。方法只是一系列语句的集合,一种用于操作对象的途径。这种定位下,你无法(也无需)把方法赋给某个变量。因为,你只能通过对象来调用方法(或者通过类调用静态方法);你无法(也无需)为方法绑定自由变量。因为,Java 中方法绑定在所属类上,所以,你只需要把变量绑定到类上(成为实例变量,或者静态变量),就可以为属于该类的方法所用。

Python 社区有一种描述对比了类和闭包:“对象是附有行为的数据,而闭包是附有数据的行为。”这个说法揭示了,在函数式编程中,函数成为闭包之后,取得了和类相对应的地位。这其实就是一等函数的意义。

匿名函数和闭包

我们再顺着上例进一步探求 Swift 中函数和闭包的关系。其实,我们可以发现inc这个内部函数除了被返回之外,并没有其他作用。它其实并不需要名字,可被定义为一个匿名函数。那么我们再试试用 Swift 来定义一个匿名函数。会发现,只能定义为如下形式:

复制代码
func makeCounter() -> (() -> Int) {
var count = 0
return { () -> Int in
count += 1
return count
}
}

返回的那部分其实是一个闭包。Swift 中并没有特殊的匿名函数语法构造。如果,你想写一个传统意义上的匿名函数,你就只能给出一个闭包。也就是说 Swift 中匿名函数和闭包的定义方法是一样的。可以看出在 Swift 中,闭包和函数的分野也并不显著,起码匿名函数和闭包并无区分。或者说 Swift 的函数都具有绑定自由变量的能力,也就是闭包性。前例中的自增器的函数写法,和匿名函数写法(或者说是闭包写法),返回的都是闭包。这是函数式编程语言的特点。这一点,在 Swift 中也体现出来了。

如果再做个比较,会发现 Swift 中的闭包语法比 Objective-C 简单清晰很多。这是因为,Swift 是以支持函数式编程思想为设计基础的编程语言。闭包性是 Swift 这类支持函数式编程的编程语言的一部分语言特性。而 Objective-C 这种较纯的面向对象编程语言则需在后期添加闭包这种特性。这时常带来困难,以及语法上的晦涩。在对闭包的支持这一点上,Swift 更接近 Scheme。

另一种闭包

上文中使用的闭包的概念,并不被 SICP 的作者认可。他们认为使用“闭包”这个名词表示带有自由变量的过程的实现技术,是一件很不幸的事情。他们更认可“闭包”本来的意义:在抽象代数中,一集合元素称为在某个运算(操作)之下闭合,如果将该运算应用于这一集合中的元素,产生出的仍然是该集合里的元素。

对应于计算机语言中的概念。一般说,某种组合数据对象的操作满足闭包性,那就是说,通过它组合起来的数据对象得到的结果本身还可以通过同样的操作再进行组合。比如,如果我们可以创建数组元素也是数组的数组,那么我们就说创建数组的操作具有闭包性质。因为创建出来的数组仍然可以作为数组的元素,用于创建新数组。诸多编程语言的数据组合机制都不满足这一性质,或者使得其中的闭包性质很难使用。Fortan,Basic 里,组合数据的一种典型方法是将它们放入数组,但却不能将数组放入数组。Pascal 和 C 允许结构的元素又是结构,却要求程序员显式地操作指针,并限制性地要求结构的每个域都只能包含预先定义好形式的元素。

Swift 的集合类型的操作满足这一意义上的闭包性质。Swift 中可以将数组作为另外一个数组的元素。也可以将字典作为字典的值。Scheme 可以说是一种弱类型编程语言,序对(pair)是其构造复合数据的基本结构,Scheme 中对于序对中存储什么完全没有限制,所以当然可以建立序对的序对,这也是 Scheme 的表(list,更接近于链表的一种数据结构)的构成方式。而 Swift 是一种强类型编程语言。在声明数组,字典时,需要提供元素的类型信息。可能是作为一种补偿,Swift 提供的集合类型大都支持泛型。在强类型的安全和弱类型的灵活之间取得了平衡。

参考文档


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2015-12-03 07:424738

评论

发布
暂无评论
发现更多内容

Java中的自旋锁和适应性自旋锁是什么意思?其分类依据是啥?

wljslmz

Java 自旋锁 10月月更 适应性自旋锁

《零代码教练指南》正式发布

明道云

存储优化--分区与冷热分离

喵叔

10月月更

【CSS】:什么是z-index属性?该属性有哪些取值类型?

翼同学

CSS 前端 样式设置 10月月更

OpenHarmony如何控制屏幕亮度

坚果

OpenHarmony 10月月更

[架构实战] 学习笔记二

爱学习的麦子

【LeetCode】重新格式化电话号码Java题解

Albert

LeetCode 10月月更

今日国庆,祝福祖国!【文末超级福利】

图灵社区

读书 国庆节

今日国庆,祝福祖国!【文末超级福利】

图灵教育

读书 国庆节

ESP32-C3 学习测试 蓝牙 篇(四、GATT Server 示例解析)

矜辰所致

蓝牙 ESP32-C3 10月月更 GATT

spring-cloud-kubernetes与SpringCloud Gateway

程序员欣宸

Kubernetes spring-cloud 10月月更 spring-cloud-kubernetes

能不能手写Vue响应式?前端面试进阶

bb_xiaxia1998

Vue

When allowCredentials is true, allowedOrigins cannot contain the special value ___ since that cannot be set on the _Access-Contr

共饮一杯无

Java springboot 10月月更

操作系统导论:分页

小白钊钊

操作系统 java; 10月月更

架构师的十八般武艺:线上运维

agnostic

运维

在vue的v-for中,key为什么不能用index?

bb_xiaxia1998

Vue

Qt解压带有密码的加密文件

中国好公民st

c++ Qt Company 10月月更

【结构体内功修炼】结构体内存对齐(一)

Albert Edison

C语言 结构体 10月月更 内存对齐

从特斯拉人形机器人亮相看AI人工智能模型落地面临的两个难题

felix

落地 机器人 AI人工智能

Python应用之计算三角形面积

梦笔生花

10月月更 Python代码 计算三角形面积

大画 Spark :: 网络 (8)-Spark 网络中的“四次握手”Driver 如何获取 Executor 的 EndpointRef 烧脑

dclar

大数据 hadoop spark 源代码 spark源码

mysql中的事务隔离级别序列化如何实现

知识浅谈

MySQL 隔离级别 10月月更

Spring Boot 集成 Redis 配置 MyBatis 二级缓存

微枫Micromaple

redis 缓存 mybatis springboot 10月月更

跟随一组图片,了解Go Channel的底层实现

董哥的黑板报

Go 后端 服务端 操作系统 runtime

【一Go到底】第一天---初识Goooooooooooooooooooooooo

指剑

Go go并发 10月月更

【愚公系列】2022年10月 Go教学课程 015-运算符之赋值运算符和关系运算符

愚公搬代码

10月月更

Go学习之路-1.认识GO语言

子不语Any

Go 后端 10月月更

体验 Orbeon form PE 版本提供的 JavaScript Embedding API

汪子熙

Java SAP commerce 10月月更 oberon

一起玩OptaPlanner-Study,玩转第一个程序

成长兔🐇

【牛客刷题-算法】1-算法入门-数据结构-栈

清风莫追

算法与数据结构 10月月更

微服务稳定性保障

穿过生命散发芬芳

微服务 10月月更

谈谈闭包——以Swift为例_移动_郭麟_InfoQ精选文章