当前位置: 首页 > 编程日记 > 正文

swift3.0之闭包

Swift 相比原先的 Objective-C 最重要的优点之一,就是对函数式编程提供了更好的支持。 Swift 提供了更多的语法和一些新特性来增强函数式编程的能力,本文就在这方面进行一些讨论。

Swift 概览

对编程语言有了一些经验的程序员,尤其是那些对多种不同类型的编程语言都有经验的开发者, 在学习新的语言的时候更加得心应手。原因在于编程语言本身也是有各种范式的, 把握住这些特点就可以比较容易的上手了。

在入手一门新的语言的时候,一般关注的内容有:

  1. 原生数据结构

  2. 运算符

  3. 分支控制

  4. 如果是面向对象的编程语言,其面向对象的实现是怎样的

  5. 如果是函数式编程语言,其面向函数式编程的实现是怎样的

通过这几个点,其实只要阅读 Swift 文档的第一章,你就可以对这个语言有一个大概的印象。 比如对于数据结构,Swift 和其他的编程语言大体一样,有 Int, Float, Array, Dictionary 等, 运算符也基本与 C 语言一致等。 本文主要集中于对 Swift 函数式编程方面的特点进行一些盘点,因此在这里假设大家对 Swift 的基本语法已经有所了解。

对于一种编程范式,要掌握它也要抓住一些要点。对于支持函数式编程的语言,其一般的特点可能包含以下几种:

  1. 支持递归

  2. 函数本身是语言 First Class 的组成要素,且支持高阶函数和闭包

  3. 函数调用尽可能没有副作用 (Side Effect) 的条件

接下来我们来逐个盘点这些内容。

递归

Swift 是支持递归的,事实上现在不支持递归的编程语言已经很难找到了。在 Swift 里写一个递归调用和其他编程语言并没有什么区别:

1
2
3
4
5
6
7
8
9
func fib(n: Int) -> Int {
  if n <= 1 {
    return 1
  }
  else {
    return fib(n-1) + fib(n-2)
  }
}
fib(6) // output 13

关于 Swift 的递归没有什么好说的。作为一个常识,我们知道递归是需要消耗栈空间的。 在函数式编程语言中,递归是一个非常常用的方法,然而使用不慎很容易导致栈溢出的问题。 如果将代码改写为非递归实现,又可能会导致代码的可读性变差,因此有一个技巧是使用“尾递归”, 然后让编译器来优化代码。

一个 Common Lisp 的尾递归的例子是

1
2
3
4
5
6
(defun fib(n)
    (fib-iter 1 0 n))
(defun fib-iter(a b count)
    (if (= count 0)
        b
        (fib-iter (+ a b) a (- count 1))))

我们可以把我们上述的 Swift 代码也改写成相同形式

1
2
3
4
5
6
7
8
9
10
11
func fibiter(a: Int, b: Int, count: Int) -> Int {
  if count==0 {
    return b
  }
  else {
    return fibiter(a + b, a, count-1)
  }
}
func fib(n: Int) -> Int {
  return fibiter(1, 1, n);
}

我们可以 Playground 里观察是否使用尾递归时的迭代结果变化。

553.jpg

值得注意的是,这里出现了一个 Swift 的问题。虽然 Swift 支持嵌套函数,但是当我们将fibiter 作为一个高阶函数包含在fib函数之内的时候却发生了 EXC_BAD_ACCESS 报错, 并不清楚这是语言限制还是 Bug。

Swift 的高阶函数和闭包

在 Objective-C 时代,使用 block 来实现高阶函数或者闭包已经是非常成熟的技术了。 Swift 相比 Objective-C 的提高在于为函数式编程添加了诸多语法上的方便。

首先是高阶函数的支持,可以在函数内定义函数,下面就是一个很简洁的例子。

1
2
3
4
5
6
7
8
9
func greetingGenerator(object:String) -> (greeting:String) -> String {
  func sayGreeting(greeting:String) -> String {
    return greeting + ", " + object
  }
  return sayGreeting
}
let sayToWorld = greetingGenerator("world")
sayToWorld(greeting: "Hello"// "Hello, World"
sayToWorld(greeting: " 你好 "// " 你好, World"

如果使用 block 实现上述功能,可读性就不会有这么好。而且 block 的语法本身也比较怪异, 之前没少被人吐槽。Swift 从这个角度来看比较方便。事实上,在 Swift 里可以将函数当做对象赋值, 这和很多函数式编程语言是一样的。

作为一盘大杂烩,Swift 的函数系统也很有 JavaScript 的影子在里面。比如可以向下面这样定义函数:

1
2
3
4
5
let add = {
  (a:Int, b:Int) -> Int in
  return a+b
}
add(1, 2) // 3

等号之后被赋予变量add的是一个闭包表达式,因此更准确的说, 这是将一个闭包赋值给常量了。注意在闭包表达式中,in关键字之前是闭包的形式定义,之后是具体代码实现。 Swift 中的闭包跟匿名函数没有什么区别。 如果你将它赋值给对象,就跟 JavaScript 中相同的实践是一样的了。幸好 Swift 作为 C 系列的语言, 其分支语句 if 等本身是有作用域的,因此不会出现下列 JavaScript 的坑:

1
2
3
4
5
6
7
if (someNum>0) {
  function a(){ alert("one") };
}
else {
  function a(){ alert("two") };
}
a() // will always alert "two" in most of browsers

Swift 的闭包表达式和函数都可以作为函数的参数,从下面的代码我们可以看出闭包和函数的一致性:

1
2
3
4
5
6
7
8
9
10
11
12
func function() {
  println("this is a function")
}
let closure = {
  () -> () in
  println("this is a closure")
}
func run(somethingCanRun:()-> ()) {
  somethingCanRun()
}
run(function)
run(closure)

类似于 Ruby,Swift 作为函数参数的闭包做了一点语法糖。 在 Ruby 中使用 Block 的时候,我们可以这样写:

1
(1...5).map {|x| x*2} // => [2, 4, 6, 8]

在 Swift 当中我们可以得到几乎一样的表达式。

1
2
var a = Array(1..5).map {x in x*2}
// a = [2, 4, 6, 8]

也就是说, 如果一个函数的最后一个参数是闭包,那么它在语法上可以放在函数调用的外面。 闭包还可以用$0、$1等分别来表示第 0、第 1 个参数等。 基本的运算符也可以看做函数。 下面的几种方式都可以实现逆序倒排的功能。

1
2
3
4
5
let thingsToSort = Array(1..5)
var reversed1 = sort(thingsToSort) { a, b in a<b}
var reversed2 = sort(thingsToSort) { $0 < $1}
var reversed3 = sort(thingsToSort, <) // operator as a function
// all the above are [5, 4, 3, 2, 1]

总体来说,Swift 在添加方便函数操作、添加相关语法糖方面走的很远,基本上整合了目前各种语言中比较方便的特性。 实用性较好。

Side Effects

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量 (函数外的变量) 或修改参数 (wiki)。 函数副作用会给程序带来一些不必要的麻烦。

为了减少函数副作用,很多函数式编程语言都力求达到所谓的“纯函数”。 纯函数是指函数与外界交换数据的唯一渠道是参数和返回值, 而不会受到函数的外部变量的干扰。 乍看起来这似乎跟闭包的概念相抵触,因为闭包本身的一个重要特点就是可以访问到函数定义时的上下文环境。

事实上,为了在这种情况下支持纯函数,一些编程语言如 Clojure 等提供的数据结构都是不可变 (或者说 Persist) 的。 因此其实也就没有我们传统意义上的所认为的“变量”的概念。比如说,在 Python 中,字符串str就是一类不可变的数据结构。 你不能在原来的字符串上进行修改,每次想要进行类似的操作,其实都是生成了一个新的str对象。 然而 Python 中的链表结构则是可变的。且看下面的代码,在 Python 中对a字符串进行修改并不会影响b, 但是同样的操作作用于链表就会产生不一样的结果:

1
2
3
4
5
a = "hello, "
b = a
a += "world"
print a # hello, world
print b # hello,

Swift 的数据结构的 Persist 性质跟 Python 有点类似。需要注意的是,Swift 有变量和常量两种概念, 变量使用var声明,常量使用let声明,使用var声明的时候,Swift 中的字符串的行为跟 Python 相似, 因此修改字符串可以被理解为生成了一个新的字符串并修改了指针。同样, 使用var声明的数组和字典也都是可变的。

在 Swift 中使用let声明的对象不能被赋值,基本数据结果也会变得不可变,但是情况更复杂一点。

1
2
3
4
5
let aDict = ["k1":"v1"]
let anArray = [1, 2, 3, 4]
aDict["k1"] = "newVal" // !! will fail !!
anArray.append(5) // !! will fail !!
anArray[0] = 5 // anArray = [5, 2, 3, 4] now !

从上面的代码中可以看出,使用let声明的字典是完全不可变的,但是数组虽然不可以改变长度, 却可以改变数组元素的值!Swift 的文档中指出这里其实是将 Array 理解为定长数组从而方便编译优化, 来获得更好的访问性能。

综上所述,对象是否可变的关系其实略有复杂的,可以总结为:

  1. 使用var和let,Int和String类型都是不可变的,但是var时可以对变量重新赋值

  2. 使用let声明的常量不可以被重新赋值

  3. 使用let声明的Dictionary是完全不可变的

  4. 使用let声明的Array长度不可变,但是可以修改元素的值

  5. 使用let声明的类对象是可变的

综上所述,即使是使用let声明的对象也有可能可变,因此在多线程情况下就无法达到“无副作用”的要求了。

此外 Swift 的函数虽然没有指针,但是仍通过参数来修改变量的。只要在函数的参数定义中加入inout关键字即可。 这个特性很有 C 的风格。

个人觉得在支持通过元组来实现多返回值的情况下,这个特性不但显得鸡肋,也是一个导致程序产生“副作用”的特性。 Swift 支持这样的特性,恐怕更多的是为了兼容 Objective-C 以及方便在两个语言之间搭建 Bridge。

1
2
3
4
5
func inc(inout a:Int) {
  a += 1
}
var num = 1
inc(&num) // num = 2 now!

综上所述,使用 Swift 自带的数据结构并不能很好的实现“无副作用”的“纯函数式”编程, 它并没有比 Python、Ruby 这类语言走的更远。幸好作为一种关注度很高的语言, 已经有开发者为其实现了一套完全满足不可变要求的数据结构和库:Swiftz。 坚持使用let和 Swiftz 提供的数据结构来操作,就可以实现“纯函数式”编程。

总结

在我看来,Swift 虽然实现了很多其他语言的亮点特性,但是总体实现来说并不是很整齐。 它在函数式编程方面添加了很多特性,但在控制副作用方面仅能达到平均水准。 有些特性看起来像是为了兼容原来的 Objective-C 才加入的。

Swift 写起来相对比 Objective-C 更方便一点,脱离 Xcode 这样的 IDE 来写也是应该是可以的。 目前 Swift 只支持集中少量的原生数据结构而没有标准库,更不具备跨平台特性,这是一个缺点。 在仔细阅读了文档之后发现 Swift 本身的语法细节还是很多的,就比如switch分置语句的用法就有很多内容。 入门学习的容易程度并没有原来想象的那么好。我个人并不觉得这门语言会对其他平台的开发者有很大吸引力。

Swift 是一门很强大的语言,在其稳定版本发布之后我认为我会从 Objective-C 转向 Swift 来进行编程, 它在未来很可能成为 iOS 和 Mac 开发的首选。

相关文章:

Linux下gdb attach的使用(调试已在运行的进程)

在Linux上&#xff0c;执行有多线程的程序时&#xff0c;当程序执行退出操作时有时会遇到卡死现象&#xff0c;如果程序模块多&#xff0c;代码量大&#xff0c;很难快速定位&#xff0c;此时可试试gdb attach方法。 测试代码main.cpp如下&#xff0c;这里为了使程序退出时产生…

一行Python代码能实现这么多丧心病狂的功能?(代码可复制)

最近看知乎上有一篇名为《一行 Python 能实现什么丧心病狂的功能&#xff1f;》&#xff08;https://www.zhihu.com/question/37046157&#xff09;的帖子&#xff0c;点进去发现一行Python代码可以做这么多丧心病狂的功能&#xff01;整理了一下知乎上这篇文章的内容&#xff…

一步一步写算法(之图结构)

原文:一步一步写算法&#xff08;之图结构&#xff09; 【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 图是数据结构里面的重要一章。通过图&#xff0c;我们可以判断两个点之间是不是具有连通性…

FFmpeg中可执行文件ffprobe用法汇总

从https://ffbinaries.com/downloads 下载最新的4.1版本的Windows 64位FFprobe&#xff0c;FFprobe用于从多媒体流中获取相关信息或查看文件格式信息&#xff0c;并以可读的方式打印&#xff0c;FFprobe可以作为一个命令行程序单独使用。 通过执行以下命令将FFprobe信息重定位…

CocoaPods导入的库其头文件导入的方法

尽管CocoaPods使用十分方便,但其导入的第三方框架还是要经过几步操作,才能供项目使用; 第一步:导入库 1>-在终端进入项目的根目录; 2>-输入:touch Podfile,则项目文件夹会创建一个空的Podfile,这时,你可以将你想要导入的库写在里面.如: platform :ios, 6.0 pod RESid…

Google、微软、阿里、腾讯、百度这些大公司在GitHub上开源投入排名分析 | CSDN原力计划...

扫码参与CSDN“原力计划”作者 | 村中少年来源 | CSDN原力计划获奖作品现在有越来越多的公司都参与了开源&#xff0c;其背后有各自的目的所在&#xff0c;姑且不予讨论。本文是从多个方面分析各大公司在开源上的投入情况。由于全世界绝大多数的开源项目都有发布到Github上&…

jquery源码解析:each,makeArray,merge,grep,map详解

jQuery的工具方法&#xff0c;其实就是静态方法&#xff0c;源码里面就是通过extend方法&#xff0c;把这些工具方法添加给jQuery构造函数的。 jQuery.extend({ ...... each: function( obj, callback, args ) { //$.each(arr , function(i,value){}),第三个参数用于…

swift实现提示框第三方库:MBProgressHUD

GitHud的下载地址是&#xff1a;https://github.com/jdg/MBProgressHUD/ 下载完成后&#xff0c;将MBProgressHUD.h和MBProgressHUD.m拖入已经新建好的Swift项目。因为使用的swift语言&#xff0c;所以拖入项目的时候会提示是否新建一个桥接objective-c与swift的文件&#xff…

这段Python代码让程序员赚300W,公司已确认!网友:神操作!

Python到底还能给人多少惊喜&#xff1f;笔者最近看到了这两天关于Python最热门的话题&#xff0c;关于《地产大佬潘石屹学Python的原因》&#xff0c;结果被这个回答惊到了&#xff1a;来源&#xff1a;知乎 https://www.zhihu.com/question/355880221笔者翻了翻那些回答&…

FFmpeg中可执行文件ffmpeg用法汇总

从https://ffbinaries.com/downloads 下载最新的4.1版本的Windows 64位FFmpeg&#xff0c;FFmpeg是一个快速的音频/视频转换工具&#xff0c;FFmpeg可以作为一个命令行程序单独使用。 通过执行以下命令将FFmpeg信息重定位到ffmpeg_help.txt文件中便于查看&#xff0c;其内容如…

下载Ext JS 5.1 gpl版本的方法

先进入官网&#xff1a;http://www.sencha.com然后在导航的Products中选择Sencha Ext JS&#xff0c;会看到以下页面&#xff1a;这时候不要单击Download按钮&#xff0c;而是要单击导航中的DETAILS&#xff0c;页面切换后&#xff0c;就可在底部看到GPL版本的下载按钮了&#…

对MBProgressHUD进行封装并精简使用

几个效果图&#xff1a; 以下源码是MBProgressHUD支持最新的iOS8的版本&#xff0c;没有任何的警告信息 MBProgressHUD.h 与 MBProgressHUD.m MBProgressHUD.hMBProgressHUD.m以下是本人在MBProgressHUD基础上封装的类&#xff0c;觉得部分的使用基于block ShowHUD.h 与 Show…

基于Hash的消息认证码HMAC简介及在OpenSSL中使用举例

HMAC(Hash-based Message Authentication Code)&#xff1a;基于Hash的消息认证码&#xff0c;是一种通过特别计算方式之后产生的消息认证码(MAC)&#xff0c;使用密码散列函数&#xff0c;同时结合一个加密密钥。它可以用来保证数据的完整性&#xff0c;同时可以用来作某个消息…

英特尔发布oneAPI软件计划及beta产品,面向异构计算提供统一可扩展的编程模型

近日&#xff0c;在2019年超级计算大会上&#xff0c;英特尔发布了一项全新软件行业计划oneAPI&#xff0c;助力充分释放高性能计算与人工智能技术融合时代多架构计算的潜力&#xff0c;同时发布了一个oneAPI beta产品。 英特尔oneAPI行业计划&#xff0c;为跨多种包括CPU、GP…

Mac下配置Maven

1.Java环境变量设置就不说。 但是配置Maven需要检查下Java环境变量的设置。需要检查JAVA_HOME环境变量以及Java命令 wanyakundeMacBook-Pro:Library wanyakun$ echo $JAVA_HOME /Library/Java/JavaVirtualMachines/jdk1.7.0_60.jdk/Contents/Home wanyakundeMacBook-Pro:Librar…

UIAlertAction的用法

let alertController UIAlertController(title:"系统提示", message:"您确定要退出程序吗&#xff1f;", preferredStyle: .alert) let cancelAction UIAlertAction(title:"取消", style: .cancel, handler:nil) let okAction UIAlertAction(tit…

在Windows和Linux上编译gRPC源码操作步骤(C++)

gRPC最新发布版本为v1.23.0&#xff0c;下面以此版本为例说明在Windows和Linux下编译过程。 Windows7/10 vs2103编译gRPC源码操作步骤&#xff1a; 1. 需要本机已安装Git、CMake、Perl、Go、yasm&#xff1b; 2. 依次执行如下命令&#xff1a; git clone https://github.co…

这些算法工程师,他们真的是太难了!

现在的算法工程师真的是太难了&#xff01;要让AI会看人眼都分辨不清的医疗影像数据又不够&#xff0c;还得用前沿技术好不容易学会看片&#xff0c;还要让AI会分析病理赋予AI诊断疾病的使命然后几十种模型&#xff0c;N次计算只给一张显卡Boss就问&#xff1a;你打算怎么做&am…

Java 汉子转拼音

1. 引入: pinyin4j-1.1.0 包; http://pan.baidu.com/s/1eQ8a874 2. 转换; private static String ChineseToPinyin(String name){String pinyinName"";char[] nameChar name.toCharArray(); HanyuPinyinOutputFormat defaultFormat new HanyuPinyinOutputFormat()…

Swift - 使用Alamofire通过HTTPS进行网络请求,及证书的使用

&#xff08;本文代码已升级至Swift3&#xff09; 我原来写过一篇文章介绍如何使用证书通过SSL/TLS方式进行网络请求&#xff08;Swift - 使用URLSession通过HTTPS进行网络请求&#xff0c;及证书的使用&#xff09;&#xff0c;当时用的是 URLSession。本文介绍如何使用 Alam…

gRPC简介及简单使用(C++)

gRPC是一个现代的、开源的、高性能远程过程调用(RPC)框架&#xff0c;可以在任何平台运行。gRPC使客户端和服务器端应用程序能够透明地进行通信&#xff0c;并简化了连接系统的构建。gRPC支持的语言包括C、Ruby、Python、Java、Go等。 gRPC默认使用Google的Protocol Buffers&a…

YC中国被撤,陆奇独立运营个人新品牌「奇绩创坛」

整理 | Jane出品 | AI科技大本营&#xff08;ID&#xff1a;rgznai100&#xff09;近日&#xff0c;Y Combinator&#xff08;以下简称 YC&#xff09; 发布消息称&#xff0c;YC 将撤回 YC 中国业务与运营&#xff0c;这一品牌也将停止使用&#xff0c;YC的战略布局将调整重新…

shell 脚本逐行读取多个文件,并逐行对应

#!/bin/bashfor i in seq 448doaaased -n "$i"p num.txtbbbsed -n "$i"p text.txt/root/cooper/sms.pl $aaa $bbbdonenum.txt 记录了348个号码text.txt中记录了348个字段效果是取num.txt中第一行作为第一行参数 取text.txt中第一行作为第二个参数num.txt要…

iOS常用知识点1

多线程、特别是NSOperation 和 GCD 的内部原理。 运行时机制的原理和运用场景。 SDWebImage的原理。实现机制。如何解决TableView卡的问题。 block和代理的&#xff0c;通知的区别。block的用法需要注意些什么。 strong&#xff0c;weak&#xff0c;retain&#xff0c;assign&a…

美团BERT的探索和实践 | CSDN原力计划

扫码参与CSDN“原力计划”作者 | 杨扬 佳昊 金刚等来源 | CSDN原力计划作品*点击阅读原文&#xff0c;查看美团技术团队更多干货文章。背景2018年&#xff0c;自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;领域最激动人心的进展莫过于预训练…

程序员的自我修养--链接、装载与库笔记:可执行文件的装载与进程

可执行文件只有装载到内存以后才能被CPU执行。 1. 进程虚拟地址空间 程序和进程有什么区别&#xff1a;程序(或者狭义上讲可执行文件)是一个静态的概念&#xff0c;它就是一些预先编译好的指令和数据集合的一个文件&#xff1b;进程则是一个动态的概念&#xff0c;它是程序运…

JDBC实例--工具类升级,使用Apache DBCP连接池重构DBUtility,让连接数据库更有效,更安全...

直接使用JDBC访问数据库时&#xff0c;需要避免以下隐患: 1. 每一次数据操作请求都需要建立数据库连接、打开连接、存取数据和关闭连接等步骤。而建立和打开数据库连接是一件既耗资源又费时的过程&#xff0c;如果频繁发生这种数据库操作&#xff0c;势必会使系统性能下降。 2.…

程序员的自我修养--链接、装载与库笔记:Linux共享库的组织

共享库(Shared Library)概念&#xff1a;其实从文件结构上来讲&#xff0c;共享库和共享对象没什么区别&#xff0c;Linux下的共享库就是普通的ELF共享对象。由于共享对象可以被各个程序之间共享&#xff0c;所以它也就成为了库的很好的存在形式&#xff0c;很多库的开发者都以…

iOS下JS与原生OC互相调用

iOS开发免不了要与UIWebView打交道&#xff0c;然后就要涉及到JS与原生OC交互&#xff0c;今天总结一下JS与原生OC交互的两种方式。 JS调用原生OC篇 方式一 第一种方式是用JS发起一个假的URL请求&#xff0c;然后利用UIWebView的代理方法拦截这次请求&#xff0c;然后再做相…

马斯克发首款会上火星的电动皮卡:28万起,可防弹,造型相当“赛博朋克”...

整理 | Jane、非主流出品 | AI科技大本营&#xff08;ID&#xff1a;rgznai100&#xff09;【导读】马斯克今日发布酝酿多年、“真爱系列”的第一辆电动皮卡Cybertruck&#xff0c;Cybertruck 是赛博朋克&#xff08;cyberpunk&#xff09;与卡车&#xff08;truck&#xff09;…