Swift 闭包

前言

  • 闭包是一个自包含的功能性代码模块。

    • 一段程序代码通常由常量、变量和表达式组成,然后使用一对花括号 “{}” 来表示闭合并包裹着这些代码,由这对花括号包裹着的代码块就是一个闭包。
    • 通俗的解释就是一个 Int 类型里存储着一个整数,一个 String 类型包含着一串字符,同样,闭包是一个包含着函数的类型。
    • Swift 中的闭包与 C 和 OC 中的 Block 以及其他一些编程语言中的 lambdas 比较相似。Block 和闭包的区别只是语法的不同而已,而且闭包的可读性比较强。
  • 在 Swift 中闭包有着非常广泛的应用,有了闭包,你就可以处理很多在一些古老的语言中不能处理的事情,这是因为闭包使用的多样性。

    • 比如你可以将闭包赋值给一个变量。
    • 你也可以将闭包作为一个函数的参数。
    • 你甚至可以将闭包作为一个函数的返回值。
    • 闭包可以捕获和存储其所在上下文中任意常量和变量的引用,并且 Swift 会为你管理在捕获过程中涉及的所有内存操作。
  • 闭包是引用类型的。

    • 无论你将函数/闭包赋值给一个常量还是变量,实际上都是在将常量/变量设置为对应函数/闭包的引用,这也意味着如果你将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包。
    • 在使用闭包时需要注意循环引用。
  • 从本质上来说,函数、方法和闭包是一体的,闭包的功能类似于函数嵌套,但是闭包更加灵活,形式更加简单。在 Swift 语言中有三种闭包形式。

    • 全局函数:是一个有名字但不会捕获任何值的闭包。
    • 嵌套函数:是一个有名字并可以捕获到其封闭函数域内的值的闭包。
    • 匿名闭包:闭包表达式是一个利用轻量级语法所写的,可以捕获其上下文中变量或常量值。

1、闭包的形式

1.1 函数形式闭包

  • 函数形式闭包示例

    1
    2
    3
    4
    5
    6
    7
    let namesArray: Array = ["Jill", "Tim", "Chris"]

    func myConpare(s1: String, s2: String) -> Bool {
    return s1 > s2
    }

    let names = namesArray.sort(myConpare)

1.2 一般形式闭包

  • 一段程序代码通常由常量、变量和表达式组成,然后使用一对花括号 “{}” 来表示闭合并包裹着这些代码,由这对花括号包裹着的代码块就是一个闭包。

    1
    2
    3
    4
    { (参数名1: 参数类型, 参数名2: 参数类型, ...) -> 返回值类型 in

    语句组
    }
    • 闭包写在一对大括号 {} 中,用 in 关键字分割。
    • in 后的语句是闭包的主体。
    • in 之前的参数和返回值类型是 “语句组” 中所使用的参数和返回值格式的一种指示,并不必在语句组中进行逻辑运算与返回。
    • 可以使用常量、变量、inout、可变参数、元组类型作为闭包的参数,但不能在闭包参数中设置默认值,定义返回值和函数返回值的类型相同。
    • 闭包表达式的运算结果是一种函数类型,可以作为表达式、函数参数和函数返回值。
  • 一般形式闭包示例

    1
    2
    3
    4
    5
    6
    let namesArray: Array = ["Jill", "Tim", "Chris"]

    let names = namesArray.sort { (s1: String, s2: String) -> Bool in

    return s1 > s2
    }

1.3 参数类型隐藏形式闭包

  • Swift 中有类型推断的特性,可以根据上下文推断出参数类型,所以我们可以去掉参数类型。

    1
    2
    3
    4
    { (参数名1, 参数名2, ...) -> 返回值类型 in

    语句组
    }
  • 参数类型隐藏形式闭包示例

    1
    2
    3
    4
    5
    6
    let namesArray: Array = ["Jill", "Tim", "Chris"]

    let names = namesArray.sort { (s1, s2) -> Bool in

    return s1 > s2
    }

1.4 返回值类型隐藏形式闭包

  • Swift 中有类型推断的特性,可以根据上下文推断出返回值类型,所以我们可以去掉返回值类型。

    1
    2
    3
    4
    { (参数名1, 参数名2, ...) in

    语句组
    }
  • 返回值类型隐藏形式闭包示例

    1
    2
    3
    4
    5
    6
    let namesArray: Array = ["Jill", "Tim", "Chris"]

    let names = namesArray.sort { (s1, s2) in

    return s1 > s2
    }

1.5 return 隐藏形式闭包

  • 如果在闭包中只有一条语句,比如示例中的 return s1 > s2,那么这种语句只能是返回语句,此时关键字 return 可以省略,省略后的格式变为一种隐式返回。

    1
    2
    3
    4
    { (参数名1, 参数名2, ...) in

    语句组(省略 return
    }
  • return 隐藏形式闭包

    1
    2
    3
    4
    5
    6
    let namesArray: Array = ["Jill", "Tim", "Chris"]

    let names = namesArray.sort { (s1, s2) in

    s1 > s2
    }

1.6 参数名省略形式闭包

  • 闭包的使用非常的灵活,我们可以省略闭包参数列表中的参数的参数类型定义,被省略的参数类型会通过闭包函数的类型进行推断。

  • 同时,我们也可以在闭包函数体中通过使用闭包的参数名简写功能,直接使用 $0$1$2 等名字就可以引用闭包的参数值,$0 指第一个参数,$1 指第二个参数, Swift 能够根据闭包中使用的参数个数推断出参数列表的定义。

  • 如果同时省略了参数名和参数类型,那么 in 关键字也必须被省略,此时闭包表达式完全由闭包函数体构成。

    1
    2
    3
    {
    语句组(使用 $0、$1、$2
    }
  • 参数名省略形式闭包示例

    1
    2
    3
    4
    5
    6
    let namesArray: Array = ["Jill", "Tim", "Chris"]

    let names = namesArray.sort {

    $0 > $1
    }

1.7 trailing 形式闭包

  • 闭包可以做其他函数的参数,而且通常都是函数的最后一个参数。但是如果作为参数的这个闭包表达式非常长,那么很有可能会影响函数调用表达式的可读性,这个时候我们就应该使用 trailing 闭包。

  • trailing 闭包和普通闭包的不同之处在于它是一个书写在函数参数括号之外(之后)的闭包表达式,函数会自动将其作为最后一个参数调用。

  • 当函数有且仅有一个参数,并该参数是闭包时,不但可以将闭包写在 () 外,还可以省略 ()。

    1
    2
    3
    4
    exampleFunction(para1, para2) {

    语句组(使用 $0、$1、$2
    }
  • trailing 形式闭包示例

    1
    2
    3
    4
    5
    6
    let namesArray: Array = ["Jill", "Tim", "Chris"]

    let names = namesArray.sort() {

    $0 > $1
    }

2、闭包捕获

  • 闭包可以在其定义的上下文中捕获常量或变量,即使定义这些常量或变量的原作用域已经不存在,仍然可以在闭包函数体内引用和修改这些常量或变量,这种机制被称为闭包捕获。

  • 比如:嵌套函数就可以捕获其父函数的参数以及定义的常量和变量,全局函数可以捕获其上下文中的常量或变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    func increment(amount: Int) -> (() -> Int) {

    var total = 0

    func incrementAmount() -> Int {

    // total 是外部函数体内的变量,这里是可以捕获到的
    total += amount

    return total
    }

    // 返回的是一个嵌套函数(闭包)
    return incrementAmount
    }

    // 闭包是引用类型,所以 incrementByTen 声明为常量也可以修改 total
    let incrementByTen = increment(10)
    incrementByTen() // return 10,incrementByTen 是一个闭包

    // 这里是没有改变对 increment 的引用,所以会保存之前的值
    incrementByTen() // return 20
    incrementByTen() // return 30

    let incrementByOne = increment(1)
    incrementByOne() // return 1,incrementByOne 是一个闭包
    incrementByOne() // return 2
    incrementByTen() // return 40
    incrementByOne() // return 3

3、闭包捕获列表

  • Swift 中可以显式地指定闭包的捕获列表。

  • 捕获列表需要尾随 in 关键字,并且紧跟着参数列表。

    1
    2
    3
    { [unowned self] (a:A, b:B) -> ReturnType in
    ...
    }
  • 通过标记 selfunowned 或者用 weak 来打破循环引用,这种方法常常用来修改闭包捕获的这个 self 的属性,苹果官方语言指南要求如果闭包和其捕获的对象相互引用,应该使用 unowned,这样能够保证他们会同时被销毁,这大概是为了避免对象被释放后维护 weak 引用空指针的开销。

    1
    2
    3
    { [unowned self] in 
    ...
    }
  • 捕获列表除了可以设置 self,还可以单独声明引用类型成员变量,这避免了引用 thing1thing2 时污染周围代码。

    1
    2
    3
    { [thing1 = self.grabThing(), weak thing2 = self.something] in
    ...
    }

4、闭包循环引用

5、闭包常用关键字

5.1 @escaping 关键字

  • 用关键字 @noescape 修饰的闭包称为非逃逸闭包,而用 @escaping 修饰的闭包称为逃逸闭包。

    • 逃逸闭包,表示此闭包还可以被其他闭包调用,比如我们常用的异步操作。
    • 非逃逸闭包,传入闭包参数的调用限制在调用的函数体内,对性能有一定的提升,同时将使你能在闭包中隐式地引用 self
  • 在 Swift 标准库中很多方法,都用了 @noescape 属性,比如 Array 对应的方法 mapfilterreduce

    1
    2
    3
    4
    5
    func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]

    func filter(@noescape includeElement: (Self.Generator.Element) -> Bool) -> [Self.Generator.Element]

    func reduce<T>(initial: T, @noescape combine: (T, Self.Generator.Element) -> T) -> T
  • 在方法调用时,方法列表中传入的参数会被拷贝(值类型拷贝内部属性,引用类型拷贝指针),在方法体中操作的实际是拷贝的版本,在方法调用结束时,这些拷贝的版本也会被销毁。而闭包类型的参数和其它类型的参数不太相同。

    • 在 Swift 2.2 版本中

      • 闭包类型的参数默认会在方法返回后被返回,也就是说你可以在外部保存那些方法参数中的闭包,通常把这种特性称为 “escape” 延迟调用。
      • 如果你不需要闭包的 “escape” 特性,只想把闭包作为一段从外部写入的灵活代码,则可以在闭包参数名后加上 @noescape 关键字,这样闭包的代码就和其它参数一样,在方法返回前返回。
      • noescape 的参数不能被传递到方法外部。
      • 使用 @noescape 的用法是为方法提供灵活的外部代码,并且 @noescape 的闭包不会产生循环引用,所以调用本类型中的属性时不需要加 self
    • 在 Swift 3.0 中

      • 闭包延迟调用的默认状态发生了反转,noescape 成为了闭包的默认状态,@noescape 修饰符已经被删除了。
      • 现在如果你想使用 “escape” 特性闭包的话,则需要使用 @escaping 显示的声明。
      • 虽然 3.0 中的闭包做了修改,不过在 Swift 2.2 中数组中常用的 map、filter 等方法还都接受 @noescape 闭包参数。
  • 示例

    • 在 Swift 2.2 版本中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      struct ClouseTest {

      var num = 0

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

      // escaping 型的闭包,默认

      mutating func methodWithClouse(addedNum: Int, completeHandler: () -> Void) {

      num += addedNum

      // 把闭包参数加入到缓存数组中,未在方法中执行
      handlerCache.append(completeHandler)
      }

      // noescape 型的闭包,@ noescape 修饰

      func useNum(completeHandler: @noescape (Int) -> Int) -> Int {

      print(num)

      return completeHandler(num)
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      var ct = ClouseTest()
      ct.methodWithClouse(addedNum: 5) {
      ct.num += 10
      }
      print(ct.num) // num 为 5, 闭包中的代码是延迟调用的,在调用前不会执行

      // escaping 型的闭包,默认

      ct.handlerCache.first!() // 调用缓存的闭包参数
      print(ct.num) // num 的值变为 15

      // noescape 型的闭包

      let handleReuslt = ct.useNum { num in
      num + 10
      }
      print(handleReuslt) // 25
    • 在 Swift 3.0 中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      struct ClouseTest {

      var num = 0

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

      // escaping 型的闭包,@escaping 修饰

      mutating func methodWithClouse(addedNum: Int, completeHandler: @escaping () -> Void) {

      num += addedNum

      // 把闭包参数加入到缓存数组中,未在方法中执行
      handlerCache.append(completeHandler)
      }

      // noescape 型的闭包,默认

      func useNum(completeHandler: (Int) -> Int) -> Int {

      print(num)

      return completeHandler(num)
      }
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      var ct = ClouseTest()
      ct.methodWithClouse(addedNum: 5) {
      ct.num += 10
      }
      print(ct.num) // num 为 5, 闭包中的代码是延迟调用的,在调用前不会执行

      // escaping 型的闭包

      ct.handlerCache.first!() // 调用缓存的闭包参数
      print(ct.num) // num 的值变为 15

      // noescape 型的闭包,默认

      let handleReuslt = ct.useNum { num in
      num + 10
      }
      print(handleReuslt) // 25

5.2 @autoclosure 关键字

  • 用关键字 @autoclosure 修饰的闭包称为自动闭包。

    • 自动闭包,顾名思义是一种自动创建的闭包,用于包装函数参数的表达式,可以说是一种简便语法.
    • 自动闭包不接受任何参数,被调用时会返回被包装在其中的表达式的值。
    • 自动闭包的好处是让你能够延迟求值,因为代码段不会被执行直到你调用这个闭包,这样你就可以控制代码什么时候执行。
    • 含有 autoclosure 特性的声明同时也具有 noescape 的特性,即默认是非逃逸闭包,除非传递可选参数 escaping。如果传递了该参数,那么将可以在闭包之外进行操作闭包,形式为 @autoclosure(escaping)
  • 下面一起来看一个简单例子,比如我们有一个方法接受一个闭包,当闭包执行的结果为 true 的时候进行打印。

    1
    2
    3
    4
    5
    6
    func printIfTrue(predicate: ()-> Bool) {

    if predicate() {
    print("the result is true")
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 直接调用方法
    printIfTrue { () -> Bool in
    return 2 > 1
    }

    // 闭包在圆括号内
    printIfTrue(predicate: {
    return 2 > 1
    })

    // 使用尾部闭包方式,闭包体在圆括号之外
    printIfTrue() {
    return 2 > 1
    }

    // 省略 return
    printIfTrue(predicate: {
    2 > 1
    })

    // 使用尾随闭包
    printIfTrue {
    2 > 1
    }
  • 但是不管哪种方式,表达上不太清晰,看起来不舒服,于是 @autoclosure 就登场了,我们可以在参数名前面加上 @autoclosure 关键字,这样我们就得到了一个写法简单,表意清楚的式子,被 @autoclosure 标注的闭包不再需要写在 “{ }” 中。

    1
    2
    3
    4
    5
    6
    func printIfTrue(predicate: @autoclosure () -> Bool) {

    if predicate() {
    print("the result is true")
    }
    }
    1
    2
    // 直接进行调用,Swift 将会把 2 > 1 这个表达式自动转换为 () -> Bool。
    printIfTrue(predicate: 2 > 1)
  • 如果有多个闭包,那么就有优势了,而 @autoclosure 是可以修饰任何位置的参数。

    1
    2
    3
    4
    5
    6
    7
    8
    func printInformation(predicate1: @autoclosure () -> Bool, predicate2: @autoclosure () -> Bool) {

    if predicate1() && predicate2() {
    print("the result is true")
    } else {
    print("the result is false")
    }
    }
    1
    printInformation( predicate1: 3 > 2, predicate2: 4 > 1)

6、闭包风格

  • 在 Swift 3.0 之前定义闭包可以用多种格式来表示。比如

    1
    2
    3
    4
    5
    6
    // 定义方式 1
    let plus: (Int, Int) -> Int = { x in
    return x.0 + x.1
    }

    print(plus(1, 2))
    1
    2
    3
    4
    5
    6
    // 定义方式 2
    let plus: (Int, Int) -> Int = {x, y in
    return x + y
    }

    print(plus(1, 2))
    • 这两种格式的运算结果相同
    • 第一种闭包体中只声明了一个参数,这个参数需要与上下文中的格式相对应,所以 x 代表了一个元组 (Int, Int)
    • 第二种闭包体中传入了两个参数,所以分别对应了上下文中的两个 Int` 类型。
  • 实际上这种语法示存在歧义的,Swift 3.0 修复了这种歧义,使用更加严格的方式来响应上下文,参数的表达方式只有一种,即参数列表最外层括号内部的类型。

    1
    2
    3
    4
    5
    6
    // 定义方式 1
    let plus: ((Int, Int)) -> Int = { x in
    return x.0 + x.1
    }

    print(plus(1, 2))
    1
    2
    3
    4
    5
    6
    // 定义方式 2
    let plus: (Int, Int) -> Int = {x, y in
    return x + y
    }

    print(plus(1, 2))
文章目录
  1. 1. 前言
  2. 2. 1、闭包的形式
    1. 2.1. 1.1 函数形式闭包
    2. 2.2. 1.2 一般形式闭包
    3. 2.3. 1.3 参数类型隐藏形式闭包
    4. 2.4. 1.4 返回值类型隐藏形式闭包
    5. 2.5. 1.5 return 隐藏形式闭包
    6. 2.6. 1.6 参数名省略形式闭包
    7. 2.7. 1.7 trailing 形式闭包
  3. 3. 2、闭包捕获
  4. 4. 3、闭包捕获列表
  5. 5. 4、闭包循环引用
  6. 6. 5、闭包常用关键字
    1. 6.1. 5.1 @escaping 关键字
    2. 6.2. 5.2 @autoclosure 关键字
  7. 7. 6、闭包风格
隐藏目录