Swift 学习- 08 -- 闭包

Posted Dingzhijie

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift 学习- 08 -- 闭包相关的知识,希望对你有一定的参考价值。

// 闭包是自包含的函数代码块, 可以在代码中被传递和使用, swift 中的闭包 与 C语言 和 OC 中的代码块 (blocks) 以及其他一些编程语言中的匿名函数比较类似

 

// 闭包可以捕获和存储其所在上下文中任意常量和变量的引用, 被称为包裹常量和变量, swift 会为你管理在捕获过程中涉及到的所有内存操作

 

// 在 ‘函数‘ 章节中介绍的全局和嵌套函数实际上也是特殊的闭包, 闭包采取如下三种形式之一:

// 1: 全局函数是一个有名字但不会捕获任何职的闭包

// 2: 嵌套函数是一个有名字并且可以捕获其封闭函数域内值的闭包

// 3: 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

 

// swift 的闭包表达式拥有简洁的风格, 并鼓励在常见的场景中进行语法优化,主要优化如下:

// 1: 利用上下文推断参数和返回值类型

// 2: 隐式返回单表达式闭包, 即单表达式闭包可以省略 return 关键字

// 3: 参数名缩写

// 4: 尾随闭包语法

 

 

// 闭包表达式

// ‘嵌套函数‘ 是一个在较复杂函数中方便进行命名和定义自包含代码模块的方式, 当然,有时候编写写小巧的没有完整定义和命名的类函数结构也是很有用处的, 尤其是你在处理一些函数并需要将另外一些函数作为该函数的参数时

 

// ‘闭包表达式‘是一种利用简洁语法构建内联闭包的方式, 闭包表达式提供了一些语法优化, 使得撰写闭包变得简单明了, 下面闭包表达式的例子是通过使用几次迭代展示了sorted(by:) 方法定义和语法优化的方式, 每一次迭代都用更简洁的方式描述了相同的功能

 

 

// sorted 方法

// swift 标准库提供了名为 sorted(by:)的方法, 它会根据你所提供的用于排序的闭包函数将已知类型数组中的值进行排序, 一旦排序完成, sorted(by:) 方法会返回一个与原数组大小相同, 包含同类型元素且元素已正确排序的新数组, 原数组不会被sorted(by:) 方法修改

 

// 下面的闭包表达式示例使用 sorted(by:) 方法对一个 string 类型的数组进行字母逆序排序, 以下是初始数组

 

let names = ["Chris","Alex","Ewa","Barry","Daniella"]

 

// sorted(by:)方法接受一个闭包,改闭包函数需要传入与数组元素类型相同的两个值,并且返回一个布尔值来表明当排序结束后传入的第一个参数排在第二个参数的前面还是后面, 如果第一个参数值出现在第二个参数值 前面, 排序闭包函数需要返回 true ,反之需要返回 false 

 

// 改例子对一个 String 类型的数组进行排序, 因此排序闭包函数类型需要为 (String, String) -> Bool.

 

// 提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数, 并将其作为 sorted(by:) 方法的参数传入:

 

func backward(_ s1:String,_ s2:String) -> Bool{

    return s1 < s2

}

 

var reversedNames = names.sorted(by:backward(_:_:))

print(reversedNames)

 

 

// 闭包表达式语法

// 闭包表达式语法有如下的一般形式

// { (parameters) -> returnType in

//     statements

// }

 

// 闭包表达式可以是 in-out 参数, 但是不能设定默认值, 也可以使用具名的可变参数 (但是如果可变参数不放在参数列表最后一位的话,调用闭包时编辑器将报错) , 元组也可以作为参数和返回值

 

// 下面的例子展示了之前 backward(_:_:)函数对应的闭包表达式的版本的代码

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

    return s1 > s2

})

 

// 需要注意的是内联闭包参数和返回值类型声明与 backward(_:_:) 函数类型声明相同, 在这两种方式中, 都写成 (s1:Sreing ,s2:String) -> Bool, 然而在内联闭包表达式中,函数和返回值都写在大括号内, 而不是大括号外

// 闭包的函数体部分有关键字 ‘in‘ 引入, 改关键字表示闭包的参数和返回值类型定义已经完成, 闭包函数体即将开始.

// 由于这个闭包的函数部分如此短, 以至于可以将其改写成一行代码

 

// 改例子中 , sorted(by:) 方法的整体调用保持不变, 一对圆括号仍然包裹了方法的整个参数,然而, 参数现在变成了内联闭包. 

 

 

// 根据上下文推断类型

// 因为排序闭包函数是作为 sorted(by:) 方法的参数传入的, swift 可以推断其参数的返回值的类型, sorted(by:) 方法被一个字符串数组调用, 因此其参数必须是 (String,String) -> Bool 类型的函数, 这意味着 (String, String) 和 Bool 的类型并不需要作为闭包表达式的一部分, 因为所有的类型都可以被正确推断,返回箭头 (->) 和围绕在参数周围的括号也可以被省略

 

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

 

// 实际上, 通过内联闭包表达式构造的闭包作为参数传递给函数或方法时, 总是能够推断出闭包的参数和返回值类型, 这意味着闭包作为函数或者方法的参数时, 你几乎不需要利用完整格式构造内联闭包

 

// 尽管如此, 你仍然可以明确写出这有这完整格式的闭包, 如果完整格式的闭包能够提高代码的可队形, 则我们更鼓励采用完整格式的闭包, 而在 sorted(by:)方法的例子里面, 显然闭包的目的就是排序, 由于这个闭包是为了处理字符数组的排序, 因此读者能够推测出这个闭包是用于字符串处理的 

 

 

 

// 单表达式闭包隐式返回

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

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

 

 

// 参数名称缩写

// swift 自动为内联闭包提供了参数名称缩写功能, 你可以直接通过 $1, $1 ,$2 来顺序调用闭包的参数,依次类推

// 如果你在闭包表达式中使用参数名称缩写, 你可以在闭包定义中省略参数列表, 并且对应参数名称缩写的类型会通过函数类型进行推断, in 关键字也同样被省略, 因为此时闭包表达式完全由闭包函数体构成

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

 

//  在这个例子中, $0 和 $1 表示闭包中的第一个和迭戈 String 类型的参数

 

 

 

// 运算符方法

// 实际上还有一种更剪简短的方式来编写上面例子中的闭包表达式, swift 的 String 类型定义 大于号(>) 的字符串实现, 其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值, 而这正好与 sorted(by:)方法的参数需要的函数类型相符合, 因此, 你可以简单地传递一个 大于号 (>) swift 可以在自动推断出你想使用 大于号 的字符串函数实现

 

reversedNames = names.sorted(by: >)

 

 

 

// 尾随闭包

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

// ‘尾随闭包‘是一种书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用, 在使用尾随闭包时 , 你不用写出他的参数标签

 

func someFunctionThanTakesAClosure(closure:()->Void){

    

}

 

// 以下是不使用尾随闭包进行函数调用

someFunctionThanTakesAClosure(closure: {

    // 闭包主体

})

 

// 以下是使用尾随闭包进行函数调用

someFunctionThanTakesAClosure() {

    // 闭包主体

}

 

// 在闭包表达式语法一节中作为 sorted(by:) 方法参数的字符串排序闭包可以改写为:

reversedNames = names.sorted(){

    $0 > $1

}

 

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

// 举个栗子 : swift 的 Array 类型有一个 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,1468415]

 

// 现在可以通过传递一个尾随闭包给 numbers 数组的 ,map(_:) 方法来创建对应的字符串版本数组

 

let strings = numbers.map { (number: Int) -> String in

    var number = number

    var output = ""

    repeat{

        output = digitNames[number % 10]! + output

        number /= 10

    }while number > 0

    return output

}

 

print(strings)

 

// map(_:) 为数组中每一个元素调用了一次闭包表达式, 你不需要指定闭包的输入参数 number 的类型, 因为可以通过要映射的数组类型进行推断.

// 在该例子中, 局部变量 number 的值由闭包中的 number 参数获得, 因此可以在闭包函数体内对其进行修改, (闭包或者函数的参数是常量),闭包表达式指定返回类型为 String, 以表明存储映射的新数组的类型为 String

// 闭包表达式在每次被调用的时候创建了一个 output 的字符串并返回, 其使用求余运算符 (%) 计算最后一位数字并利用 digitNames 字典获取所映射的字符串, 这个闭包能够用于创建任意正整数的字符串表示

 

// 注意 : 字典 digitNames 下标后跟着一个叹号 (!), 因为字典下标返回一个可选值 (optional value), 表明该键不存在或者会查找失败, 在上例子中, 由于可以确定 ‘number % 10‘ 总是 digitNames 字典的有效下标, 因此感叹号可以用于强解析 存储在下标的可选类型的返回值中的 String 的值

 

// 在上面的例子中, 通过尾随闭包语法, 优雅的代码

 

 

 

// 值捕获

// 闭包可以在其被定义的上下文中捕获 常量和变量, 即使定义这些 常量和变量的原作用域已经不存在, 闭包仍然可以早闭包函数体内引用和修改这些值

 

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

 

func makeIncrementer(forIncrementer amount: Int) -> () -> Int {

    var runningTotal = 0

    func incrementer() -> Int {

        runningTotal += amount

        return runningTotal

    }

    return incrementer

}

 

// 举个例子,这有一个叫做 makeIncrementer 的函数,其包含了一个叫做 incrementer 的嵌套函数。嵌套函数 incermenter() 从上下文捕获了两个值, runningTotal 和 amount ,捕获这些值之后, makeIncrementer 将 incrementer 作为闭包返回, 每次调用 incrementer 时,将会以 amount amount 作为增量增加 runningTotal 的值。

 

// 注意 : 为了优化,如果一个值不会被闭包改变, 或者在闭包创建后不会改变, swift 可能会改为捕获并保存一份对值的拷贝, swift 也会负责被捕获值的所有内容的管理工作, 包括释放不在需要的变量

 

 

// 下面是一个使用 makeIncrementer 的例子:

let incrementByTen = makeIncrementer(forIncrementer: 10)

 

// 该例子定义了一个叫做 incrementByTen 的常量.该常量指向一个每次调用会将其 runningTotal 变量增加 10 的 incrementer() 函数, 

 

print(incrementByTen())

print(incrementByTen())

print(incrementByTen())

 

// 如果你创建了另一个 incrementer ,他会有属于自己的引用,指向一个全新的,独立的 runningTotal 变量:

 

let incrementBySeven = makeIncrementer(forIncrementer: 7)

print(incrementBySeven())

 

// 再次调用原来的 incrementByTen() 会继续增加自己的 runningTotal 变量, 该变量和 incrementBySeven() 中 捕获的变量没有任何联系

 

print(incrementByTen())

 

 

 

// 闭包是引用类型

// 上面的例子中, incrementBySeven 和 incrementByTen 都是常量, 但是这些常量指向的闭包仍然可以增加其捕获的变量的值, 这是因为函数和 闭包都是引用类型

 

// 无论你将函数或闭包赋值给一个常量还是变量, 你实际上都是讲常量或变量的值设置为对应函数或闭包的引用

 

 

// 逃逸闭包

// 当一个闭包作为参数传到一个函数中, 但是这个闭包在函数返回之后才被执行, 我们称该闭包从函数中逃逸. 当你定义接受闭包作为参数的函数是, 你可以在参数名之前标注 @escaoing, 用来致命这个闭包是允许 ‘逃逸‘ 出这个函数的

 

// 一种能使闭包 ‘逃逸‘ 出函数的方法是, 将这个闭包保存在一个函数的定义的变量中, 举个栗子, 很多启动异步操作的函数接受一个闭包参数作为 completion handler . 这类函数会在异步操作开始之后立刻返回, 但是闭包直到异步结束之后才会被调用, 在这种情况下, 闭包需要 ‘逃逸‘出函数, 因为闭包需要在函数返回之后被调用

 

var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void){

    completionHandlers.append(completionHandler)

}

 

// someFunctionWithEscapingClosure(_:) 函数接受一个闭包作为参数, 该闭包被添加到一个函数外定义的数组中, 如果你不将这个函数标记为 @escaping, 就会返回一个编译错误

 

// 将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。比如说,在下面的代码中,传递到 someFunctionWithEscapingClosure(_:) 中的闭包是一个逃逸闭包,这意味着它需要显式地引用 self。相对的,传递到 someFunctionWithNonescapingClosure(_:) 中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self。

 

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)

// 打印出 "200"

 

completionHandlers.first?()

print(instance.x)

// 打印出 "100"

 

 

 

// 自动闭包

// ‘自动闭包‘ 是一种自动创建的闭包, 用于包装传递给函数作为参数的表达式, 这种闭包不接受任何参数, 当它被调用的时候, 会返回被包装在其中的表达式的值, 这种便利语法让你能够省略闭包的花括号, 用一个普通的表达式代替显示的闭包

 

// 我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message 参数;它的 condition 参数仅会在 debug 模式下被求值,它的 message 参数仅当 condition 参数为 false 时被计算求值。

 

// 自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。

 

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

print(customersInLine.count)

// 打印出 "5"

 

let customerProvider = { customersInLine.remove(at: 0) }

print(customersInLine.count)

// 打印出 "5"

 

print("Now serving \(customerProvider())!")

// Prints "Now serving Chris!"

print(customersInLine.count)

// 打印出 "4"

// 尽管在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。

// 请注意 : customerProvider 的类型不是 String , 而是 () -> String,一个没有参数且返回值为 String 的函数, 将闭包作为参数 传递给函数时, 你能获得同样的延时求值行为

 

func serve(customer customerProvider: () -> String) {

    print("Now serving \(customerProvider())!")

}

 

serve(customer: {customersInLine.remove(at: 0)})

 

以上是关于Swift 学习- 08 -- 闭包的主要内容,如果未能解决你的问题,请参考以下文章

Swift学习笔记-继续学习闭包

swift学习笔记2——函数闭包

swift学习第十五天:闭包

Swift学习笔记:闭包

Swift学习笔记之闭包

《从零开始学Swift》学习笔记(Day 22)——闭包那些事儿!