Swift教程之闭包

闭包

Swift的闭包类型类似于C和Objective-C中的块以及其他编程语言的Lambdas。

闭包可捕获并存储其定义的上下文中的任何常量和变量的引用,称为捕获常量和变量。

全局函数和嵌套函数实际上是闭包的特殊情况,闭包有三种形式:

  • 全局函数是具有函数名且不捕获值的闭包。
  • 嵌套函数是具有函数名且捕获函数体内的值的闭包。
  • 闭包表达式是轻量级的未命名闭包,且从其上下文中捕获值。

Swift的闭包表达式经过优化后鼓励使用简单、整洁的语法,包括:

  • 从上下文推倒参数和返回值类型
  • 单个闭包表达式的隐式返回
  • 参数名称简写
  • 尾随闭包语法

<br />

闭包表达式

闭包表达式是一种以简短语法编写内联闭包的方法。下面的闭包表达式示例通过闭包表达式说明这些语法优化。

排序方法

sorted(by:)方法根据传入的排序闭包的输出对已知类型的数组进行排序,并返回已排序好的新数组,原数组未被修改。

sorted(by:)方法接收一个闭包,该闭包接收与数组元素相同类型的两个参数,并返回Bool值,表示前值应该在后值之前(返回true)或之后(返回false)出现。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

然后,这是一个相当长的方式来编写本质上是单表达式函数(a > b),可使用闭包表达式语法来编写排序闭包。

闭包表达式语法

闭包表达式语法有以下形式:

{ (parameters) -> return type in

    statements

}

闭包表达式语法中的参数可以是in-out参数和可变参数,但不能有默认值。元组可用作参数类型和返回类型。

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

可将闭包表达式写在单行:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

从上下文推导类型

由于排序闭包作为参数传递给方法,Swift可推断其参数的类型和返回值的类型。因为可以推断所有的类型,所以返回箭头和参数名周围括号也可省略。

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

单表达式闭包的隐式返回

单表达式闭包可省略return关键字来隐式返回单个表达式的结果:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

参数名简写

Swift自动为内联闭包提供简写参数名称,可通过名称$0、$1、$2等引用闭包参数值。

若闭包表达式使用简写参数名称,则可以从定义中省略闭包的参数列表,并根据预期函数类型推断简写参数名称的数量和类型,in关键字也可省略,因为闭包表达式完全由其主体组成:

reversedNames = names.sorted(by: { $0 > $1 } )

这里的$0和$1表示闭包的第一个和第二个String参数。

运算符方法

还有更加简短的方式书写上述闭包表达式。Swift的操作符本质上就是函数,可以简单地传入运算符作为闭包表达式:

reversedNames = names.sorted(by: >)

<br />

尾随闭包

当需要传入一个较长的闭包表达式作为函数的最后一个参数时,那这个闭包表达式就是尾随闭包。使用尾随闭包语法时,不要将闭包的参数标签写入函数调用的一部分。

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}
 
// Here's how you call this function without using a trailing closure:
 
someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})
 
// Here's how you call this function with a trailing closure instead:
 
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

可以将sorted(by:)方法的字符串排序闭包写成尾随闭包:

reversedNames = names.sorted() { $0 > $1 }

当闭包表达式是函数的唯一参数时,且该表达式为尾随闭包,那么在调用该函数时不需要在函数名后写一对圆括号()

reversedNames = names.sorted { $0 > $1 }

Array类型的map(_:)方法将闭包作为唯一参数,其作用是返回一个按照闭包的映射规则映射所有元素的新数组。将该闭包表达式写成尾随闭包:

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

<br />

捕获值

闭包能从定义的上下文捕获常量和变量,即使定义常量和变量的原始范围不再存在,闭包也可以引用病修改这些值。

在Swift中,可以捕获值的最简单的闭包形式是一个嵌套函数。嵌套函数可以捕获其外部函数的任何参数,也可以捕获外部函数中定义的任何常量和变量。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

上述嵌套函数incrementer捕获了外部函数makeIncrementer的参数amount和外部函数定义的runningTotal变量。

incrementer函数保存为常量或变量,函数内对runningTotalamount的引用依然存在:

let incrementByTen = makeIncrementer(forIncrement: 10)   
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

创建另一个incrementer函数,捕获值的引用也随之初始化:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

调用原来的incrementByTen函数会继续引用自己的runningTotal变量,且不会影响到incrementBySeven函数捕获到的变量:

incrementByTen()
// returns a value of 40

<br />

闭包是引用类型

函数和闭包都是引用类型,无论何时为常量或变量分配函数或闭包,就将该常量或变量设置为对函数或闭包的引用。若将闭包分配给两个不同的常量或变量,这些常量或变量都将使用相同的闭包:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

<br />

逃逸闭包

非逃逸闭包的生命周期:

  1. 把闭包作为参数传递给函数。
  2. 函数中运行该闭包。
  3. 退出函数。

非逃逸闭包的引用计数和退出函数时保持不变。

逃逸闭包与非逃逸闭包相反,当函数退出时,逃逸闭包的引用仍然被其他对象持有,不会在持有函数结束后释放。

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}
 
class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}
 
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
 
completionHandlers.first?()
print(instance.x)
// Prints "100"

使用逃逸闭包的两个场景:

  • 异步调用:如果需要调度队列中异步调用闭包, 这个队列会持有闭包的引用,至于什么时候调用闭包,或闭包什么时候运行结束都是不可预知的。
  • 需要存储闭包作为属性,全局变量或其他类型做稍后使用。

<br />

自动闭包

自动闭包是一个自动创建的闭包,用于包装一个作为参数传递给函数的表达式。它没有任何参数,当被调用时,它返回包含在其中的表达式的值。

自动闭包让代码延迟之行,在调用闭包之前,内部代码不会运行。延迟之行对具有副作用或计算上昂贵的代码很有用,可以控制何时执行该代码:

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
 
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
 
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

即使customersInLine数组的第一个元素被封装内部的代码删除,数组元素在世纪调用闭包之前不会被删除。customerProvider的类型不是String,而是 () -> String :一个返回字符串没有参数的函数。

当将闭包作为函数传递给函数时,依然会延迟执行:

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

注意

过度使用自动闭包会使代码难以理解,应通过上下文和函数名称说明正在使用延迟执行。

同时使用逃逸闭包和自动闭包:

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
 
print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351

推荐阅读更多精彩内容