swift进阶(二) - 闭包(Closure)

前言:swift是一门很优雅,很简洁,功能很强大的语言,同时也是一门很复杂的语言。进门比较简单,看完苹果官方的文档就可以基本入门,进阶很难,很多比较好用的特性需要深入探索。

1.什么是闭包(Closure)?

闭包是一个完整的设计功能模块,可以在代码中传递和使用,类似于Object-C的block(但是还是有区别,下面会说明)或者其他语言的匿名函数(lambdas)。闭包可以捕获或者储存它们所在上下文的常量和变量。在Swift里等价于函数,是一等公民。

闭包有三种形式:

  • 全局函数,有名字的闭包并且不捕获任何值(定义的一般函数)
  • 嵌套函数,有名字的闭包,可以在闭包所在函数内部捕获值(函数里嵌套函数)
  • 闭包表达式,没有名字的闭包,使用简洁的语法,可以在包裹闭包的上下文捕获值(闭包)

举例说明:

//Global function
func block() {
    print("block")    //block
}

//Nested function
func block(){
    let name = "block"
    func printStr() {
        print(name)
    }
    printStr()
}
block()    //block

//Closure expression
let block = {
    print("block")
}
block()    //block

swift对闭包的表达式做了相关的优化:

  • 从上下文推断传入参数和返回值
  • 单一表表达式闭包的隐式返回
  • 简短的参数名
  • 尾随闭包

举例说明:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted { (a: Int, b: Int) -> Bool in
    return a > b
}
//下面常量返回的都是[8, 7, 5, 4, 3, 2, 1]

//从上下文推断传入参数和返回值
let sortedNums2 = numbers.sorted { (a, b) in
    return a > b
}

//单一表表达式闭包的隐式返回
let sortedNums = numbers.sorted { $0 > $1 }

//简短的参数名
let sortedNums2 = numbers.sorted { (a, b) in
    return a > b
}

//尾随闭包
let sortedNums = numbers.sorted { $0 > $1 }

2.闭包的表达式语法(Closure Expressions)

定义:

{(parameters) -> return type in
  statements
}

举一些例子:

//没有参数和返回值的block定义
let noParameterAndNoReturnValue: () -> () = {
    print("Hello world!")
}

//没有参数,有返回值的block定义
let noParameterAndReturnValue: () -> (Int) = {
    return 5
}

//有一个参数和返回值的block定义
let oneParameterAndNoReturnValue: (Int) -> (Int) = { x in
    return x + 2
}

//有多个参数和返回值的block定义
let mutipleParameterAndNoReturnValue: (Int, Int) -> (Int) = { (x, y) in
    return x + y
}

3.简短的参数名字(Shorthand argument syntax)

swift支持类型推断,什么意思呢?就是闭包的参数和返回类型都可以交给编译器去推断,在编码阶段就可以省略。闭包里面$0,$1代表的是传入的第一个参数和第二个参数,下面看代码:

//$0代表第一个参数,$1代表第二个参数,语法非常简洁
let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted { $0 > $1 }    //[8, 7, 5, 4, 3, 2, 1]

顺便提一下函数(闭包)参数省略的过程,还是以数组降序为例:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted { (a: Int, b: Int) -> Bool in
    return a > b
}    //[8, 7, 5, 4, 3, 2, 1]

如果一个函数的返回类型和参数类型可以推导出来,则返回类型和参数类型都可以省略。删除:Int,-> Bool,上面的表达式变成:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted { (a, b) in
    return a > b
}    //[8, 7, 5, 4, 3, 2, 1]

如果参数的个数可以推导出来,则参数可以省略,使用$0,$1的方式代表参数。参数省略了,in关键字在这里就没有意义了,也可以省略,则上面的表达式变成:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted {
    return $0 > $1
}    //[8, 7, 5, 4, 3, 2, 1]

在swift里,如果函数体只有一行,则可以把return关键字省略,单一表达式闭包隐式返回,则代码进一步演变成:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted {
   $0 > $1
}    //[8, 7, 5, 4, 3, 2, 1]

最后,还能更近一步简化。可能很多人都郁闷了,就剩两个参数和操作符了,还能怎么简化?别急,swift里面还有一个简化规则,因为<也是函数,并且和函数sorted函数接收的参数个数,类型和返回值都一样,所以,能推导出来的东西都能简化,那么,更暴力的简化来了:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted (by: > )    //[8, 7, 5, 4, 3, 2, 1]

看出来什么了没有?没错,这里的简化不是放在闭包里面的。我的理解是,整个闭包等价于>函数,所以,可以把整个闭包替换成了>函数,举个例子:

let block: (Int, Int) -> (Int) = { $0 + $1 }
func testBlock(block: (Int, Int) -> (Int)) -> Int {
    return block(1,3)
}

testBlock{ $0 + $1 }    //3
testBlock(block: block)    //3

4.尾随闭包(Trailing Closures)

If you need to pass a closure expression to a function as the function’s final argument and the closure expression is long, it can be useful to write it as a trailing closure instead. A trailing closure is written after the function call’s parentheses, even though it is still an argument to the function. When you use the trailing closure syntax, you don’t write the argument label for the closure as part of the function call.

如果函数的最后一个参数是闭包,可以使用尾随闭包代替,举个例子:

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
}

以数组的排序函数作为例子来看一下:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted(by: { $0 > $1 })    //没有使用尾随闭包,整个闭包写在sorted函数参数圆括号内,闭包内容多的话会显的很乱
let sortedNums = numbers.sorted() { $0 > $1 }    //使用尾随闭包,这样会使代码看起来很整洁

swift里还有一个规则,如果函数只有闭包一个参数,作为尾随闭包,可以把()去掉,使代码更为简洁,代码如下:

let numbers = [1, 3, 2, 4, 7, 8, 5]
let sortedNums = numbers.sorted(){ $0 > $1 }    //没有去掉"()"
let sortedNums = numbers.sorted { $0 > $1 }    //去掉"()"

5.闭包捕获值(Capturing Values)

A closure can capture constants and variables from the surrounding context in which it is defined.

闭包可以捕获包裹它的上下文所定义的常量和变量。

(1)全局函数

var number = 0
var add = {
    number += 1
    print(number)
}

add()    //1
add()    //2
add()    //3
print(number)    //3

从上面的代码可以看出来,闭包捕获的是值的引用,当闭包内修改闭包外的值,闭包外的值也会跟着改变。

(2)函数嵌套函数

func makeIncrementer(from start: Int, amount: Int) -> ()->Int {
    var number = start
    return {
        number += amount
        return number
    }
}

let incrementer = makeIncrementer(from: 0, amount: 1)
incrementer()  //1
incrementer()  //2
incrementer()  //3

函数makeIncrementer返回的是一个没有参数返回整数的函数(闭包),所以,常量incrementer其实就是一个函数。每次调用incrementer()都会执行闭包里面的操作,而闭包的上下文就是makeIncrementer函数。从这也可以看出来,函数既能当返回值,也可以做参数,在swift妥妥的一等公民,比在Object-C的功能强大多了。

(3)swift中closure(闭包)和Object-C中block的区别

//block
NSInteger number = 1;
NSMutableString *str = [NSMutableString stringWithString: @"hello"];
void(^block)() = ^{
  NSLog(@"%@--%ld", str, number);
};
[str appendString: @" world!"];
number = 5;
block();    //hello world!--1

//closure
var str = "hello"
var number = 1
let block = {
    print(str + "--" + " \(number)")
}
str.append(" world!")
number = 5
block()    //hello world!--5

一句话来说,block内部会对值类型做一份复制,并不指向之前的值的内存地址,而对于对象类型则会指向对象当前的内存地址,所以修改number时,block里的number数值不变,而修改字符串时,block里的字符串则改变了;closure则默认给外部访问的变量加上了__block修饰词的block。至于闭包里的循环引用,可以看一下OC与Swift闭包对比总结这篇文章。

6.逃逸闭包(Escaping Closures)

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

逃逸闭包,指的是当一个函数有闭包作为参数,但是闭包的执行比函数的执行要迟。通俗来说,这个闭包的作用域本来是在当前函数里面的,然后它要逃出这个作用域,不想和函数同归于尽。那么闭包怎么逃逸呢?最简单的方法是把闭包赋值给外面的变量,举个例子:

var completionHandlers: [() -> Void] = []
//必须加上@escaping,不然编译会报错
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure {
    print("hello")
}    //函数执行完,不会打印"hello"
completionHandlers.first?()    //打印"hello"

如果逃逸闭包访问的是类里面的成员,必须带上self来访问;如果访问的是全局的变量,则和非逃逸闭包一样。我的理解是,既然闭包逃逸了,则出了函数的作用域,则如果需要访问类里面的成员也找不到地址,因为函数已经被销毁,所以,需要带上类的地址self指针。

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"

7.自动闭包(Autoclosures)

An autoclosure is a closure that is automatically created to wrap an expression that’s being passed as an argument to a function. It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it. This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.

自动闭包,我理解是,没有参数,函数体只有返回值,没有多余的其他变量,举个例子:

let printStr = { "hello" }
print(printStr())  //hello

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

注意:要保证自动闭包里面代码能正确执行,比如,在执行customerProvider()之前把数组清空,那么执行customerProvider()会报错,代码如下:

customersInLine.removeAll()
customerProvider()    //fatal error: Index out of range

自动闭包作为函数参数,不写"{}",直接写返回值

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

//一般闭包
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: {customersInLine.remove(at: 0)})    //Now serving Chris!

//自动闭包
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))    //Now serving Chris!

逃逸的自动闭包:

var customersInLine = ["Barry", "Daniella"]

// 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!"

8.总结

swift的闭包比Object-C的block功能强大很多,更简洁,更高效。而且,很多集合类型都集成了闭包,比如:map,flatMap等等。这些闭包的功能都很强大,也为swift添加了不少便利性。

参考:
Swift 烧脑体操(二) - 函数的参数
The Swift Programming Language (Swift 3.0.1)
Chapter 9: Closures Swift Programming from Scratch
OC与Swift闭包对比总结
iOS OC语言: Block底层实现原理

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

推荐阅读更多精彩内容

  • 以下翻译自Apple官方文档,结合自己的理解记录下来。翻译基于 swift 3.0.1 原文地址 Closure...
    艺术农阅读 1,428评论 0 3
  • 86.复合 Cases 共享相同代码块的多个switch 分支 分支可以合并, 写在分支后用逗号分开。如果任何模式...
    无沣阅读 1,272评论 1 5
  • Swift-闭包 Swift闭包的含义 闭包是自包含的功能代码块,可以用作函数的参数或者返回值 闭包可以捕获上下文...
    stackJolin阅读 1,162评论 0 2
  • 我无意指摘你的人生,只是觉得你可以更好。
    我是许云阅读 259评论 0 0
  • 文/@纹子同学 极品同事(一) 中年妇女一枚,声音像小姑娘一样嗲,刚进单位,谁也不认识,我被迫跟她一组(后来...
    纹子同学阅读 250评论 0 0