Swift 闭包-牛人的玩物

本文深入讲解Swift中的闭包概念,包括闭包的定义、类型、语法优化及应用场景等。通过实例解析闭包如何捕获变量,帮助理解闭包在实际编程中的作用。

闭包的概念 

  闭包就是一个自包含的功能块,你可以按照自己的想法在程序的任何地方使用这个功能块,还可以把功能块传来传去,哪需要传哪,这就是闭包.

  自包含的意思是:不依赖与其他函数的功能块,自己就能够完成一个完整的功能,能够以独立的方式供其他程序使用.
  除了自包含功能块以外,闭包还有一个特异功能,它可以捕获和存储其上下文中任意的变量和常量的引用.想想嵌套函数,嵌套函数被包含在函数里面,它可以使用外面函数定义的任何变量和常量,这就是闭包的功能.

  根据我们对闭包的认识,一起来看看我们代码里面哪些是闭包?

  全局函数是不是闭包?
  先想想函数是不是自包含的功能块?那肯定是啊,函数当然是自包含的功能块,那它当然是闭包.再想想闭包可以捕获上下文中任意的变量和常量.全局函数可没有这个功能,因为全局函数定义在全局,没有上下文这个概念,那怎么去捕获变量和常量呢.所以总结起来全局函数是闭包,但是没有闭包的特异功能.

  嵌套函数是不是闭包?

  函数都是自包含的功能块,嵌套函数也是函数,当然也是自包含的功能块,所以嵌套函数是闭包.而且嵌套函数是嵌在函数里面的函数,它有上下文,就是包含它的那个函数,它可以使用包含它的外围函数里面定义的容易常量和变量,所以嵌套函数是闭包并且拥有闭包的特有功能.

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

Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下
1:利用上下文推断参数和返回值类型
2:隐式返回单表达式闭包,即单表达式闭包可以省略return关键字
3:参数名称缩写
4:尾随(Trailing)闭包语法

闭包表达式

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

//下面的闭包表达式示例使用sort(_:)方法对一个String类型的数组进行字母逆序排序,以下是初始数组值:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

/*
sort(_:)方法需要传入两个参数:

1:已知类型的数组
2:闭包函数,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入
  的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回true,
  反之返回false。
*/

//该例子对一个String类型的数组进行排序,因此排序闭包函数类型需为(String, String) -> Bool
func backwards(s1: String, s2: String) -> Bool {
    return s1 > s2
}
var reversed = names.sort(backwards)
//reversed为["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果第一个字符串 (s1) 大于第二个字符串 (s2),backwards函数返回true,表示在新的数组中s1应该出现在s2前。 对于字符串中的字符来说,“大于” 表示 “按照字母顺序较晚出现”。 这意味着字母"B"大于字母"A",字符串"Tom"大于字符串"Tim"。 其将进行字母逆序排序,"Barry"将会排在"Alex"之前。

然而,这是一个相当冗长的方式,本质上只是写了一个单表达式函数 (a > b)。 在下面的例子中,利用闭合表达式语法可以更好的构造一个内联排序闭包。

闭包表达式语法形式如下:

{ (参数列表) -> 返回值 in

}
/*
  (参数列表) -> 返回值 是函数的类型并且后面跟着in,在in后面写函数体.
   闭包表达式语法可以使用常量、变量和inout类型作为参数,不提供默认值。 也可以在参数列表的最后使用可变参数。 
   元组也可以作为参数和返回值。
*/

//面的例子展示了之前backwards函数对应的闭包表达式版本的代码:
reversed = names.sort({ (s1: String, s2: String) -> Bool in
    return s1 > s2
})

//["Ewa", "Daniella", "Chris", "Barry", "Alex"]

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

闭包的函数体部分由关键字in引入。 该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
因为这个闭包的函数体部分如此短以至于可以将其改写成一行代码:
*/

reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )

//这说明sort(_:)方法的整体调用保持不变,一对圆括号仍然包裹住了函数中整个参数集合。
//而其中一个参数现在变成了内联闭包(相比于backwards版本的代码).
 

根据上下文推断类型

因为排序闭包函数是作为sort(_:)方法的参数进行传入的,Swift可以推断其参数和返回值的类型。 sorted期望第二个参数是类型为(String, String) -> Bool的函数,因此实际上String,String和Bool类型并不需要作为闭包表达式定义中的一部分。 因为所有的类型都可以被正确推断,返回箭头 (->) 和围绕在参数周围的括号也可以被省略:

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

//实际上任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数时,都可以推断出闭包的参数和返回值类型,
//这意味着几乎不需要利用完整格式构造任何内联闭包。

然而仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则可以采用完整格式的闭包。而在sort(_:)方法这个例子里,闭包的目的就是排序,能够推测除这个闭包是用于字符串处理的,因为这个闭包是为了处理字符串数组的排序。

单表达式闭包隐式返回

//单行表达式闭包可以通过隐藏return关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:

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

在这个例子中,sort(_:)方法的第二个参数函数类型明确了闭包必须返回一个Bool类型值。因为闭包函数体只包含了一个单一表达式 (s1 > s2),该表达式返回Bool类型值,因此这里没有歧义,return关键字可以省略。

参数名称缩写

Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数。如果在闭包表达式中使用参数名称缩写,您可以在闭包参数列表中省略对其的定义,并且对应参数名称缩写的类型会通过函数类型进行推断。in关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:

reversed = names.sort( { $0 > $1 } )
//在这个例子中,$0和$1表示闭包中第一个和第二个String类型的参数。

运算符函数

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

reversed = names.sort(>)

尾闭包

//如果需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。 尾随闭包是一个书写在函数括号之后的闭包表达式,
//函数支持将其作为最后一个参数调用。

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

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

// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
    // 闭包主体部分
}
//注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。
//在上例中作为sorted函数参数的字符串排序闭包可以改写为:

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

捕获值

闭包可以在其定义的上下文中捕获常量或变量。 即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。 Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。 嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

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

    var runningTotal = 0
    func incrementor() -> Int{
        runningTotal += amout
        return runningTotal
    }

   return incrementor
}

/*
  makeIncrementor是外围函数,incrementor是嵌套函数,从上面例子可以看到,incrementor可以使用外围函数makeIncrementor的参数amout
  和在外围函数makeIncrementor中定义的变量runningTotal.最后makeIncrementor的返回值是一个函数.
*/

let incrementByTen = makeIncrementor(forIncrement: 10)
incrementByTen()    //返回值10
incrementByTen()    //返回值20
incrementByTen()    //返回值30

/*
  看一下嵌套函数捕获参数和捕获外围函数里面定义的变量或者常量的区别:
  incrementor嵌套函数捕获 amout:
  在incrementor里面实际是捕获并且存储了 amout 参数的副本,这个副本随着incrementor一起被存储,随着incrementor的存在而存在,消亡而消亡.

  incrementor嵌套函数捕获上下文变量runningTotal:
  incrementor实际是捕获runningTotal的引用,而不是仅仅复制该变量的初始值,也就是说在incrementor里面对runningTotal的改变
  也会影响到外围函数makeIncrementor里面变量runningTotal.这个引用的捕获保证了当makeIncrementor结束时并不会消失,
  也保证了当下一次执行incrementor函数时,runningTotal可以继续增加.
*/

挖挖闭包的老底
      闭包和函数一样,都是引用类型,引用类型的意思就是当把函数或者闭包赋值给某个常量或者变量的时候,不是把它的值的一份副本传给这个变量或者常量,而是让这个变量或者常量成为指向函数或者闭包的一个引用.

      如果将闭包赋值给了两个不同的常量或者变量.两个值都会指向同一个闭包,对其中任何一个值进行操作都会直接影响另外一个,因为它们相当于指向同一块内存地址.

扩展部分

闭包一般是用来作为函数的参数。不过某些情况下,使用本地闭包也是十分方便的。假设有一个ViewController里面包含了两种 GUI 模式:
enum GUIMode {
    case Mode1
    case Mode2
}
对于每一种 GUI 模式,我们都需要对三个 label 设置某些属性:
var guiMode: GUIMode = .Mode1 {
didSet {
    switchguiMode {
    case .Mode1:
        label1.text = "1"
        label1.textColor = UIColor.redColor()
        label1.font = UIFont(name: "HelveticaNeue", size: 10)
        
        label2.text = "2"
        label2.textColor = UIColor.blueColor()
        label2.font = UIFont(name: "HelveticaNeue", size: 12)
        
        label3.text = "3"
        label3.textColor = UIColor.yellowColor()
        label3.font = UIFont(name: "HelveticaNeue", size: 11)
        
    case .Mode2:
        label1.text = "4"
        label1.textColor = UIColor.yellowColor()
        label1.font = UIFont(name: "HelveticaNeue", size: 11)
        
        label2.text = "5"
        label2.textColor = UIColor.blueColor()
        label2.font = UIFont(name: "HelveticaNeue", size: 9)
        
        label3.text = "6"
        label3.textColor = UIColor.brownColor()
        label3.font = UIFont(name: "HelveticaNeue", size: 10)
    }
}
}
这里有很多重复的代码。你可以创建一个函数,在函数里对一个 label 的属性进行设置,但是这个函数我们基本不会在别的地方再次使用。因此,在这种情况下使用闭包就是一个相当不错的解决方案:
var guiMode: GUIMode = .Mode1 {
didSet {
    let styleLabel: (_ label:UILabel,_ text:String,_ color:UIColor,_ size:CGFloat) -> () = { (label,text,color,size) in
        label.text = text
        label.textColor = color
        label.font = UIFont(name: "HelveticaNeue", size:size)
    }
    
    switchguiMode {
    case .Mode1:
        styleLabel(label: label1, text: "1", color: UIColor.redColor(), size:10)
        styleLabel(label: label2, text: "2", color: UIColor.blueColor(), size:12)
        styleLabel(label: label3, text: "3", color: UIColor.yellowColor(), size:11)
    case .Mode2:
        styleLabel(label: label1, text: "4", color: UIColor.yellowColor(), size:11)
        styleLabel(label: label2, text: "5", color: UIColor.blackColor(), size:9)
        styleLabel(label: label3, text: "6", color: UIColor.brownColor(), size:10)
    }
}
}
这样一来,代码就少了很多,看起来也更加简洁了。


在 Swift 中,闭包捕获他们所引用的变量:虽然这些变量在闭包之外声明,但只要在闭包内使用都会默认被闭包保留引用(retain),这是为了确保闭包执行时,这些变量还活着(译者注:没有被提前释放)。
定义一个简单的Pokemon(口袋妖怪)类:
class Pokemon: CustomDebugStringConvertible {
    let name: String
    init(name: String) {
        self.name = name
    }
    var debugDescription: String { return "<Pokemon \(name)>" }
    deinit { print("\(self) escaped!") }
}
接下来声明一个简单的函数,它接受一个闭包作为参数,然后在一段时间后执行这个闭包(使用 GCD)

func delay(seconds: Int, closure: @escaping ()->()) {
    let time = DispatchTime.now() + .seconds(seconds)
    DispatchQueue.main.asyncAfter(deadline: time) {
        print("delay")
        closure()
    }
}

直接看一个例子,更多详情内容,原文这里

func demo() {
    var pokemon = Pokemon(name: "Mew")
    print("Initial pokemon is \(pokemon)")
    
    delay(seconds: 1) { [capturedPokemon = pokemon] in
        print("closure 1 — pokemon captured at creation time: \(capturedPokemon)")
        print("closure 1 — variable evaluated at execution time: \(pokemon)")
        pokemon = Pokemon(name: "Pikachu")
        print("closure 1 - pokemon has been now set to \(pokemon)")
    }
    
    pokemon = Pokemon(name: "Mewtwo")
    print("pokemon changed to \(pokemon)")
    
    delay(seconds: 2) { [capturedPokemon = pokemon] in
        print("closure 2 — pokemon captured at creation time: \(capturedPokemon)")
        print("closure 2 — variable evaluated at execution time: \(pokemon)")
        pokemon = Pokemon(name: "Charizard")
        print("closure 2 - value has been now set to \(pokemon)")
    }
}
执行顺序如下:

Initial pokemon is <Pokemon Mew>
pokemon changed to <Pokemon Mewtwo>
delay
closure 1 — pokemon captured at creation time: <Pokemon Mew>
closure 1 — variable evaluated at execution time: <Pokemon Mewtwo>
closure 1 - pokemon has been now set to <Pokemon Pikachu>
<Pokemon Mew> escaped!
delay
closure 2 — pokemon captured at creation time: <Pokemon Mewtwo>
closure 2 — variable evaluated at execution time: <Pokemon Pikachu>
<Pokemon Pikachu> escaped!
closure 2 - value has been now set to <Pokemon Charizard>
<Pokemon Mewtwo> escaped!
<Pokemon Charizard> escaped!
逐步解释一下:

1
:把pokemon一开始设置为 Mew
2
:创建闭包1并且它的新本地变量 capturedPokemon 捕获了 pokemon的值(此刻 pokemon的值为 New,并且闭包也捕获了pokemon变量的引用,capturedPokemon pokemeon都会在闭包代码中使用)
3
:然后将 pokemon修改为 Mewtwo
4
:创建闭包 2,它的新本地变量 capturedPokemon捕获了 pokemon的值(此刻 pokemon的值为 Mewtwo,并且闭包也捕获了pokemon变量的引用,capturedPokemon pokemeon都会在闭包代码中使用)
5
:此刻,demo()函数已经执行完毕了
6
:一秒钟后,GCD执行第一个闭包
  1
:它的打印结果为 Mew,即第二步创建闭包时捕获在 capturedPokemon变量中的值
  2
:它也会根据所捕获 pokemon的引用,找出变量的当前值,它目前为 Mewtwo(至少是在第五步离开demo()函数前的值)
  3
:然后将变量 pokemon的值改为 Pikachu(再次强调,闭包捕获的是变量 pokemon的引用,所以demo()函数中的 pokemon变量与闭包中进行赋值操作的 pokemon变量具有同的引用)
  4
:当闭包执行完毕被 GCD释放后,没有对象在强引用 Mew了,因此会释放掉。但是第二个闭包的 capturedPokemon依然捕获着 Mewtwo,并且第二个闭包也捕获了 pokemon变量的引用,此刻它的值为 Pikachu
7
:又过了一秒钟,GCD开始执行第二个闭包
  1
:它的打印结果为 Mewtwo,即步骤四第二个闭包创建时捕获在 capturedPokemon变量中的值
  2
:它也会根据所捕获 pokemon的引用,找出变量的当前值,它目前为 Pikachu(因为在第一个闭包中已经修改了它)
  3
:最后,将 pokemon变量设置为 Charizard,由于 Pikachu小精灵只被 pokemon变量强引用,而此时 pokemon已不再指向它了,所以也会立即被释放。
  4
:当闭包执行完毕被 GCD释放后,本地变量 capturedPokemon脱离了作用域,所以 Mewtwo会被释放,同时指向 pokemon变量的强引用也会消失,小精灵 Charizard也会被释放

总结

是不是感觉有点烧脑?这很正常,闭包捕获语义有时候会比较复杂,尤其类似最后那个例子。我们要记住下面几个关键点:

1
:在 Swift闭包中使用的所有外部变量,闭包会自动捕获这些变量的引用
2
:在闭包执行时,会根据这些变量引用得到所对应的具体值
3
:因为我们捕获的是变量的引用(而不是变量自身的值),所以你可以在闭包内部修改变量的值(当然变量要声明为 var,而不能是 let
4:你可以在闭包创建时获取变量中的值,然后把它存储到本地常量中,而不是捕获变量的引用。我们可以使用带中括号的捕获列表来实现。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值