Swift基础入门知识学习(10)-函数(函式)-讲给你懂
超速学习-重点笔记
目录
理解难度
★★★★★
实用程度
★☆☆☆☆
打起精神,因为这里要说明在Swift教程中很多人不容易理解的第一件事:闭包(closure)。
闭包是Swift最强大的功能之一,但也是很容易让人困惑的功能。你会发现它们很难!不用担心,这代表你的大脑正在正常运作。
到底什么时候会用上闭包呢?以下是你可能会用到的情况:
- 延迟后运行一些代码。
- 动画完成后运行一些代码。
- 下载完成后运行一些代码。
- 当用户从菜单中选择一个选项时,运行一些代码。
闭包让我们将一些功能包在一个变量中,然后存储在某个地方。我们也可以从函数返回它,并将闭包存储在其他地方。
闭包(closure)简单来讲就是一个匿名函数,与普通函数同样为一个独立的代码区块,可以在代码中被传递和使用。闭包的特性为能够捕获和储存在其前后文中任何常量与变量,进而独立的执行工作。
闭包有点难以阅读——特别是当它们接受和/或返回自己的参数时。但没关系:这迈出的一小步,如果你卡住了,再看一遍,多看几遍…没事的。
闭包有三种表现方式:
- 函数章节中提到的全局函数(全域函式)是一种有名称但不会捕获任何值的闭包。
- 嵌套函数(巢状函式)(nested function)是一种有名称且被包含在另一个函数(即上层函数)中的闭包,嵌套函数(巢状函式)可以从上层函数中捕获值。
- 闭包表达式就是使用简洁语法来描述的一种没有名称的闭包,可以在代码中被传递和使用,特性为能够捕获和储存在其前后文中任何常量与变量的参数,进而独立地执行工作。
闭包表达式
最简单的示例:
let 闭包 = {
print("这就是一个闭包")
}
闭包()//调用
这样就创建了一个没有名称的函数,并将该函数分配给“闭包”。你现在可以像调用一般函数一样调用“闭包( )”。
闭包的本质就是代码块,它是函数的升级版;函数是有名称、可复用的代码块,闭包则是比函数更加灵活的匿名代码块。
闭包与大括号
闭包和函数都可以取参数,但它们取参数的方式大不相同。以下是一个接受字符串和整数的函数:
func pay(user: String, amount: Int) {
// code
}
以下是与闭包完全相同的内容:
let payment = {
(user: String, amount: Int) in
// code
}
如你所见,参数已移动到大括号内,in关键字用于标记参数列表的结束和闭包主体本身的开始。
闭包表达式(closure expression)是一种利用简洁语法建立匿名函数的方式。同时也提供了一些优化语法,可以使得代码码变得更好懂及直觉。闭包表达式的格式如下:
{ (参数) -返回值类型 in
内部执行的代码
}
闭包将参数放在大括号内,以避免混淆Swift:如果我们写了let payment = (user: String, amount: Int)那么看起来我们试图创建一个元组,而不是闭包,那会有问题。
在大括号内包含参数列表说明了为什么 in 关键字如此重要——没有它,Swift很难知道你的闭包主体的实际起点,因为没有第二组大括号了。
在闭包中接受参数
例如,我们可以像这样制作一个接受地名字符串作为其唯一参数的闭包:
let driving = {
(place: String) in
print("I'm going to \(place) in my car")
}
函数和闭包之间的区别之一是,在运行闭包时不使用参数标签。因此,为了调用driving(),我们现在要写以下内容:
driving("London")
上述代码中可以看到,与函数相同是以大括号{ }将代码包起来,但省略了名称,包着参数的小括号( )摆到{ }里并接着箭头->及返回值类型。然后使用in分隔内部执行的代码。
闭包表达式可以使用常量、变量和inout类型作为参数,但不能有预设值。也可以在参数列表的最后使用可变量量参数(variadic parameter)。元组也可以作为参数和返回值。
下面是一个例子,从将一个函数当做另一个函数的参数开始,原始代码如下:
// 这是一个要当做参数的函数 功能为将两个传入的参数整数相加并返回
func addTwoInts(number1: Int, number2: Int) -> Int {
return number1 + number2
}
// 建立另一个函数,有三个参数依序为
// 类型为 (Int, Int) -> Int 的函数, Int, Int
func printMathResult(
_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
这边将函数addTwoInts()修改成一个匿名函数(即闭包)传入,代码如下:
// 呼叫 printMathResult() 函数 参数分别为 闭包, Int, Int
printMathResult({
(number1: Int, number2: Int) -> Int in
return number1 + number2
}, 12, 85)
// 打印出:97
/* 第一个参数为一个匿名函数(闭包) 如下
{(number1: Int, number2: Int) -> Int in
return number1 + number2
}
*/
上述代码中可以看到函数printMathResult()依旧为三个参数,第一个参数原本应该是一个函数,但可以简化成一个闭包并直接传入,其后为两个Int的参数。
接下来会使用上面这个例子,介绍几种优化语法的方式,越后面介绍的代码语法会越简洁,但使用上功能是一样的。
根据前后文推断类型
因为两数相加闭包是作为函数printMathResult()的参数传入的,Swift 可以自动推断其参数及返回值的类型(根据建立函数printMathResult()时的参数类型(Int, Int) -> Int),因此这个闭包的参数及返回值的类型都可以省略,同时包着参数的小括号()及返回箭头->也可以省略,修改如下:
printMathResult(
{
number1, number2 in
return number1 + number2}, 12, 85)
// 打印出:97
/* 第一个参数修改成如下
{number1, number2 in return number1 + number2}
*/
闭包返回值
闭包也可以返回值,它们的编写方式与参数相似:你将它们写入闭包中,直接in关键字前面。
为了证明这一点,我们将关闭我们的driving()并让它返回其价值,而不是直接打印它。
原来是这样打印:
let driving = {
(place: String) in
print("I'm going to \(place) in my car")
}
现在希望返回一个字符串而不是直接打印的闭包,因此我们需要in之前使用 -> String 表示返回的类型,然后像一般函数一样使用return:
let drivingWithReturn = {
(place: String) -> String in
return "I'm going to \(place) in my car"
}
然后可以运行该闭包并打印其返回值:
let message = drivingWithReturn("London")
print(message)
单表达式闭包隐式回传
单行表达式闭包可以通过隐藏return来隐式回传单行表达式的结果,修改如下:
printMathResult({
number1, number2 in
number1 + number2}, 12, 85)
// 打印出:97
/* 第一个参数修改成如下
{number1, number2 in number1 + number2}
*/
参数简称
Swift 能够自动为闭包提供参数简称的功能,可以直接以$0,$1,$2这种方式来依序呼叫闭包的参数。
如果使用了参数简称,就可以省略在闭包参数列表中对其的定义,且对应参数简称的类型会通过函数类型自动进行推断,所以同时in也可以被省略,修改如下:
printMathResult({
$0 + $1}, 12, 85)
// 打印出:97
/* 第一个参数修改成如下
{$0 + $1}
*/
什么时候应该使用参数简称?
以下三点请考虑:
- 参数很多吗?如果是这样,参数简称就最好别用。因为这会适得其反!如果你需要将$3还是$4与$0进行判断,给他们一个实际的名称,代码就会变得更加清晰。
- 该功能是常用的吗?随着你的Swift技能的进步,你会开始意识到,有些人会使用闭包的常见函数。所以你的代码要考虑到其他人可以轻松理解$0的含义。
- 在你的方法中使用了多次参数简称吗?如果你需要引用$0超过两三次,也许你应该给它一个真名。
再次强调重要的是,你的代码需要易于阅读和理解;至少你自己下一次再看,都能够一眼就看明白。虽然有时要简化代码,但并不总是漫无目的的简化。需要根据具体情况来优化和简化代码。
运算子函数
实际上还有一种更简洁的语法。Swift 的String类型定义了关于加号+的字串实作,其作为一个函数接受两个数值,并返回这两个数值相加的值。而这正好与最一开始的函数addTwoInts()相同,因此你可以简单地传入一个加号+,Swift会自动推断出加号的字串函数实作,修改如下:
printMathResult(+, 12, 85)
// 打印出:97
// 第一个参数修改成: +
以上介绍了四种优化闭包的语法,使用上功能都与最一开始的闭包相同,所以可以依照需求以及合适性,使用不同的优化语法,来让你的代码更简洁与直觉,当然都使用完整写法的闭包也是可以的。
尾随闭包
如果需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包(trailing closure)来加强函数的可读性。尾随闭包是一个写在函数括号()之后的闭包表达式,函数支援将其作为最后一个参数呼叫。以下是一个例子:
参数为闭包的函数
func someFunction(closure: () -> Void) {
// 内部执行的代码
}
// 内部参数名称为 closure
// 闭包的类型为 () -> Void 没有参数也没有返回值
不使用尾随闭包的函数
someFunction(closure: {
// 闭包内的代码
})
// 可以看到这个闭包作为参数 是放在 () 里面
使用尾随闭包的函数
someFunction() {
// 闭包内的代码
}
// 可以看到这个闭包作为参数 位置在 () 后空一格接着写下去
如果函数只有闭包这一个参数时,甚至可以将函数的()省略,修改如下:
// 使用尾随闭包进行函数呼叫 省略函数的 ()
someFunction {
// 闭包内的代码
}
let names = ["AT", "AE", "D", "S", "BE"]
//尾随闭包
var reversed = names.sorted() {
$0 > $1 }
print(reversed)
捕获值
闭包可以在其定义的前后文中捕获(capture)常量或变量,即使定义这些常量或变量的原使用区域已经不存在,闭包仍可以在闭包函数体内修改这些值。
Swift 中,可以捕获值的闭包的最简单形式是嵌套函数(巢状函式),也就是定义在其他函数内的函数。嵌套函数(巢状函式)可以捕获并存取上层函数(把它定义在其中的函数)内所有的参数以及定义的常量与变量,即使这个嵌套函数(巢状函式)已经回传,导致常量或变量的作用范围不存在,闭包仍能对这些已经捕获的值做操作。
以下是一个例子:
// 定义一个函数 参数是一个整数 回传是一个类型为 () -> Int 的闭包
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
// 用来储存计数总数的变量
var runningTotal = 0
// 嵌套函数(巢状函式) 简单的将参数的数字加进计数并返回
// runningTotal 和 amount 都被捕获了
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
// 返回捕获变量参考的嵌套函数(巢状函式)
return incrementer
}
一个函数makeIncrementor ,它有一个Int型的参数amout, 并且它有一个外部参数名字forIncremet,意味着你调用的时候,必须使用这个外部名字。返回值是一个()-> Int的函数。
内部存取了的runningTotal及amount变量,是因为它从外部函数捕获了这两个变量的参考。而这个捕获参考会让runningTotal与amount在呼叫完makeIncrementer函数后不会消失,并且下次呼叫incrementer函数时,runningTotal仍会存在。
以下是呼叫这个函数的例子:
// 宣告一个常量
// 会被指派为一个每次呼叫就会将 runningTotal 加 10 的函数 incrementer
let incrementByTen = makeIncrementer(forIncrement: 10)
// 呼叫多次 可以观察到每次返回值都是累加上去
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30
// 如果另外再宣告一个常量
// 会有属于它自己的一个全新独立的 runningTotal 变量参考
// 与上面的常量无关
let incrementBySix = makeIncrementer(forIncrement: 6)
incrementBySix() // 6
incrementBySix() // 12
// 第一个常量仍然是对它自己捕获的变量做操作
incrementByTen() // 40
闭包是参考类型
前面的例子中,incrementByTen和incrementBySix是常量,但这些常量指向的闭包仍可以增加其捕获的变量值,这是因为函数与闭包都是参考类型。
参考类型就是无论将函数(或闭包)指派给一个常量或变量,实际上都是将常量或变量的值设置为对应这个函数(或闭包)的参考(参考其在记忆体空间内配置的位置)。
所以当你将闭包指派给了两个不同的常量或变量,这两个值都会指向同一个闭包(的参考),如下:
// 指派给另一个常量
let alsoIncrementByTen = incrementByTen
// 仍然是对原本的 runningTotal 操作
alsoIncrementByTen() // 50
逃逸闭包
当一个闭包被当做参数传入一个函数中,但是这个闭包在函数返回后才被执行(例如像是闭包被当做函数的返回值,然后接着被拿去做别的操作),这样称作闭包从函数中逃逸(escape)。你可以在参数类型前面加上@escaping来明确标注这个闭包是可以逃逸的。例子如下:
// 参数为一个闭包的函数 参数类型前面标注 @escaping
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
上述代码可以看到函数someFunctionWithEscapingClosure()将一个闭包当作参数传入,并将这个闭包加入到一个定义好的阵列,以将这个阵列(与其内的闭包们)作为后续使用,这时如果没有在参数类型前面加上@escaping,则会遇到编译时错误。
另外还有一点,将闭包标注为@escaping,表示你必须在闭包中显式地参考self,而非逃逸的闭包可以隐式地参考self(例如原本应该写self.x的,可以简化写成x,因为可以隐式参考self,会自动推断为self的x属性),例子如下:
// 定义另一个[参数不为逃逸的闭包]的函数
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
// 定义一个类别
class SomeClass {
var x = 10
func doSomething() {
// 使用到前面定义的两个函数 都使用了尾随闭包来让语法更为简洁
// 传入当参数的闭包 内部都是将实体的属性指派为新的值
// 参数类型标注为 @escaping 的闭包
// 需要显式地参考 self
someFunctionWithEscapingClosure {
self.x = 100 }
// 而为非逃逸的闭包
// 其内可以隐式地参考 self
someFunctionWithNonescapingClosure {
x = 200 }
}
}
// 生成一个实体
let instance = SomeClass()
// 呼叫其内的方法
instance.doSomething()
// 接着那两个前面定义的函数都会被呼叫到 所以最后实体的属性 x 为 200
print(instance.x)
// 接着呼叫阵列中的第一个成员
// 也就是示范逃逸闭包的函数中 会将闭包加入阵列的这个动作
// 而这个第一个成员就是 { self.x = 100 }
completionHandlers.first?()
// 所以这时实体的属性 x 便为 100
print(instance.x)
自动闭包
自动闭包(autoclosure)是一种自动被建立的闭包,用于包装后传递给函数作为参数的表达式。这种闭包没有参数,而当被使用时,会返回被包装在其内的表达式的值。
也就是说,自动闭包是一种简化的语法,让你可以用一个普通的表达式代替显式的闭包,进而省略了闭包的大括号{ }。
自动闭包让你可以延迟求值,因为这个闭包会直到被你呼叫时才会执行其内的代码,以下先示范一个普通的闭包如何延迟求值:
// 首先宣告一个有五个成员的阵列
var customersInLine = ["Albee","Alex","Eddie","Zack","Kevin"]
// 打印出:5
print(customersInLine.count)
// 接着宣告一个闭包 会移除掉阵列的第一个成员
let customerProvider = {
customersInLine.remove(at: 0) }
// 这时仍然是打印出:5
print(customersInLine.count)
// 直到这个闭包被呼叫时 才会执行
// 打印出:开始移除 Albee !
print("开始移除 \(customerProvider()) !")
// 这时就只剩下 4 个成员了 打印出:4
print(customersInLine.count)
上述代码可以看到闭包直到被呼叫时,才会移除成员,所以如果不呼叫闭包的话,则阵列成员都不会被移除。另外要注意一点,这个闭包customerProvider的类型为() -> String,而不是String。
将闭包作为参数传递给函数时,一样可以延迟求值,如下:
// 这时 customersInLine 为 ["Alex", "Eddie", "Zack", "Kevin"]
// 定义一个[参数为闭包]的函数
func serve(customer customerProvider: () -> String) {
// 函数内部会呼叫这个闭包
print("开始移除 \(customerProvider()) !")
}
// 呼叫函数时 [移除阵列第一个成员]这个动作被当做闭包的内容
// 闭包被当做参数传入函数
// 这时才会移除阵列第一个成员
serve(customer: {
customersInLine.remove(at: 0) } )
接着则介绍如何使用自动闭包完成上述一样的动作。你必须在参数类型前面标注@autoclosure,以表示这个参数可以是一个自动闭包的简化写法,这时就可以将该函数当做接受String类型参数的函数来呼叫。这个类型前面标注@autoclosure的参数会将自己转换成一个闭包,如下:
// 这时 customersInLine 为 ["Eddie", "Zack", "Kevin"]
// 这个函数的参数类型前面标注了 @autoclosure
// 表示这参数可以是一个自动闭包的简化写法
func serve(customer customerProvider: @autoclosure () -> String) {
print("开始移除 \(customerProvider()) !")
}
// 因为函数的参数类型有标注 @autoclosure 这个参数可以不用大括号 {}
// 而仅仅只需要[移除第一个成员]这个表达式 而这个表达式会返回[被移除的成员的值]
serve(customer: customersInLine.remove(at: 0))
如果你想要自动闭包允许逃逸,则必须同时标注@autoclosure和@escaping,以下是一个例子:
// 这时 customersInLine 为 ["Zack", "Kevin"]
// 宣告另一个变量 为一个阵列 其内成员的类型为 () -> String
var customerProviders: [() -> String] = []
// 定义一个函数 参数类型前面标注 @autoclosure @escaping
// 表示参数是一个可逃逸自动闭包
func collectCustomerProviders(
_ customerProvider: @autoclosure @escaping () -> String) {
// 函数内部的动作是将当做参数的这个闭包 再加入新的阵列中
// 因为可逃逸 所以不会出错
customerProviders.append(customerProvider)
}
// 呼叫两次函数
// 会将 customersInLine 剩余的两个成员都移除并转加入新的阵列中
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
// 打印出:获得了 2 个成员
print("获得了 \(customerProviders.count) 个成员")
// 最后将这两个成员也从新阵列中移除
for customerProvider in customerProviders {
print("开始移除 \(customerProvider()) !")
}
// 依序打印出:
// 开始移除 Zack !
// 开始移除 Kevin !
看明白了吗?
估计看完有点矇吧…
理解难度
★★★★★
实用程度
★☆☆☆☆
没关系多看几次。但是也不用花太多的时间卡在这里。这里的理解难度高,而实用程度低,表示闭包代码有其他的取代方式。当下能够写出自己看得懂的代码,比用了一段代码却不知道为啥还来得重要。
也许你永远也不会用上闭包!何必因这个混淆的篇章就打坏了你的学习兴致呢?大可不必。