Swift 闭包

前言

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

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

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

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

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

1、闭包的形式

1.1 函数形式闭包

  • 函数形式闭包示例

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

    let namesArray: Array = ["Jill", "Tim", "Chris"]
        
    let names = namesArray.sort { (s1: String, s2: String) -> Bool in
        
        return s1 > s2
    }
    

1.3 参数类型隐藏形式闭包

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

    { (参数名1, 参数名2, ...) -> 返回值类型 in
    
        语句组
    }
    
  • 参数类型隐藏形式闭包示例

    let namesArray: Array = ["Jill", "Tim", "Chris"]
        
    let names = namesArray.sort { (s1, s2) -> Bool in
            
        return s1 > s2
    }
    

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

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

    { (参数名1, 参数名2, ...) in
    
        语句组
    }
    
  • 返回值类型隐藏形式闭包示例

    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, ...) in
    
        语句组(省略 return)
    }
    
  • return 隐藏形式闭包

    let namesArray: Array = ["Jill", "Tim", "Chris"]
        
    let names = namesArray.sort { (s1, s2) in
            
        s1 > s2
    }
    

1.6 参数名省略形式闭包

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

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

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

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

    let namesArray: Array = ["Jill", "Tim", "Chris"]
        
    let names = namesArray.sort {
            
        $0 > $1
    }
    

1.7 trailing 形式闭包

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

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

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

    exampleFunction(para1, para2) {
        
        语句组(使用 $0、$1、$2)
    }
    
  • trailing 形式闭包示例

    let namesArray: Array = ["Jill", "Tim", "Chris"]
        
    let names = namesArray.sort() {
            
        $0 > $1
    }
    

2、闭包捕获

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

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

    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 关键字,并且紧跟着参数列表。

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

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

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

4、闭包循环引用

5、闭包常用关键字

5.1 @escaping 关键字

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

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

    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 版本中

      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)
          }
      }
      
      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 中

      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)
          }
      }
      
      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 的时候进行打印。

    func printIfTrue(predicate: ()-> Bool) {
        
        if predicate() {
            print("the result is true")
        }
    }
    
    // 直接调用方法
    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 标注的闭包不再需要写在 "{ }" 中。

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

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

6、闭包风格

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

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

    // 定义方式 1
    let plus: ((Int, Int)) -> Int = { x in
        return x.0 + x.1
    }
    
    print(plus(1, 2))
    
    // 定义方式 2
    let plus: (Int, Int) -> Int = {x, y in
        return x + y
    }
    
    print(plus(1, 2))
    
原文地址:https://www.cnblogs.com/QianChia/p/8625992.html