Swift:闭包

中文文档

  • 函数是特殊的闭包
全局函数是一个有名字但不会捕获任何值的闭包
嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
  • Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
利用上下文推断参数和返回值类型
隐式返回单表达式闭包,即单表达式闭包可以省略 return 关键字
参数名称缩写
尾随闭包语法

一、闭包表达式

1、闭包表达式语法
  • 闭包表达式语法有如下的一般形式:
{ (parameters) -> return type in
    statements
}
  • 以数组的sorted(by:)方法为例, 该方法对数组进行排序, 并返回新的数组, 原数组不变
let list = [1, 4, 7, 2, 6, 3, 8, 9, 5]
let sorted: (Int, Int) -> Bool = { (x: Int, y: Int) -> Bool in
    return x > y
}
let newList = list.sorted(by: sorted)
  • 由于这个闭包的函数体部分如此短, 以至于可以将其改写成一行代码:
let newList = list.sorted(by: { (x: Int, y: Int) -> Bool in return x > y })
2、根据上下文推断类型
  • 因为排序闭包函数是做为sorted(by:)方法的参数传入的, Swift可以推断其参数和返回值的类型

  • sorted(by:) 方法被一个字符串数组调用,因此其参数必须是 (String, String) -> Bool 类型的函数。

  • 这意味着 (String, String)Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:

let newList = list.sorted(by: { x, y in return x > y })
3、单表达式闭包隐式返回
  • 单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
let newList = list.sorted(by: { x, y in x > y })
  • 在这个例子中,sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。因为闭包函数体只包含了一个单一表达式(x > y),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略。
4、参数名称缩写
  • Swift自动为内联闭包提供了参数名称缩写功能, 你可以直接通过$0, $1, $2来顺序调用闭包的参数

  • 以此类推。如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。

  • in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:

let newList = list.sorted(by: { $0 > $1 })
  • 在这个例子中,$0$1 表示闭包中第一个和第二个 Int 类型的参数。
5、运算符方法
  • 上面的闭包表达式, 可以使用更简短的方式
let newList = list.sorted(by: > )

二、尾随闭包

  • 如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
}

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

// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
    // 闭包主体部分
}
  • 闭包表达式语法一节中作为 sorted(by:) 方法参数的字符串排序闭包可以改写为:
let newList = list.sorted(by: { $0 > $1 })
  • 如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:
let newList = list.sorted { $0 > $1 }

三、值捕获

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

  • Swift 中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
let incrementerTen = makeIncrementer(forIncrement: 10)
print(incrementerTen())        // 打印: 10
print(incrementerTen())        // 打印: 20

let incrementerSeven = makeIncrementer(forIncrement: 7)
print(incrementerSeven())        // 打印: 7
print(incrementerSeven())        // 打印: 14

注意
如果你将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员而捕获了该实例,你将在闭包和该实例间创建一个循环强引用。Swift 使用捕获列表来打破这种循环强引用。

四、闭包是引用类型

  • 上面的例子中,incrementBySevenincrementByTen 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型。

  • 无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用 incrementByTen 是一个常量,而并非闭包内容本身。

  • 这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:

let alsoIncrementByTen = incrementerTen
alsoIncrementByTen()
// 返回的值为30

五、逃逸闭包

  • 当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。

  • 当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的

  • 例如下面的代码, 当someFunctionWithEscapingClosure(_:)函数执行完之后, 传入的闭包被completionHandlers数组存储, 然后在其他地方调用, 此时就必须使用逃逸闭包, 使传入的闭包可以在someFunctionWithEscapingClosure(_:)函数执行完之后, 再使用

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure {
    print(123)
}
print("----------------")
completionHandlers.first?()
  • 将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self

  • 比如说, 下面的代码中, 传递到someFunctionWithEscapingClosure(_:)中的闭包是一个逃逸闭包, 这意味着他需要显示的引用self

  • 相对的, 传递到someFunctionWithNonescapingClosure(_:)中的闭包是一个非逃逸闭包, 这意味着它可以隐式引用self

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

// 定义一个类, 起名为 SomeClass
class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出 "200"

completionHandlers.first?()
print(instance.x)
// 打印出 "100"

六、自动闭包

  • 自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。

  • 这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。

  • 这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

  • 我们经常会调用 使用自动闭包做为参数的函数,但是很少去实现这样的函数。

  • 举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message 参数;它的 condition 参数仅会在 debug 模式下被求值,它的 message 参数仅当 condition 参数为 false时被计算求值。

  • 自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。

  • 下面的代码展示了闭包如何延时求值。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出 "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出 "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"
  • 请注意,customerProvider 的类型不是 String,而是 () -> String,一个没有参数且返回值为 String 的函数。

  • 尽管在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。

  • 将闭包作为参数传递给函数时,你能获得同样的延时求值行为。

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出 "Now serving Alex!"
  • 下面这个版本的 serve(customer:) 完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 @autoclosure 来接收一个自动闭包。

  • 现在你可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。customerProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure 特性。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印 "Now serving Ewa!"

注意
过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。

  • 如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure@escaping 属性。@escaping 属性的讲解见上面的逃逸闭包
// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
// 向数组中添加两个闭包, 由于是自动闭包, 所以直接当做参数传入, 不需要大括号{}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// 打印 "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// 打印 "Now serving Barry!"
// 打印 "Now serving Daniella!"
  • 在上面的代码中,collectCustomerProviders(_:) 函数并没有调用传入的 customerProvider 闭包,而是将闭包追加到了 customerProviders 数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。因此,customerProvider 参数必须允许“逃逸”出函数作用域。

你可能感兴趣的:(Swift:闭包)