Swift 中的闭包(Closures)

前言

今天我想聊一聊 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 们一起讨论一起学习。

推荐阅读更多精彩内容