Swift 5.0 Language Guide - closure (闭包)

7.Closure

闭包,是自包含的功能块,可以在代码中传递和使用,闭包和C,OC中的block很相似,也和其他语言中的lambdas很相似。

闭包可以从定义他们的上下文中获取和存储对任意常量和变量的引用,这个被称作常量和变量的闭包,Swift为你处理捕获的所有内存管理。

⚠️如果你不熟悉捕获的概念,请别担心,在下面的捕获值中有详细的解释.

在上一章函数中提到的全局和嵌套函数,实际上是闭包的特殊情况.闭包采用三种形式之一:

  • 全局函数是一种有一个名字,却不捕获任何值的闭包
  • 嵌套函数是一种有一个名字,可以捕获来自封闭函数的值
  • 闭包表达式是一种没有名字的闭包,很轻量级的写法,可以捕获来自上下文的值

Swift的闭包表达式在通常场合下有一个干净的,清晰的风格,和优化的作用,鼓励简洁,无杂乱的语法,这些优化包括:

  • 根据上下文推断参数和返回值类型
  • 来自简单表达闭包的隐式返回
  • 速记参数名
  • 尾随闭包语法

1. 闭包表达式

嵌套函数(在嵌套函数中引入)是一种方便的方法,可以将自包含的代码块命名和定义为更大函数的一部分。但是,在没有完整声明和名称的情况下编写类似函数的构造的更短版本有时是有用的。当您使用将函数作为其一个或多个参数的函数或方法时,尤其如此。

闭包表达式是一种用简洁,集中的语法书写的内联闭包的方式,闭包表达式提供了几种用简短形式写闭包的语法优化,同时没有失去清晰度和意图.下面这个闭包表达式例子说明了这些优化,通过几次迭代精简一个简单例子sorted(by:) ,每一次迭代都能用更简洁的方式表达同样的功能。

1.1 排序方法

Swift 的标准库提供了一个方法叫sorted(by:),对一个已知类型的数组进行排序,基于你提供的排序闭包的输出。一旦它完成排序过程,这个方法就会返回一个相同类型和大小的新的数组,元素的顺序是正确的了.原始的数组没有被这个方法改变.

下面的这个闭包表达式例子用这个方法对一个字符串数组按照逆字母排序,这是要被排序的初始化数组:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

这个 sorted(by:)方法接受一个闭包,带2个相同类型的参数作为数组的内容,并且返回一个BOOL值说第一个值是否应该出现在第二个值之前还是之后,一旦值要排序了. 如果第一个值应该出现在第二个值之前,这个排序闭包需要返回true,否则就返回false.

这个例子是对字符串类型的数组进行排序的,所以排序闭包需要是一个 (String, String) -> Bool类型的函数.

提供排序闭包的一种方法是编写正确类型的普通函数,并将其作为参数传递给sorted(by:)方法:

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果第一个字符串(S1) 比第二个string(S2)大,函数 backward(_:_:) 将返回true,表示在排序数组中,S1 应该出现在S2前面.
对于一串字符串里的字符,‘大于’意思是在字母表中出现的晚. 这意味着‘B’大于‘A’, 'Tom' 大于 'Tim'. 这给一个反字母表排序,‘Barry’ 出现在 'Alex' 前,等等.
然而,这是一个基本写一个简单表达式函数(a>b)的比较冗长的方式,在这个例子中,在一行内写一个排序闭包更受欢迎,用闭包表达式语法.

1.2 闭包表达式语法

闭包表达式语法是下面的常用格式:

{ (parameters) -> return type in
    statements
}

parameters可以试输入输出参数,但他们不能有默认值。如果你命名一个多变参数,你也可以用多变参数。元组也可用作参数和返回值.

下面这个例子展示了backward(_:_:)函数的闭包表达式写法:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

注释:(inline)内联闭包的参数和返回值的声明和 backward(_:_:) 函数的声明是完全一样的,在两个情况下,它都可以写成(s1: String, s2: String) -> Bool. 然而,对于线内(inline)闭包表达式,参数和返回值被写在大括号(the curly braces)里面,而不是外面.
闭包体是由in 关键的开始的. 这个关键字表示闭包参数和返回值得定义已经完成了,闭包体要开始了.

因为闭包体很短,它甚至可以写在一行里.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

这说明对sorted(by:)方法的整体调用保持不变,一对圆括号仍然包含该方法的整个参数。然而,这个参数现在是一个内联闭包。

1.3 从上下文推断类型 ( Inferring Type From Context)

因为这个排序闭包是传一个参数到一个方法中,Swift可以推断参数和返回值类型,这个 sorted(by:)方法被一个字符串数组调用,所以参数一定是 (String, String) -> Bool类型的函数.这意识是 (String, String) 和 Bool 不需要写在闭包表达式的定义里. 因为所有的类型都可以被推断,所以这个返回箭头 (->) 和参数名旁边的圆括号也可以被省略:

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

当传一个闭包到一个函数或方法里作为内联闭包表达式的时候,通常都能推断出参数和返回值类型. 作为一个结果,你不需要写一个全形式的内联闭包当这个闭包做为一个函数或方法的参数时.

尽管如此,如果你愿意的话,你仍然可以定义明确的类型,这么做是鼓励读者读你的代码时避免歧义. 在 sorted(by:)方法情况下,从正在进行排序的事实可以清楚地看出闭包的目的。读者可以安全地假设这个闭包可能使用字符串值.因为他辅助字符串数组的排序.

1.4 简单表达式闭包的隐式返回值(Implicit Returns from Single-Expression Closures)

简单表达式闭包可以从他们的定义中 通过省略return 关键字隐式地返回他们简单表达式的结果,像上一个例子的这个写法:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

这里,这个sorted(by:)函数类型的参数,使得一个BOOL必须是闭包的返回值更清晰。因为这个闭包体包含一个简单的表达式(s1 > s2) ,它的返回值是一个Bool,这里没有歧义,return关键字可以省略.

1.5 缩写参数名字(Shorthand Argument Names

Swift 给内联闭包自动提供了速记参数名字,可以通过参数名字$0, $1, $2等,来引用闭包参数的值,
如果在你的闭包表达式中用速记参数名字,你可以从定义中省略闭包的参数列表,参数名字的数量和类型可以从期望的函数类型中被推断出来. in 关键字也可以被省略,因为闭包表达式可以完全由主体组成:

reversedNames = names.sorted(by: { $0 > $1 } )

这里,$0 和 $1 指的是闭包的第一个和第二个字符串参数.

1.6 运算符方法(Operator Methods

实际上有一个更短的方式写上面的闭包表达式,Swift 的字符串类型定义它的特殊的字符串实现大于号运算符 (>) 作为字符串类型的两个参数的比较,返回值是Bool类型. 这完全匹配上sorted(by:) 需要排序的方法. 所以,你可以简单通过传入一个大于号,Swift 将推断出你想用的字符串的特殊实现:

reversedNames = names.sorted(by: >)

更多运算符方法,请参见Operator Methods.

2.  尾随闭包

如果你需要传一个闭包表达式给一个函数作为函数的最后一个参数,并且这个闭包表达式比较长,用尾随闭包代替的写法是有用的. 尾随闭包写在函数调用的圆括号后面,即使它仍然是一个函数的参数. 当你用尾随闭包语法时,你不需要为闭包写参数标签作为函数调用的一部分.

    func someFunctionThatTakesAClosure(closure: () -> Void) {
        // function body goes here
    }

    // Here's how you call this function without using a trailing closure:

    someFunctionThatTakesAClosure(closure: {
        // closure's body goes here
    })

    // Here's how you call this function with a trailing closure instead:

    someFunctionThatTakesAClosure() {
        // trailing closure's body goes here
    }

上面闭包表达式语法( Closure Expression Syntax )部分中的字符串排序闭包可以写在排序(by:)方法的括号之外,作为尾随闭包:

reversedNames = names.sorted() { $0 > $1 }

如果一个闭包表达式用来作为函数或方法的唯一参数,你提供这个表达式作为尾随闭包,当你调用这个函数时,你就不需要在函数或方法名后写一对圆括号()

reversedNames = names.sorted { $0 > $1 }

当这个闭包非常长,不可能在内联函数里一行写完时,尾随闭包是最有用的. 像一个例子,Swift的数组有map(_:)方法,它让闭包作为它的简单参数.这个闭包被数组里的每一个item调用,并且为每一个item返回一个可替代的映射值(可能是其他类型的). 这个映射的类型和返回值的类型由闭包指定.

在应用了提供的闭包给每一个数组元素之后,map(_:) 方法返回一个新的数组,数组里包含了所有新的一一对应的映射值.

这里你可以知道怎样用带有尾随闭包的 map(_:) 方法来转换一个Int数组为一个String 数组,数组 [16, 58, 510]用来创建一个新的数组["OneSix", "FiveEight", "FiveOneZero"]:

let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

上面的代码创建一个映射字典,这个字典是整型数字和他们名字的英语版本. 它也定义了一个整型数组,准备好去转换成字符串.

你现在可以用数字数组来创建一个String类型的数组,通过给map(_:) 方法传一个闭包表达式作为尾随闭包:

let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

map(_:) 会为数组的每一个item调用一次尾随闭包. 你不需要清楚闭包的输入参数 number 的类型,因为Swift可以根据被映射的数组的值推断闭包参数的类型.

在这个例子中,变量number被闭包的 number 参数的值初始化,所以变量 number 可以在闭包内修改(函数和闭包的参数通常是常量)这个闭包表达式也指定一个String类型的返回值类型,来表示这个将被存进映射后输出数组的类型.

这个闭包表达式每次被调用的时候,创建一个string叫output.它通过余数运算符(number % 10)来计算最后一个数字,用这个数字在digitNames 字典中查找一个合适的string. 这个闭包可以被用来创建一个string来表达任意一个大于0的整数.

⚠️ digitNames[number % 10]后面跟了一个!,因为字典的值返回一个可选值,表示如果key值不存在这个字典查找可能会失败.
在上面的例子中,它保证number % 10一直是有值的键值对,所以感叹号!标记强制解包string的值存可选项的返回值.

digitNames 字典返回的string被加到output前面,有效地创建一个由数字转过来的string版本.(number % 10的余数是,6是16的,8是58的,0是510的.)

number 变量被10除,因为是整数,在除法过程中被整除,所以16成了1,58就成了5,510就成了51.

这个处理会重复,直到number等于0,这个时候,output string 就回被闭包返回,并且被map(_:) 方法加到output数组.

在上面的例子中,尾随闭包语法的使用简洁地概括了闭包功能立即在函数的后面,不需要在外面的括号里解包整个闭包.

3. 捕捉值

闭包可以从定义它的周围上下文中捕获常量和变量。然后闭包可以引用并修改其体内的常量和变量的值,即使定义常量和变量的原始范围不再存在。
在Swift中,可以捕获值的最简单形式的闭包是嵌套函数,写在另一个函数体内。嵌套函数可以捕获其外部函数的任何参数,还可以捕获外部函数中定义的任何常量和变量。

这是一个调用函数的示例makeIncrementer,其中包含一个名为的嵌套函数incrementer。嵌套incrementer()函数捕获两个值,runningTotal并且amount,从它的周围环境。捕获这些值后,incrementer将作为一个闭包返回值被makeIncrementer 返回,这个闭包每次被调用的时候,runningTotal都会递增amount

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer的返回类型是 () -> Int,这意味着它返回的是一个函数,而不是一个简单的值。这个函数没有参数,在它每次被调用的时候,返回一个Int值.要学习函数怎样返回其他函数,参见函数类型作为返回值类型.

这个函数makeIncrementer(forIncrement:) 定义一个int变量叫runningTotal,去存储这个当前正在运行的加起来总和的返回值.这个变量初始值是0.

这个函数makeIncrementer(forIncrement:) 有一个简单的int参数,参数标签是forIncrement,参数名是amount. 传递给此参数的参数值指定runningTotal每次调用返回的增量函数时应递增多少。该makeIncrementer函数定义了一个名为的嵌套函数incrementer,它执行实际的递增。此功能只是增加了amountrunningTotal,并返回结果。

单独考虑时,嵌套incrementer()函数可能看起来不常见:

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

incrementer()函数没有任何参数,但它在其函数体内引用runningTotal和引用amount。它通过从周围函数捕获对runningtotal和amount的引用来实现这一点和其自身的函数体中使用它们。通过参考捕捉保证runningTotalamount不消失的时候调用makeIncrementer结束,而且也保证了runningTotal可用下一次incrementer函数被调用。

⚠️作为优化,如果这个值不被闭包变异,并且在闭包被创建之后如果这个值不被改变,Swift用捕捉并存储一个copy的值,
Swift还处理在不再需要变量时处理变量所涉及的所有内存管理。

这是一个实际的例子makeIncrementer

let incrementByTen = makeIncrementer(forIncrement: 10)

 

此示例设置一个常量incrementByTen,该常量调用以引用每次调用时添加10到其runningTotal变量的增量函数。多次调用该函数会显示此行为:

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

如果你创建第二个增量器,它将拥有自己存储的对新的单独runningTotal变量的引用:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

 

 

incrementByTen再次调用原始增量器()会继续增加其自己的runningTotal变量,并且不会影响由incrementBySeven以下内容捕获的变量:

incrementByTen()
// returns a value of 40

 

⚠️如果为类实例的属性分配闭包,并且闭包通过引用实例或其成员来捕获该实例,则将在闭包和实例之间创建一个强引用循环。Swift使用捕获列表来打破这些强大的参考周期。有关更多信息,请参阅闭包的强引用周期

2.2 闭包是引用类型

在上面的例子中,incrementBySevenincrementByTen是常量,但这些常量引用的闭包仍然能够增加runningTotal它们捕获的变量。这是因为函数和闭包是引用类型

无论何时将函数或闭包赋值给常量或变量,实际上都是将该常量或变量设置为对函数或闭包的引用。在上面的例子中,它是闭包的选择,它incrementByTen 引用的是常量,而不是闭包本身的内容。

这也意味着如果为两个不同的常量或变量分配闭包,那么这两个常量或变量都引用相同的闭包。

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

上面的例子显示调用alsoIncrementByTen与调用相同incrementByTen。因为它们都引用相同的闭包,它们都会递增并返回相同的运行总计。

2.3 逃逸闭包

当一个闭包被作为参数传给另一个函数时,我们说这个闭包逃出了一个函数。但函数返回之后它会被调用. 当你声明一个函数,闭包是它参数中的一个,你可以在参数的类型前写@escaping来代表这个闭包允许逃离.

闭包逃离的一种方法是在函数的外面定义一个变量存起来.例如, 很多函数开启一个异步操作来让闭包作为实现处理。这个函数在它开始操作之后返回,但是闭包被有被调用,直到这个操作完成,闭包需要逃离,在之后被调用,例如:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

函数 someFunctionWithEscapingClosure(_:)让一个闭包作为参数,并且把闭包添加到一个数组里,这个数组在函数外定义,如果你不把这个函数的参数用 @escaping 标记,你会遇到一个编译错误. 

用@escaping 标记一个闭包,意味着你必须在闭包里明确地引用self,比如在下面的代码里,传给函数someFunctionWithEscapingClosure(_:) 的是一个逃逸闭包,这意味着它需要明确的引用self. 作为对比,传给函数 someFunctionWithNonescapingClosure(_:)的是一个非逃逸闭包,这意味着你可以隐含的调用self.

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

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

2.4 自动闭包

一个autoclosure是一个自动创建的闭包,来包装一个表达式,这个表达式是被作为参数传给函数的,它不接受任何参数,当它被调用的时候,它返回这个被包装在里面的表达式的值. 这语法方便之处在于,你可以通过写一个正常的表达式而不是显式闭包来省略函数参数周围的大括号.

调用一个接受自动闭包的函数是很常见的,但实现这样的函数是不常见的. 比如,the assert(condition:message:file:line:) 为condition和message 参数接收autoclosure.  函数的condition 参数只有在调试版本上才被计计算,message 参数只有在condition为false的时候才会被计算. 

autoclosure 允许你延迟求值,因为在它内部的代码直到闭包被调用时才会执行. 延迟求值对于代码来说很有用的,这些代码有副作用或者计算成本高,因为当代码被评估的时候它让你控制. 下面的砝码展示闭包如何延迟求值.

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

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

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

即使 customersInLine的第一个元素被闭包内部的代码删除了,这个元素实际上在闭包调用的时候没有被删除.如果闭包一直不被调用,那闭包内部的表达式永远不会执行,这意味着数组元素永远不会被删除.请注意,customerProvider的类型不是String,而是() -> String,一个没有参数有一个string返回值的函数.

当你传一个闭包作为函数参数时,你会得到与延迟求值相同的行为。

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

 上面的serve(customer:) 函数采用显式闭包,返回一个客户的名字. 下面的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))
// Prints "Now serving Ewa!"

⚠️过度使用自动闭包(autoclosures)会让你的代码难以理解.上下文和函数名称,应该明确表示正在延迟执行.

如果你想要允许自动闭包(autoclosure)逃逸,请使用@autoclosure@escaping属性。上面的@escaping在Escaping Closures中描述了该属性。

// customersInLine is ["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.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"

在上面的代码中,不是调用闭包传入作参数customerProviderargument,这个函数collectCustomerProviders(_:) 添加闭包到customerProviders数组, 这个数组定义在函数的作用域外,这意味着数组里的闭包可以在函数返回后被执行.作为结果,customerProvider参数必须允许逃离函数作用域.

 

 

 

 

 

 

 

 

  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值