Swift 中的闭包(Closures)

作者: 诸葛俊伟 | 来源:发表于2016-05-20 05:57 被阅读319次

    前言

    今天我想聊一聊 Swift 中的闭包(Closures)。闭包在 Swift 中的功能非常强大。它可以简化我们的代码,同时使得程序猿更容易写出更有逻辑性的代码。现在好像是越来越流行"函数式编程"了,闭包是 Swift 函数式编程的基础,它和其它语言(如:Haskell,Scala)中的 lambda 比较相似,也与 C 和 Objective-C 中的 blocks 类似。想了解一下函数式编程的小伙伴可以戳这里,一个挺不错的函数式编程介绍视频。这里还有一本书是专门讲 Swift 函数式编程的,出自大喵神。

    引用 Swift 创始人 Chris Lattner 的一句话:

    Swift 引入了泛型和函数式编程的思想,极大地扩展了设计的空间。

    基本概念

    闭包在 Swift 中是一种功能性的自包含模块,可以作为一种参数类型在代码中被传递和使用。更具体的概念可以去官网查看,网上也有各种各样的翻译版。这里我整理一些我认为非常重要的概念。

    • 闭包的简化过程
    • 捕获值和内存环(Memory cycle)
    • 非逃逸闭包和自动闭包(@noescape & @autoclosure)

    闭包的简化过程

    在我的计算器 Demo 中(Github 地址),运算符的操作就是用的闭包,如下:

    "×": Operation.BinaryOperation { $0 * $1 },
    "÷": Operation.BinaryOperation { $0 / $1 },
    "+": Operation.BinaryOperation { $0 + $1 },
    "−": Operation.BinaryOperation { $0 - $1 },
    

    上面的式子已经是最终形态,对于学习闭包,熟悉这个简化过程还是挺重要的,我以"×"为例,将过程写在下面。

    一开始,像我这种屌丝程序员肯定会以最屌丝的方式写“两数相乘”,就像这样:

    func multiply(op1: Double, op2: Double) -> Double {
      return op1 * op2
    }
    

    多么完美的表达。但是,Swift 为我们提供了一个非常强大的功能-闭包。我们可以把我们所写的函数主体搬到字典中,于是"×"这一段就成了:

    "×": Operation.BinaryOperation ((op1: Double, op2: Double) -> Double {
      return op1 * op2 
      }
    ),
    

    根据闭包的定义,

    所以我们需要把大括号放到前面,然后加上 in。这就是闭包,闭包是类型,可以放在字典中进行使用。

    "×": Operation.BinaryOperation ({(op1: Double, op2: Double) -> Double in
      return op1 * op2 
      }
    ),
    

    因为在 Swift 中,类型是可以自动识别的,所以那些 Double 都是可以省略的,所以就成了:

    "×": Operation.BinaryOperation ({(op1, op2) in return op1 * op2 }),
    

    而 Swift 中有默认的参数,就是 $0, $1 等,所以就成了:

    "×": Operation.BinaryOperation ({($0, $1) in return $0 * $1 }),
    

    当你用了默认的参数,前面的那个参数就不需要写了,因为都默认了嘛。

    "×": Operation.BinaryOperation ({ return $0 * $1 }),
    

    既然前面参数不用写,后面肯定是返回的值呀,所以 return 这个关键词也不用写了。

    "×": Operation.BinaryOperation ({ $0 * $1 }),
    

    根据尾部闭包(Trailing Closures)的定义,如果闭包是函数的最后一个参数,可以把闭包体写在圆括号的外面,这样:

    "×": Operation.BinaryOperation (){ $0 * $1 },
    

    而如果这个闭包是函数的唯一参数,那么这个圆括号也可以不用写,就成了我们开头给的最终形态了:

    "×": Operation.BinaryOperation { $0 * $1 },
    

    从那么长的一段代码,最后简化成这么一点点,闭包的强大之处可见一斑。而我认为最关键的不是长短,是逻辑,它使得两数相乘的这个逻辑更加清晰明了了,就像我们手写计算过程一样,就是第一个数乘以第二个数:$0 * $1。这也反映出了一点点函数式编程的思想。

    捕获值和内存环(Memory cycle)

    闭包其实是引用类型,因为闭包是函数,而函数在 Swift 中就是普通的类型(Types),他们都是存在于堆(heap)中的。它们可以被存放在数组,字典,等结构中。闭包在 Swift 中是一等公民,关于First-class citizen

    此外,闭包可以捕获上下文中定义的的任何常量和变量的引用,如果变量或者常量不在 heap 中,就把它们存到heap 中。那些被捕获的变量,如果它们的闭包还在 heap 中,那么它们也得待在 heap 中。这就可能出现内存环(Memory cycle),中文翻译的有点蠢。。差不多和数据库中的死锁类似。

    它不会像死锁一样马上导致奔溃,但就像死锁一样,A 需要 B,而 B 也需要 A,于是就导致了一个内存环。A 有一个 strong 的 pointer 指向 B, B 也有一个 strong 的 pointer 指向 A,于是 A 和 B 都无法从 heap 中释放出来。如果对象很大,那么内存很快就会被消耗掉。

    比如在我们计算机 Demo 中,增加一个一元运算:

    brain.addUnaryOperation("Red√") { 
                display.textColor = UIColor.redColor()
                return sqrt($0)
            }
    

    这段代码无法通过编译,提示需要在 display 前面加上 self.。因为编译器要你知道,这个闭包要捕获 self,然后用一个 strong 的 pointer 指向它。于是 Model 和 Controller 就有了互相指向对象的指针,就产生了内存环。

    strong,weak,和 unowned

    这里插播一段广告,概念介绍。。

    strong 是默认的引用计数,所以 strong 关键字一般不写出来。 任何地方用了 strong 的指针指向一个实例,那么它就必须待在 heap 中,直到没有东西再指向它。

    weak 就是,如果她们都对我没那么感兴趣,那我离开好了,你们自己设为 nil。因为可以设置为 nil,所以 weak 只能用于 Optional pointers。关键的是,weak 指针不把对象放在 heap 中,比如 outlets。

    unowned,这个比较危险,很少用,一般就是用来打断内存环的。它的意思是不要创建一个强指针,所以如果指向的对象不在 heap 中了,就会 crash。

    解决方法

    所以我们应该如何修改代码应对我们上面提到的那个错误呢?有两种方法,比如用 weak,也可以用 unowned。

    weak 的话,可以在闭包里加上 [weak weakSelf = self] in,来申明一个特殊的变量,用于闭包中。

    brain.addUnaryOperation("Red√") { [weak weakSelf = self] in
                weakSelf?.display.textColor = UIColor.redColor()
                return sqrt($0)
            }
    

    unowned 的话,可以写成这样:

    brain.addUnaryOperation("Red√") { [unowned me = self] in
                me.display.textColor = UIColor.redColor()
                return sqrt($0)
            }
    

    非逃逸闭包和自动闭包

    非逃逸闭包

    闭包逃逸指的是,当一个闭包作为一个函数的变量的时候,它在函数返回之后被调用。当你想申明那个闭包是非逃逸的,就用 @noescape,放在参数名前面。

    举个例子,将官网上的例子稍作改动:

    这是非逃逸的闭包:

    func nonescapingClosure(@noescape closure: () -> Void) {
        closure()
    }
    

    这是逃逸的闭包,闭包的申请在函数外面:

    var completion: [() -> Void] = []
    func escapingClosure(completionHandler: () -> Void) {
        completion.append(completionHandler)
    }
    

    某个 class,将不同的值用两种闭包赋值给 x,看结果。

    class SomeClass {
        var x = 10
        func doSomething() {
            escapingClosure { [weak weakSelf = self] in weakSelf?.x = 100 }
            nonescapingClosure { x = 200 }
        }
    }
    
    let instance = SomeClass()
    instance.doSomething()
    print(instance.x)
    // Prints "200"
    
    completion.last?()
    print(instance.x)
    // Prints "100"
    

    可以看到逃逸的闭包在 completion.last?() 后才调用,才将 x 赋值成100。

    自动闭包

    所谓自动闭包,就是自动的将表达式包装好传递给函数作为参数。它不接受任何参数,当它被调用时,返回包装里面的表达式的值。它有两个优点:

    1. 因为是自动包装的,所以免去了显示闭包,省去了闭包的花括号,只要写正常的表达式就好了。在下面的例子中,当我用了 @autoclosure 后,我发现加上花括号反而会报错,无法识别 Element。
    2. 延迟处理,因为这段代码直到你调用了这个闭包才会被执行。

    举个非常简单的例子

    var myArray = ["1","2","3","4","5"]
    func addOneElement(@autoclosure arrayOne: () -> Void) {
        print("Last element of myArray is \(arrayOne())!")
    }
    
    print("Last element of myArray is \(myArray.last)!")
    // "Last element of myArray is Optional(“5”)!\n"
    addOneElement( myArray.append("6") )
    print("Last element of myArray is \(myArray.last)!")
    // "Last element of myArray is Optional(”6“)!\n"
    

    by: 诸葛俊伟
    欢迎转载,转载请注明出处。非常欢迎 Swifter 们一起讨论一起学习。

    相关文章

      网友评论

      本文标题:Swift 中的闭包(Closures)

      本文链接:https://www.haomeiwen.com/subject/szagrttx.html