IOS三人行

Closures——闭包

2016-04-29  本文已影响77人  Laughingg

Swift 2.2

我总是觉得 Swift 的语法书有点晦涩。看的不是那么让人理解。说实在话,语法书的闭包我看了好多遍,说白了我还是没看懂说了啥。语法书提的例子让我很是不能理解。哎! 只能说我自己笨吧。

1、闭包的表达式

// 闭包表达式
{ (parameters) -> returnType in
    statements
} 

闭包的定义:
闭包: 闭包是自包含的函数代码块,可以在代码中被传递和使用。

看着特么蛋疼的定义,不知所云。对于我这种俗人来说都特么是废话。 说白了我真没看懂。

对闭包的理解我看的最多的解释是,闭包和 Objc 的 block 类似。   

按照对 block 的理解,block 主要是用来保存一段代码,在需要的时候进行执行。 闭包的功能类似,差不多也是保存一段代码,在需要的时候进行执行。

1.1 闭包表达式分析

// 闭包表达式
{ (parameters) -> returnType in
    statements
}

// in 前:闭包的声明 
// in 后:闭包的具体实现
// in 只是作为闭包的声明和实现的 分隔符

// 使用闭包声明可以声明一个闭包的变量
var closures = { (parameters) -> returnType in

}

// 使用 闭包变量我们可以执行闭包
closures()

// 闭包的调用实际上是执行闭包中保存的代码块 in 后面的具体内容
// 我们使用的最简单的闭包调用是
{ (parameters) -> returnType in
    statements
}()

说明:
  -> 表示:执行结果输出给后面的值

1.2 最简单的闭包

// 没有参数没有返回值的闭包 (没有参数,没有返回值)
{ (()->()) in 

}

// 这个和上面是等价的
{ (()->Void) in 

}

// 简单闭包 
{() in 

}

// 最最最最简单的闭包 和上面还是等价的
{

}

1.3 闭包的参数

2. 内联闭包

内联闭包:将闭包作为函数的参数的闭包叫 内联闭包

2.1 sort 函数的使用

// **** sort 方法的使用 ****
// 定义一个数组
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// 使用标准的 sort 函数进行排序操作
// 会根据您提供的用于排序的闭包函数将已知类型数组中的值进行排序.
/*
 // 排序用的闭包
 {(s1: String, s2: String) -> Bool in
    return s1 > s2
 }
 
 // 闭包的数据类型
   (String, String) -> Bool
 
 // sort 函数确认的参数类型 
   (String, String) -> Bool
 // 传入两个数组元素相同类型的两个值,返回 bool 来  表明第一参数是在第二个参数的前面还是后面。
 
 // 第一个参数在第二个参数前面  闭包返回值为 true
 // 第二个参数在第一个参数前面  闭包返回值为 false
*/
let newNames = names.sort({ (s1: String, s2: String) -> Bool in
    return s1 > s2
})

// 排序后的新数组
print(newNames)

// sort 函数并没有改变原来的数组
print(names)



// *** 函数是一个特殊的闭包 *** 
// 函数的数据类型为: (String, String) -> Bool
func backwards(s1: String, s2: String) -> Bool {
    return s1 > s2
}

// 给 sort 传入一个函数( 特殊的闭包 )
var reversed = names.sort(backwards)
print(reversed)

// 将闭包作为参数传入函数这个闭包叫做内联闭包
// 内联闭包中包含闭包参数和闭包的实现(函数实现),使用 in 关键字进行分隔。
// 闭包简短可以直接写作一行
reversed = names.sort({ (s1: String, s2: String) -> Bool in return s1 > s2 } )
print(reversed)
Snip20160819_1.png

2.2 内联闭包 —— 类型上下文推断

排序闭包函数是作为sort(_:)方法的参数传入的,Swift 可以推断闭包参数和返回值的类型。
(sort 函数的参数类型 == 闭包的类型)

reversed = names.sort( { s1, s2 in return s1 > s2 } )

内联闭包表达式构造的闭包 作为参数传递给函数或方法时,都可以推断出闭包的参数和返回值类型。

  • 内联闭包作为函数或者方法的参数时,不需要利用完整格式构造内联闭包。(在写法上可以简化)
  • 完整格式的闭包能够提高代码的可读性,则可以采用完整格式的闭包。
    ** 简化写法慎用,闭包功力不深厚,可能写完之后自己都要看半天。**

2.3 内联闭包 —— 单表达式闭包隐式返回

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

reversed = names.sort( { s1, s2 in s1 > s2 } )

// 这个 return 关键字的省略是由 sort 的参数类型决定的。(其实还是类型推断的功劳)
// sort 函数的参数类型是  (String, String) -> Bool 
// 所以你写不写 return 闭包都会要求返回一个 bool 值。

2.4 内联闭包 —— 参数名称的缩写

Swift 自动为内联闭包提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数,以此类推。
内联闭包参数名称的缩写还是基于 类型上下文推断

sort 函数的参数类型是  (String, String) -> Bool 

从而可以确定作为 sort 函数参数的 内联闭包 有两个参数。

如果按照参数的列表的顺序来确定参数:
  * $0 就可以代替 内联闭包 的第一个参数,
  * $1 就可以代替 内联闭包 的第二个参数,
  * $2 就可以代替 内联闭包 的第三个参数,
  * 依次类推 ...

由于内联闭包的参数列表中参数名称都已经确定,所以参数列表写不写出来问题不大。没有参数列表和返回值类型(返回值类型直接推断得出)in 关键字存在的意义也就不大(in 关键字只是作为闭包中声明和实现的分隔标识),就可以直接省略。

** 简化版本 **

reversed = names.sort( { $0 > $1 } )

2.5 内联闭包 —— 运算符函数

Swift 的 String类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。

** String 的 > 操作符函数声明**

(String, String) -> Bool

sort(_:) 函数的参数类型

(String, String) -> Bool

使用运算符函数的简化

reversed = names.sort(>)

2.6 内联闭包 —— 尾随闭包(Trailing Closures)

如果您需要 将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。

尾随闭包:是一个书写在函数括号之后的闭包表达式。

按照字面理解
只要是函数的最后一个参数是闭包,我们就可以使用尾随闭包。

将内联闭包写在括号的外面 —— 我特么可以理解为外联闭包吗?

// 函数定义
func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
}

// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure({
    // 闭包主体部分
})

// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
    // 闭包主体部分
}



// *** 对个参数,最后一个参数是闭包 ***
func newSomeFunctionThatTakesAClosure( name: String,closure: () -> Void) {
       // 函数体部分
}  

newSomeFunctionThatTakesAClosure("zhangSan1", closure: {
      // 闭包主体部分
})

// 由于 XCode 的优化,默认生成的是这种格式。
newSomeFunctionThatTakesAClosure("zhangSan2"){
      // 闭包主体部分
}

使用尾随闭包语法对sort 函数的调用:

// 尾随闭包
reversed = names.sort() { $0 > $1 }

// 简化,省略 ()
reversed = names.sort { $0 > $1 }

当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。

** 如果函数需要一个闭包参数作为参数,且这个参数是最后一个参数,而这个闭包表达式又很长时 —— 请使用尾随闭包! **

2.6.1 map 函数的使用 (尾随闭包的优雅使用)

// *** 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]

/**
 Swift 的 Array 类型有一个map(_:)方法,
    其获取一个闭包表达式作为其唯一参数。
    
    该闭包函数会为数组中的每一个元素调用一次,并返回该元素所映射的值。
 
    具体的映射方式和 返回值类型 由闭包来指定。
 
 */

// 这里使用了尾随闭包
// map 会为 numbers 中每一个元素调用一次。调用 map 的返回值由传入的闭包决定。
let strings = numbers.map {
    (number) -> String in
    
    // 映射的具体实现
    var number = number
    var output = ""
    while number > 0 {
        output = digitNames[number % 10]! + output
        number /= 10
    }
    
    // 返回 numbers 映射 的结果
    return output
}

print(strings)
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]

map 内联闭包的写法不能再简化。map 的参数不能准确的推断 闭包的类型 , 只能确定参数不能确定返回值。

2.7. 捕获值(Capturing Values)

闭包可以在其被定义的上下文中捕获常量或变量。
即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
(特么的,没看懂。)

什么叫捕获,捕获就是可以在闭包的实现 (in 关键字的后面)中可以获取(拿到)闭包外面(大括号外面)的 变量和常量。

为什么,即使定义的常量和变量的作用域不存在了,闭包仍然可以在闭包函数的体内引用和修改这些值 ?
结合对引用计数器的使用, 每一个传入到 闭包中的 变量和常量都会进行引用计数器加 1 操作。只有在闭包所在的作用域结束后,统一对闭包中的变量和常量的引用计数器减 1 操作。
对引用计数器不理解的看objc 的内存管理。

闭包的死结 ——> 闭包的循环引用

** 嵌套函数 (闭包嵌套 )**
Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

func makeIncrementor(forIncrement amount: Int) -> () -> Int {

    // 声明一个变量(写在参数列表中的变量(常量)和写在这里声明的变量(常量)是等效的)
    var runningTotal = 0

    // 嵌套函数 ( 特殊的闭包 )
    func incrementor() -> Int {

        // 在闭包中 会对  amount 和  runningTotal 的引用计数器 进行 加 1 操作
        runningTotal += amount
        return runningTotal
    }

    // 返回一个闭包
    /*
      在这一步之后,没有变量对  incrementor 进行引用,闭包就会对 闭包中使用的 常量 和 变量进行计数器 减 1 操作。 
      当函数的作用域完毕后,一切都和谐了
    */
    return incrementor
}


// 在之后函数之后 incrementor 对 函数的 return 的结果(闭包)进行了引用。
// 同时 incrementor 也就间接引用了 闭包中的 变量和常量。
let incrementor = makeIncrementor(forIncrement: 100)

// 执行 闭包
print(incrementor())
print(incrementor())
print(incrementor())

打印结果
100
200
300

incrementor 变量的作用域过完之后,闭包中的变量和常量就会释放。

为了优化,如果一个值是不可变的,Swift 可能会改为捕获并保存一份对值的拷贝。

2.8.1 闭包的循环引用

如果您将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,您将创建一个在闭包和该实例间的循环强引用。

3 闭包是引用类型

说白了就是闭包的值传递过程传递的是引用。

var incrementor = makeIncrementor(forIncrement: 100)
var oneIncrementor = incrementor
var twoIncrementor = oneIncrementor

print(incrementor())
print(oneIncrementor())
print(twoIncrementor())

打印结果
100
200
300

// incrementor 、 oneIncrementor 、twoIncrementor 操作的是同一个闭包。

4 非逃逸闭包

逃逸: ** 当一个闭包作为参数传到一个函数中,但是 这个闭包在函数返回之后才被执行**( return 之后),我们称该闭包从函数中 逃逸

定义接受闭包作为参数的函数时,你可以在参数名之前标注@noescape,用来指明这个闭包是不允许“逃逸”出这个函数的。
** 标注为 @noescape ** 的闭包,只能在函数体内执行。生命周期就是函数体的大括号。

大白话就是 : 不允许在函数体内 return 传入的闭包。不允许将传入的闭包在函数体内保存到函数体外的变量中或数组中。

** 非逃逸闭包 **

// 非逃逸闭包
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) {
    print(#function)
    
    // 闭包只能在函数体内执行
    closure()
}

someFunctionWithNoescapeClosure { 
    print("noescape")
}

打印结果
someFunctionWithNoescapeClosure
noescape

** 逃逸闭包 **
return 逃逸

// 逃逸闭包
func someFunctionWithEscapeClosure(closure: () -> Void) -> (() -> Void){
    
    print(#function)
    // 将闭包返回
    return closure
}

let escapeClosureFunction = someFunctionWithEscapeClosure {
    print("escape")
}

// 在函数体外执行逃逸出来的闭包
escapeClosureFunction()

外部变量引用逃逸

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: () -> Void) {

    // 将变量添加到外部的数组容器中
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure {
    print("someFunctionWithEscapingClosure + 1")
}

someFunctionWithEscapingClosure {
    print("someFunctionWithEscapingClosure + 2")
}

someFunctionWithEscapingClosure {
    print("someFunctionWithEscapingClosure + 3")
}

completionHandlers[0]()
completionHandlers[1]()
completionHandlers[2]()

打印结果
someFunctionWithEscapingClosure + 1
someFunctionWithEscapingClosure + 2
someFunctionWithEscapingClosure + 3
// 非逃逸闭包逃逸,编译器会直接报错
func someFunctionWithNoescapeClosure(@noescape closure: () -> Void) -> (() -> Void) {
    print(#function)
    return closure
}

// 外部引用逃逸 ,编译直接报错
var completionHandlers: [() -> Void] = []
func someFunctionWithNoescapingClosure(@noescape completionHandler: () -> Void) {
    completionHandlers.append(completionHandler)
}

闭包标注为@noescape使你能在闭包中隐式地引用self。

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNoescapeClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// prints "200"

completionHandlers.first?()
print(instance.x)
// prints "100"

日常开发中逃逸闭包的用武之地不多。
语法书中说的使用场景是异步回调中使用。


5. 自动闭包

自动闭包: 自动创建的闭包,用于包装传递给函数作为参数的表达式。

** 自动闭包** (有没有忍无可忍的感觉啊!)

// 自动闭包没有参数列表,自动闭包的返回结果就是 statements 执行的结果
{
      statements
}
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// prints "5"

// 创建了一个 customerProvider 自动闭包
let customerProvider = { customersInLine.removeAtIndex(0) }
print(customersInLine.count)
// prints "5"

// 数组中的值,只有闭包执行后才会移除
print("Now serving \(customerProvider())!")
// prints "Now serving Chris!"

print(customersInLine.count)
// prints "4"
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serveCustomer(customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}

// 显示的传入一个自动闭包
serveCustomer( { customersInLine.removeAtIndex(0) } )
// prints "Now serving Alex!"


// customersInLine is ["Ewa", "Barry", "Daniella"]
// @autoclosure 关键字标记参数为一个自动闭包,
func serveCustomer(@autoclosure customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}

// 由于函数的参数标记是一个自动闭包,可以在函数的 () 中传入一个表达式,
// 表达式会自动变为一个自动闭包
// 第一次看到这个表达式,十个人有九个变傻逼。
serveCustomer(customersInLine.removeAtIndex(0))
// prints "Now serving Ewa!"

过度使用 @autoclosure 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。

@autoclosure特性暗含了@noescape特性, 你想让这个闭包可以“逃逸”,则应该使用@autoclosure(escaping)特性.

闭包和函数的比较

推荐使用场景

上一篇下一篇

猜你喜欢

热点阅读