浅学swift中的闭包

来源:互联网 发布:网络歌手排行榜2014 编辑:程序博客网 时间:2024/05/17 05:53

序言

闭包应用非常广泛, 它可以捕捉上下文的常量或变量,也可以使用在函数的调用之中。老生常谈, 闭包就是匿名函数代码块, 在编程中我们经常和闭包打交道, Swift语言中的闭包使得编程更加简洁、安全、实用。

本章目录

  • 闭包表达式语法
  • 从上下文推断类型
  • 速记参数名
  • 尾随闭包
  • 捕捉值
  • 闭包是引用类型
  • 逃逸闭包
  • 自动闭包

闭包表达式语法

闭包表达式语法的一般形式:

{ (parameters)-> return type in     statements}

上面的闭包的一般形式在官方文档中有介绍。官方文档说, parameters可以是输入输出参数, 但是parameters不能有一个默认值

另外, 我们注意到在闭包表达式语法中, 有一个“in”关键字, 这个“in”的含义是闭包参数和返回类型已经完成,闭包体即将开始

  • 举一个闭包的例子,在Swift中, 有一个对Array排序的方法:sorted(by:)

    let names = ["Chris", "Aeex", "Awa", "Barry", "Daniella"]let sortedArrayResult = names.sorted { (str1: String, str2: String) -> Bool in  return str2 > str1}print(sortedArrayResult)
  • 当闭包体很短的时候, 闭包可以全部写在同一行 :

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

从上下文推断类型

  • 当系统能够明确所写闭包的参数类型和返回类型时,相应参数的圆括号和返回箭头可以省略:

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

上面这样的写法使得闭包更加简洁化,Swift中的自动推断类型使得语法更加简洁和安全。尽管如此, 在我们写的过程中, 除非是很明确的系统自带函数和自定义函数, 否则建议写完全一点, 这样可以增强程序的可读性。

  • 单行表达式闭包的明确返回

单行表达式闭包能够明确返回单行表达式结果通过省略声明中的return关键字:

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

速记参数名

  • Swift自动为内联闭包提供了速记参数名来推断闭包参数值,如:0,1, $2 。如果在闭包表达式中使用了速记参数名, 既可以从闭包的定义中省略闭包参数, 速记参数名的编号和类型会从所在的函数类型被推断出来。因为闭包表达式完全由它的闭包体组成,所以“in”关键字也能够被省略。

    // 这里$0、$1表示闭包的第一个String参数和第二个String参数names.sorted(by: { $0 > $1 })
  • 操作符方法

由于Swift中的字符串类型定义了明确的操作符“>”来比较两个字符串并返回一个Bool值,因此这里恰恰和sorted(by:)一致, 这种情况下,Swift会自动推断你想要执行这个比较的方法并返回一个Bool值,那么我们可以有更简洁的写法:

  // 个人并不建议这样写,这会让读你的代码的人头疼  names.sorted(by: > )

尾随闭包

  • 如果你需要将一个闭包表达式传递给一个函数来作为这个函数的最后参数并且这个函数很长, 那么把它写成一个尾随闭包可能会很有用。 一个尾随闭包被写在这个函数调用的圆括号后面, 即使它只是这个函数的一个参数。当你使用尾随闭包语法的时候,正如部分函数的调用一样,你不用给这个闭包写参数标签。

先看官方给的简单例子:

// closure作为函数的唯一参数func someFunctionThatTakesAClosure(closure: () -> Void) {    print("this is Function Body!")}// 方式一: 没有使用尾随闭包调用函数someFunctionThatTakesAClosure(closure: {    print("this is a Closure Body!")})运行结果:this is Function Body!, 需要注意的是这里并没有打印“this is a Closure Body!”// 方式二: 使用尾随闭包调用函数someFunctionThatTakesAClosure() {    print("this is trailing closure's body!")}运行结果:this is Function Body!, 需要注意的是这里并没有打印"this is trailing closure's body!"

然而,从官方例子,我并没有看出尾随闭包的真正作用! 在方式一中并没有打印出闭包体的结果, 在方式二中并没有打印出尾随闭包体的结果, 那么, 尾随闭包到底有何作用呢? 怎么使用呢?

接着看下面

Xcode中, 系统自带的string-sorting闭包可以写在sorted(by:)方法的圆括号后面作为一个尾随闭包:

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

再看一个真正调用闭包的例子:

func trailingClosureTestFunction(name: String, age: Int, trailingClosure: () -> Void) {    let personDescription = "\(name)" + "," + "" + "\(age)!"    print(personDescription)    // 回调尾随闭包    trailingClosure()}// 调用函数trailingClosureTestFunction(name: "浪客", age: 18) {      print("我是一个尾随闭包!")}运行结果:    浪客,18!    我是一个尾随闭包!
  • 如果一个闭包表达式作为函数或方法的唯一参数, 并且提供闭包表达式作为一个尾随闭包,当调用函数的时候, 不用写函数或方法名后面的圆括号(我在Xcode 8.2.1上测试, 系统就是这样默认的)。

比如:

func trailingClosureTestFunction(trailingClosure: () -> Void) {    print("我是函数体")    trailingClosure()}// 这里省略了圆括号“()”trailingClosureTestFunction {     print("我是闭包体")}运行结果:    我是函数体    我是闭包体
  • 当闭包很长的时候,不可能将它写成内联闭包在同一行,尾随闭包就变得非常有用。 例如, Swift 中的 Array 有一个 map(_:)方法,就是用闭包表达式作为map函数的唯一参数。 对于 Array 中的每一项, 闭包都会被调用一次, 并且对每一项都会返回可选的 mapped value, mapping的本质和返回值的类型由闭包指定。

例如官方例子:

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]// map函数后面跟了一个尾随闭包// 输入参数number之所以没有指定类型, 是因为Swift能够根据数据 numbers 推断出来// 定义了一个返回类型String来表明这个类型将存储在一个被映射的输出参数let strings = numbers.map { (number) -> String in    // 这里定义一个变量类型来接收number, 是因为函数参数和闭包参数总是常量    var number = number    var output = ""    repeat {         output = digitNames[number % 10]! + output         number /= 10    } while number > 0     return output}print(strings)

remainder operator: 取余运算符“%”

捕捉值

能够捕捉值的最简单的闭包形式就是嵌套函数, 一个被嵌套的函数能够捕捉任何外部函数的参数值。

func makeIncrementer(forIncrement amout: Int) -> () -> Int {    var runningTotal = 0    func incrementer() -> Int {        runningTotal += amout        return runningTotal    }    return incrementer}let incrementByTen = makeIncrementer(forIncrement: 10)// 打印结果:10let a1 = incrementByTen()print(a1)// 打印结果:20let a2 = incrementByTen()print(a2)// 打印结果:30let a3 = incrementByTen()print(a3)// 打印结果:5let incrementByFive = makeIncrementer(forIncrement: 5)let b1 = incrementByFive()print(b1)

在上面这个例子中, 被嵌套函数 incrementer通过捕捉上下文环境而获取到函数 makeIncrementer的参数 amout和变量 runningTotal的值。

闭包是引用类型

在上面的例子中,incrementByTen 和 incrementByFive都是常量, 但是这些常量所指向的这些闭包依然能够增加他们所捕捉到的“runningTotal”变量, 这是因为函数和闭包都是引用类型。

如果你将一个闭包赋值给两个不同的常量或变量, 那么那些常量或变量都将指向同一个闭包。

// 打印结果: 40let alsoIncrementByTen = incrementByTenlet alseValue = alsoIncrementByTen()print(alseValue)

逃逸闭包

  • 当闭包被传递作为函数的一个参数但是在函数返回之后被调用时,闭包就能够逃逸出这个函数。 当你声明一个将闭包当做它的参数之一的函数时, 你就能够在这个参数类型前写上@escaping来表明这个闭包被允许逃逸。

  • 闭包能够逃逸的一种方式就是通过存储在一个被定义在这个函数外面的变量中。例如, 很多以一个异步操作开始的函数都会把闭包参数作为一个完成回调操作,在它开始这个操作后, 函数就会返回,但是这个闭包直到操作完成后才被调用。

    var completionHandlers: [() -> Void] = []func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {    completionHandlers.append(completionHandler)}
  • 这个someFunctionWidthEscapingClosure(_:)函数把一个闭包当做它的参数并且把闭包添加到一个声明在这个函数外面的数组中。 若没有将这个函数参数标记为 @escaping, 则编译器就会报错!!!

  • 将闭包标记为 @escaping意味着你必须在闭包内明确指定self。例如, 在下面的代码中,被传递给函数someFunctionWith(:)的闭包是一个逃逸闭包, 意味着它需要明确指定self。相比之下, 被传递给函数someFunctionWithNonescapingClosure( :)的闭包是一个非逃逸闭包, 意味着它能够隐式指定self.

     class SomeClass {     var completionHandlers: [() -> Void] = []     var x = 10     func doSomething() {        //这里必须明确指定self, 因为它是一个逃逸闭包, 否则报错        someFunctionWithEscapingClosure { self.x = 100 }        // 这里不需要明确指定self, 因为它是一个非逃逸闭包, 会隐式推断self        someFunctionWithNonescapingClosure { x = 200 }     }     func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {          completionHandlers.append(completionHandler)     }     func someFunctionWithNonescapingClosure(closure: () -> Void) {          closure()     }}

自动闭包

若闭包被自动创建来包裹一个表达式, 这个表达式被传递作为函数的一个参数, 那么这个闭包就是一个自动闭包。 自动闭包没有任何参数, 当被调用的时候, 会返回包裹在闭包里面的表达式的值。这种语法便利可以通过写普通的表达式而不是明确的闭包, 就让你省略函数参数的大括号。

调用采用自动闭包的函数是相当普遍的, 但是实现这类函数并不普遍, 也就是说我们一般都是用带自动闭包这样的函数, 但是我们很少自己去实现这类函数。

自动闭包可以延迟求值, 因为自动闭包里面 的代码直到你调用闭包时才会运行。 延迟求值对那些具有负面影响或者计算起来代价昂贵的代码非常有用,因为它可以让你控制何时代码需要求值。
看一个官方例子:

    var customerInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]    // 打印结果:5    print(customerInLine.count)    let customerProvider = { customerInLine.remove(at: 0) }    // 打印结果:5    print(customerInLine.count)    // 这里调用了自动闭包 customerProvider    print("Now serving \(customerProvider())")    // 调用自动闭包之后,打印结果:4    print(customerInLine.count)

在上面的例子中, 即使数组 customerInLine 的第一个元素通过闭包内部的代码被移除, 但是数组元素直到闭包被真正调用时才被移除。 如果这个闭包没有被调用, 则闭包内的表达式就绝不会计算求值, 也就是说数组元素绝不会被移除!


当你将闭包传递作为函数的一个参数时, 你也能得到同样的延迟求值效果。
例如

    var customerInLine = ["Alex", "Ewa", "Barry", "Daniella"]    func serve(customer customerProvider: () -> String) {        print("Now serving \(customerProvider())!")    }    // 打印结果: Now serving Alex!    serve(customer: { customerInLine.remove(at: 0) })

上面的 serve(customer:)函数采用了明确的闭包来返回顾客的名字。

下面的 serve(customer:)函数执行同样的操作, 但是它通过标记它的参数类型为 @autoclosure属性 来采用自动闭包, 而不是采用一个明确的闭包。现在你也能够调用这个函数,好像它采用的是一个 String 参数而不是一个闭包。这个参数自动转换成一个闭包, 因为 customerProvider 参数的类型被标记为 @autoclosure属性。
例如:

    var customerInLine = ["Alex", "Ewa", "Barry", "Daniella"]    func serve(customer customerProvider: @autoclosure () -> String) {        print("Now serving \(customerProvider())!")    }    // 调用函数结果:Now serving Alex!    // serve(customer: String)    serve(customer: customerInLine.remove(at: 0))

注意:

过度使用自动闭包会让你的代码很难理解, 上下文和函数名应该要让求值延迟清晰可见。


如果你想要一个自动闭包可以逃逸, 可以使用 autoclosure@escaping属性。
例如:
var customerInLine = [“Barry”, “Daniella”]

    var customerProviders: [() -> String] = []    func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {        customerProviders.append(customerProvider)    }    collectCustomerProviders(customerInLine.remove(at: 0))    collectCustomerProviders(customerInLine.remove(at: 0))    // 打印结果:Collected 2 closures    print("Collected \(customerProviders.count) closures.")    for customerProvider in customerProviders {        // 打印结果:        /**           Now serving Barry!           Now serving Daniella!         */        print("Now serving \(customerProvider())!")    }

在上面的代码中, collectCustomerProviders(_:)函数将 customerProvider闭包添加到customerProviders数组中, 而不是调用传递给它作为它的 customerProvider参数 的闭包。这个数组被声明在函数范围外面, 意味着这个数组中的闭包在函数返回之后才会被执行。结果, customerProvider参数的值必须被允许逃逸出这个函数的范围!





本文来自Swift3.0.1官方英文版, 作者自译, 欢迎学习, 禁止用于商业用途, 侵权必究! 由于个人能力有限, 欢迎指出其中不足之处!

0 0
原创粉丝点击