Swift进阶六——函数和闭包
函数
形参和实参
形式参数,指的是是在函数的定义中,系统并没有为其分配内存空间、但是在函数里面可以使用的参数。比如下面的a就是形式参数:
func play(a: Int) {}
实际参数,指的是在函数调用的时候,传递给函数的变量。这个变量是系统实实在在分配了内存空间的变量。比如下面的b就是实际参数:
let b = 11
play(a: b)
之前我在中介绍过函数的实参标签和形参名的区别,但是表述有误,在该文中,我当时由于没有理解清楚概念,而将【实际参数标签】误称为【形式参数标签】,在此勘误!
每一个函数的形式参数都分为实际参数标签和形式参数名两部分:实际参数标签用在调用函数的时候,形式参数名用在函数的实现当中。默认情况下,形式参数会使用他们的形式参数名作为实际参数标签。
内嵌函数
可以在一个函数的内部定义另外一个函数,这就是内嵌函数。
默认情况下,内嵌函数在外部是会被隐藏起来的,但是仍然可以通过包裹他们的函数来调用他们。包裹的函数也可以返回它内部的一个内嵌函数来在另外的范围里使用:
func chooseStepFunc(back: Bool) -> (Int)->Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return back ? stepBackward : stepForward
}
闭包
捕获值
一个闭包能够从上下文捕获已被定义的常量和变量。即便定义的这些常量和变量的原作用域已经不存在了,闭包仍然能够在其函数体内引用和修改这些值。
func makeIncrementer(forIncrement ammount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += ammount
return runningTotal
}
return incrementer
}
上面的makeIncrementer函数作用是其创建一个递增器,里面的incrementer是一个内嵌函数。
内嵌函数是一个有名字且能从上层函数捕获值的闭包,因此可以从外层捕获runningTotal的值。
当内嵌函数incrementer被返回之后,外层包裹该内嵌函数的作用域(makeIncrementer)就已经不复存在了,理论上来讲runningTotal也就应该不存在了。但是由于内嵌函数incrementer是闭包,并且incrementer捕获了runningTotal的值,因此即便是定义runningTotal的外层作用域不存在了,incrementer仍然可以得到并修改runningTotal的值。
在Swift中,作为一种优化,如果一个值在闭包中使用到但是并没有改变,或者一个值是在闭包的外面使用,那么Swift有可能会使用这个值的拷贝,而不是捕获。
let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen()) // 10
print(incrementByTen()) // 20
print(incrementByTen()) // 30
// 如果你创建了第二个Incrementer,他将会有一个新的、独立的runningTotal的引用。
let incrementBySeven = makeIncrementer(forIncrement: 7)
print(incrementBySeven()) // 7
print(incrementBySeven()) // 14
print(incrementByTen()) // 40
这段代码中,一开始的incrementByTen闭包中会捕获并引用一个runningTotal。后面又新创建了一个incrementBySeven闭包,该闭包会捕获并引用一个新的runningTotal变量。
在Swift中,函数和闭包都是引用类型,当你赋值一个闭包给函数的常量或者变量的时候,你实际上都是将常量和变量设置为对函数和闭包的引用。
let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen()) // 10
print(incrementByTen()) // 20
print(incrementByTen()) // 30
let incrementBySeven = makeIncrementer(forIncrement: 7)
print(incrementBySeven()) // 7
print(incrementBySeven()) // 14
print(incrementByTen()) // 40
// 闭包是引用类型
let alsoIncrementByTen = incrementByTen
print(alsoIncrementByTen()) // 50
由于闭包是引用类型,因此,当你分配了一个闭包给类实例的属性,并且闭包通过引用该实例或者它的成员来捕获实例,此时将会在闭包和实例之间产生循环引用。
逃逸闭包 & 自动闭包
逃逸闭包
当闭包作为一个实际参数传递给一个函数的时候,并且它会在函数返回之后调用,我们就说这个闭包逃逸了。
当你声明一个接收闭包作为形式参数的函数时,你可以在形式参数前面写@escaping来声明该闭包是允许逃逸的。
闭包可以逃逸的一种方法是将其存储在定义函数之外的变量里。比如说,很多函数接收闭包实际参数作为启动异步任务的回调,函数在启动任务后返回,但是闭包需要等到任务执行完毕之后才会被调用,此时该闭包需要逃逸,以便稍后调用。
需要注意的一点是:如果你让闭包@escaping,那么你就必须在闭包中显示地引用self,如下:
func someFunctionWithEscapingClosure(closure: @escaping ()->Void) {
closure()
}
func someFunctionWithNoneEscapingClosure(closure: ()->Void) {
closure()
}
class SomeClass {
var x = 10
func doSomeThing() {
someFunctionWithEscapingClosure {
// 逃逸闭包中必须显示地引用self,否则会报错如下:
// Reference to property 'x' in closure requires explicit 'self.' to make capture semantics explicit
self.x = 100
}
someFunctionWithNoneEscapingClosure {
x = 200
}
}
}
自动闭包
自动闭包是一种自动创建的闭包,用于包装作为实际参数传递给函数的表达式。
自动闭包不接收任何的实际参数,当它被调用时,会返回内部包装的表达式的值。
自动闭包语法的好处在于:通过写普通表达式代替显示闭包而使你省略包围函数形式参数的括号。
Swift内部的assert函数里面就用到了自动闭包,如下:
public func assert(_ condition: @autoclosure () -> Bool, _ message: () -> String = String(), file: StaticString = #file, line: UInt = #line)
调用的时候直接写所要包装的表达式即可:
var number = 3
assert(number > 3, "number不能大于3")
接下来我们看一段代码:
var names = ["mike", "norman", "lavie"]
print(names.count) // 3
// removeOneName是一个闭包表达式,它可以被延迟处理
let removeOneName = { names.removeFirst() }
print(names.count) // 3
// 此时闭包表达式removeOneName会被执行
print("Now remove \(removeOneName())") // Now remove mike
print(names.count) // 2
上面代码中的,闭包的定义以及回调都可以封装到函数中,传一个闭包作为实际参数到函数的时候,会得到与延迟处理相同的行为,因此简化如下:
func removeName(waitingRemoveName: ()->String) {
print("Now remove \(waitingRemoveName())")
}
var names = ["mike", "norman", "lavie"]
print(names.count) // 3
removeName(waitingRemoveName: { names.removeFirst() }) // Now remove mike
print(names.count) // 2
接下来我们就可以通过自动闭包的形式来简化上述代码:
func removeName(waitingRemoveName: @autoclosure()->String) {
print("Now remove \(waitingRemoveName())")
}
var names = ["mike", "norman", "lavie"]
print(names.count) // 3
removeName(waitingRemoveName: names.removeFirst()) // Now remove mike
print(names.count) // 2
在函数removeName中,通过@autoclosure标志它的形式参数waitingRemoveName使用了自动闭包。这样,调用该函数的时候就好像接收了一个String类型的实参而不是闭包。实际参数会被自动转换为闭包,因为waitingRemoveName形式参数的类型被标记为@autoclosure。
自动+逃逸
如果你想要自动闭包允许逃逸,那么你就可以同时使用@autoclosure和@escaping标志。
函数式编程
题目一
首先从一道题目说起:
读入一个文本文件,确定该文件中所有单词的使用频率并从高到低进行排序,最后打印出所有单词及其频率的排序列表。
先来看一下我们熟悉的命令式(面向对象)解决方案:
let NON_WORDS = ["the", "and", "of", "to", "a", "i", "it", "in", "or", "is", "as", "so", "but", "be"]
func wordFreq(words: String) -> [String : Int] {
var wordDic: [String : Int] = [:]
let wordList = words.split(separator: " ")
for word in wordList {
let lowerCaseWord = word.lowercased() // 小写转换
if !NON_WORDS.contains(lowerCaseWord) { // 将不需要统计的筛选出去
if let count = wordDic[lowerCaseWord] { // 统计出现次数
wordDic[lowerCaseWord] = count + 1
} else {
wordDic[lowerCaseWord] = 1
}
}
}
return wordDic
}
let words = """
There are moments in life when you miss someone so much that
you just want to pick theme from your dreams and hug theme for
real dream what you want to dream go where you want to go be
what you want to be because you have only one life and one
change to do all the things you want to do
"""
print(wordFreq(words: words))
//["where": 1, "there": 1, "are": 1, "from": 1, "that": 1, "things": 1, "what": 2, "someone": 1, "life": 2, "dreams": 1, "change": 1, "go": 2, "moments": 1, "only": 1, "do": 2, "just": 1, "you": 7, "dream": 2, "hug": 1, "all": 1, "have": 1, "much": 1, "when": 1, "miss": 1, "one": 2, "want": 5, "for": 1, "your": 1, "pick": 1, "real": 1, "because": 1, "theme": 2]
接下来对比看一下函数式的解决方案:
func wordFreq(words: String) -> [String : Int] {
var wordDic: [String : Int] = [:]
let wordList = words.split(separator: " ")
// 函数式解决方案
wordList.map({$0.lowercased()})
.filter({!NON_WORDS.contains($0)})
.forEach { (word) in
wordDic[word] = (wordDic[word] ?? 0) + 1
}
// 面向对象(命令式)解决方案
// for word in wordList {
// let lowerCaseWord = word.lowercased()
// if !NON_WORDS.contains(lowerCaseWord) {
// if let count = wordDic[lowerCaseWord] {
// wordDic[lowerCaseWord] = count + 1
// } else {
// wordDic[lowerCaseWord] = 1
// }
// }
// }
return wordDic
}
现在来比较一下命令式和函数式这两种编程范式:
命令式编程风格常常会迫使我们出于性能考虑,将不同的任务交织起来,以便能够用一次循环来完成多个任务;而函数式编程风格会用map、filter等高阶函数将我们解放出来,让我们站在更高的抽象层面上去考虑问题,将问题看得更清楚。
面向对象编程通过封装不确定性因素来使代码能被人理解,他会把这些不确定性因素封装到类里面使人看不到;函数式编程通过尽量减少不确定因素来使代码能被人理解。在这一点上,后者更好一点,因为不确定因素本身是不容易被人理解的,应该越少越好,而不是封装起来进行隐藏更好。
在面向对象的命令式编程语言里面,重用的单元是类与类之间沟通用的消息;函数式编程语言实现重用的思路很不一样,函数式编程语言提倡在有限的几种关键数据结构(如List、Set、Dictionary)上运用针对这些数据结构高度优化过得操作,以此构成基本的运转机构,开发者再根据具体用途,插入自己的数据结构和高阶函数去调整机构的运转方式。
面向对象的命令式编程程序员喜欢不断创建新的数据结构和附属的操作,因为压倒一切的面向对象编程范式就是建立新的类和类之间的消息。把所有的数据结构都封装成类,一方面压制了方法层面的重用,另一方面鼓励了大粒度的框架式的重用。比起一味地创建新的类结构体系,把封装的单元降低到函数级别,更有利于达到细粒度的基础层面的重用。函数式程序员喜欢用少数几个核心数据结构,围绕它们去建立一套充分优化的运转机构。函数式编程的程序构造更方便我们在比较细小的层面上重用代码。
大部分的编程语言都是命令式的,程序员需要告诉代码先做什么、再做什么;而函数式编程会直接告诉程序员输出的结果是什么
也许,看完了上面的例子,你会有个疑问,函数式编程看着跟链式编程很像啊,两者是不是一个东西啊?
实际上,函数式编程和链式编程完全不是一回事,函数式编程是一种编程思想,而链式编程仅仅是一种书写代码的方式。函数式编程是和面向对象的命令式编程并列的一种编程思想;而链式编程只要写起来是一个链条就可以,面向对象里你可以使用这种链的方式,函数式编程里也可以使用链式。
题目二
接下来我们通过另外一个题目来加深一下对函数式编程的理解:
现在有一个名字列表,其中一些条目由单个字符构成。任务是,将除去单字符条目之外的列表内容,放在一个逗号分割的字符串里返回,且每个名字的首字母都要大写。
首先来看一下命令式解法:
func cleanNames(names: [String]) -> String {
var cleanNames = ""
for name in names {
if name.count > 1 {
cleanNames += name.capitalized + ","
}
}
cleanNames.remove(at: cleanNames.index(before: cleanNames.endIndex)) // 移除末尾多余的逗号
return cleanNames
}
let employee = ["norman", "v", "lavie", "n", "mike", "lily", "zhangsan", "lisi", "wangwu", "w", "jackson", "jams"]
print(cleanNames(names: employee)) // Norman,Lavie,Mike,Lily,Zhangsan,Lisi,Wangwu,Jackson,Jams
命令式编程是按照“程序是一系列改变状态的命令”来建模的一种编程风格。传统的for循环是命令式编程的绝好例子:先建立初始状态,然后每次迭代都会执行循环体中的一系列命令,进而改变初始状态。
然后看一下函数式编程的解法:
func cleanNames(names: [String]) -> String {
return names.filter({$0.count > 1})
.map({$0.capitalized})
.joined(separator: ",")
}
函数式编程会将程序描述成表达式和变换,以数学方程的形式建立模型,并且尽量避免可变的状态。比如上例中,命令式解法中的cleanNames就是一种可变因素,而在函数式解法中是没有这个可变状态的,函数式是以表达式的形式对原数据进行操作和变换,期间没有不可变因素。
函数式编程语言对问题的归类不同于命令式语言,如前面所用到的几种操作(map、filter等),每一种都作为一个逻辑分类由不同的函数所代表,这些函数实现了低层次的变换,然后开发者在开发的时候会根据业务需求定义更为高阶的函数来调整其低层次运转机构的运作。
函数式编程是一种编程范式,它提供给我们的编程元素就是函数。在函数式编程中,函数是一等公民(first-class-citizen)。一等公民的意思就是:
它可以按需创建
它可以存储在数据结构中
它可以当做实参传递给另一个函数
它可以当作另一个函数的返回值
对象,是面向对象的命令式程序设计语言中的一等公民,它就满足所有上面的这些条件。在函数式编程语言里,函数就是一等公民。
函数式编程就是把函数当成是一个个的构造块,然后将这些函数组合起来,构造成一个新的构造块。
说得更白话一点,程序无非就是多个构造块的组合,只不过面向对象编程的基础组件是类,而函数式编程的基础组件是函数。
函数式编程的组合性
在函数式编程中,有一类特殊的函数,它们可以接收函数作为输入,或者返回一个函数作为输出。这种函数叫做高阶函数。这类似于高中数学中的复合函数的概念,也就是f(g(x))。
高阶函数有啥用呢?它的一个重要的作用在于,我们可以使用高阶函数去做行为的组合。
按照函数式编程的理念,提供者提供的是一个又一个的构造块,即一个又一个函数,然后使用者根据自己的需要进行组合。模型提供者提供出来的是一个又一个的构造块,以及它们的组合方式。使用者根据自己的需要将这些构造块组合起来,提供出新的模型,供其他开发者使用。就这样,模型之间一层又一层地逐步叠加,最终构建起我们的整个应用。
一个好模型的设计就应该是逐层叠加。函数式编程的组合性,就是一种好的设计方式。
面向对象编程也有组合的概念,只不过面向对象中组合的元素是类和对象,而函数式编程组合的是函数。
在实际工作中,我们可以将面向对象和函数式这两种不同的编程范式进行组合运用:用面向对象编程的方式对系统的结构进行搭建,然后,用函数式编程的理念对函数接口进行设计。
函数式编程的不变性
函数式编程的不变性主要体现在值和纯函数上。
值,你可以将它理解成一个初始化之后就不再改变的量,换句话说,当你使用一个值的时候,值是不会变的。
纯函数,是符合下面两点的函数:
对于相同的输入,给出相同的输出
没有副作用
把值和纯函数结合起来看,值保证不会显示改变一个量,而纯函数保证的是,不会隐式改变一个量。
我们知道,函数式编程中的函数一词可以理解成是数学中的函数。在这个语境中,函数就是纯函数,一个函数计算之后是不会产生额外的改变的,而函数中用到的一个一个量就是值,他们是不会随着计算改变的。所以,在函数式编程中,计算天然就是不变的。
编写纯函数的重点是,不修改任何字段,也不调用修改字段内容的方法。
还有一个实用性的编程建议是,要多从不变的角度思考问题,尽量使用语法中不变的修饰符,比如Swift中的let。
不过,纯粹的函数式编程是很困难的,我们只能把编程原则设定为:尽可能编写不变类和纯函数。
现在纯函数式的语言适用范围并不广泛,但是越来越多的语言支持了函数式编程,比如Swift、Java。
以上。