写点什么

谈谈闭包——以 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:424601

评论

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

IPP Swap孵化器系统开发之LP算力挖矿模型

薇電13242772558

智能合约 dapp开发

GPT大语言模型Vicuna本地化部署实践(效果秒杀Alpaca) | 京东云技术团队

京东科技开发者

AI 京东云 GPT 企业号 5 月 PK 榜

“前端”工匠系列(二):合格的工匠,怎么做好价值落地 | 京东云技术团队

京东科技开发者

技术架构 京东云 企业号 5 月 PK 榜

IPP Swap孵化器/LP挖矿系统开发方案

Congge420

区块链 ipfs挖矿用什么app

降低 Spark 计算成本 50.18 %,使用 Kyligence 湖仓引擎构建云原生大数据底座,为计算提速 2x

Kyligence

开源 数据分析

一种DWS迁移Oracle的CONNECT BY语法的方案

华为云开发者联盟

数据库 华为云 华为云开发者联盟 企业号 5 月 PK 榜

部分等保政策相关专业术语英文翻译汇总

行云管家

等保 等级保护 等保2.0

吉林省网络安全等级测评机构有哪些?在哪里?

行云管家

网络安全 等级保护 吉林

【源码分析】【netty】FastThreadLocal为什么快?

如果晴天

源码分析 Netty 多线程 并发 netty

架构师日记-从代码到设计的性能优化指南 | 京东云技术团队

京东科技开发者

技术架构 京东云 企业号 5 月 PK 榜

面试必备:四种经典限流算法讲解

做梦都在改BUG

Java 算法 限流

阿里巴巴官方上线!号称国内Java八股文天花板(终极版)首次开源

做梦都在改BUG

Java java面试 Java八股文 Java面试题 Java面试八股文

厚积薄发|迭代为什么叫冲刺?

CODING DevOps

DevOps 敏捷 软件工程

使用taro+canvas实现微信小程序的图片分享功能 | 京东云技术团队

京东科技开发者

taro 京东云 企业号 5 月 PK 榜

LeetCode题解:136. 只出现一次的数字,排序后搜索,JavaScript,详细注释

Lee Chen

LeetCode

浅谈微服务中限流熔断降级的方法论

做梦都在改BUG

Java 微服务 限流 熔断降级

常用的表格检测识别方法-表格区域检测方法(上)

合合技术团队

人工智能 深度学习 文字识别 表格识别 表格检测

ChatGPT系统开发AI人功智能方案

Congge420

AI Gallery ChatGPT 人工智能ChatGPT 吗?

NFT元宇宙链游系统开发逻辑分析

Congge420

区块链 NFT链游 元宇宙链游

知识拷问:工作站和服务器哪个更适合做CST电磁仿真?

思茂信息

仿真软件 cst cst使用教程 cst电磁仿真 cst仿真软件

开源7天Github斩获4.5万Stars!阿里2023版高并发设计实录鲨疯了

做梦都在改BUG

Java 架构 微服务 系统设计 高并发

数字城市发展下的技术趋势,你了解多少?

没有用户名丶

好家伙!阿里新产Java性能优化(终极版),涵盖性能优化所有操作

做梦都在改BUG

Java 面试 性能优化 性能调优

顶象App加固保障互联网+医疗安全与合规

Geek_2d6073

一文掌握 Go 并发模式 Context 上下文

陈明勇

Go golang 后端 Context 上下文

如何让技术架构师具有预知未来业务发展的能力? | 京东云技术团队

京东科技开发者

架构师 京东云 企业号 5 月 PK 榜

【FAQ】视频编辑服务常见问题及解答

HMS Core

HMS Core

阿里p8架构师耗时一年整理SpringBoot,从构建小系统到架构大系统

做梦都在改BUG

Java Spring Boot 框架

UI自动化测试革命:拥抱Maestro框架的未来之旅

麦客

ios android 测试 自动化测试

开源即时通讯IM框架MobileIMSDK的Uniapp端开发快速入门

JackJiang

网络编程 即时通讯 IM

LP流动性挖矿/算力系统开发源码搭建

Congge420

区块链 系统开发 云算力挖矿系统开发详解 云算力模式系统开发源码

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