【Swift3.1】闭包(Closures)

本文档是官方文档Swift3.1中闭包章节的中文翻译
转载请注明出处
http://blog.csdn.net/q838197181/article/details/72809891

目录

闭包

闭包是可以在你的代码中传递和使用的独立的功能模块。Swift中的闭包很像CObject-C中的blocks,和其它语言中的匿名函数也很像。

闭包可以捕获和存储它被定义的上下文环境中的常量和变量。This is known as closing over those constants and variables.(这句实在不知道怎么翻译……)Swift为你处理了所有捕获值的内存管理。

NOTE
如果你不熟悉捕获值的概念,不用着急。下面捕获值章节会详细讲解。

函数中介绍的全局函数和内嵌函数,都是特殊形式的闭包。闭包有三种形式:

  1. 全局函数是有名字,不捕获任何值的闭包。
  2. 内嵌函数是有名字,捕获外部函数值的闭包。
  3. 闭包表达式是一个使用轻量级语法所写的能够捕获它上下文环境中值的没有名字的闭包。

Swift的闭包表达式有着简洁的语法风格,在常见的使用场景中鼓励简洁,无累赘的语法。这些优化包括:

  1. 从上下文环境中推断参数类型和返回值类型。
  2. 单表达式的隐式返回
  3. 简写参数名
  4. 尾随闭包语法

闭包表达式

内嵌函数中介绍的内嵌函数,是一种在大函数中命名和定义独立代码块的方法。然而,有时候书写没有完整声明和函数名的简短的类似函数的结构是非常有用的。在你使用拥有一个或多个函数作为参数的函数或方法时更是如此。

闭包表达式是一种简洁的,专注的,在简短行内就能写完闭包的方法。闭包表达式为简化闭包的写法而又不失清晰度提供了一系列语法优化。下面的这个闭包表达式通过重新定义sort(by:)方法及进行多次迭代来说明这些优化,每一次迭代都通过一种简洁的方式使用相同的功能。

Sorted方法

Swift的标准库中提供了一个叫sort(by:)的方法,它根据你提供的排序闭包来对已知类型的数组进行排序。当它完成排序时,sort(by:)方法会返回一个和原数组相同类型,相同大小的排序正确的新数组。原数组不会被sort(by:)方法修改。

下面的闭包表达式栗子使用sort(by:)方法来将一个String数组进行按照字母顺序进行反向排序。这是待排序的初始化数组:

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

sort(by:)方法接收一个闭包,这个闭包以数组中两个相同类型的内容作为参数,并且返回一个Bool来说明排序后第一个值应该出现在第二个值的前面还是后面。如果第一个值出现在第二个值前面,这个排序闭包应该返回true,否则返回false

这个栗子是对一个String数组进行排序,所以这个闭包的类型应该是(String, String) -> Bool

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

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

如果第一个字符串s1大于第二个字符串s2backward(_:_:)函数将返回true,表明s1在排序数组中应该出现在s2前面。对于字符串中的字符,“大于”意思是“在字母表中出现在后面”。这也就是说字母B“大于字母”A,字符串Tom大于字符串Tim。这就给出了一个反序字符表的排序,"Barry"排在Alex前面,以此类推。

然而,这种方式写实际上是一个单表达式的函数a > b是非常啰嗦的,在这个栗子中,更可取的应该是使用闭包表达式语法写行内排序闭包。

闭包表达式语法

闭包表达式语法是下面这样的一般形式:

{ (parameters) -> return type in
    statements
}

闭包表达式中的参数可以是输入输出参数,但它们不能有默认值。可变参数也可以使用。元组可以被用作参数和返回值。

下面的栗子展示了之前backward(_:_:)函数的闭包表达式版本:

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

注意这个行内闭包的参数和返回值类型声明都和backward(_:_:)函数一样。在这两种情况中,都写成了(s1: String, s2: String) -> Bool。然而,行内的闭包表达式的参数和返回值类型是写在花括号里面的而不是外面。

闭包的内部代码由关键字in开始。这个关键字表明闭包的参数和返回值定义已经结束了,闭包内部的代码要开始了。

因为闭包的函数体很短,甚至可以一行就写完:

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

这个栗子中sort(by:)方法的内部调用是一样的。一对圆括号仍然包裹着方法等参数,只不过现在的参数是一个行内闭包。

从上下文中推断类型

因为排序闭包是作为参数传递给方法的,所以Swift可以推断它的参数类型和返回值类型。sort(by:)是被一个字符串数组调用的,因此它的参数类型必然是(String, String) -> Bool。这就意味着(String, String)Bool类型不用写在闭包表达式的定义中。因为所有的类型都可以被推断出来,返回箭头->和参数名外面的括号都可以省略:

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

当将一个闭包表达式传入函数或方法时,通常都可以推断出类型。所以,当你将一个行内闭包作为参数传入一个函数或者方法时,你永远也不需要写行内闭包的完整形式。

尽管如此,但只要你愿意你仍然可以显式的写清楚类型,如果这样可以避免读者阅读你的代码会引起的误会,也是值得鼓励的。在sorted(by:)这个方法中,闭包的目的很明确,就是排序执行的地方,对读者来说,认为这个闭包会使用Stirng值是安全的,因为它正在进行一个字符串数组的排序。

单表达式隐式返回

单表达式闭包可以在声明中通过省略return关键字来隐式的返回单表达式的结果,前面的栗子就可以这样写:

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

在这里,sorted(by:)方法的参数类型已经表明了,这个闭包必然会返回一个Bool。因为闭包函数体包含了一个单表达式s1 > s2会返回Bool,这是没有歧义的,所以return关键字可以被省略。

简写参数名

Swift为行内闭包自动提供简写参数名,可以通过$0, $1, $2来指向闭包的参数。

如果你在闭包表达式中使用了简写的参数,你就可以省略闭包定义中的参数,并且简写参数的序号和类型会从期望的函数类型中推断。in关键字也可以被省略,因为闭包表达式只剩它的函数体了:

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

这里,$0$1分别指向闭包的第一个和第二个参数。

运算符函数

上面的闭包表达式还可以用一种更加简短的方式来写。Swift的String数据类型定义了关于大于号>的特定实现方法,即有两个String参数,返回一个Bool。这恰好和sorted(by:)需要的方法类型相匹配。因此,你可以只传入一个大于符号,Swift会推断你想使用的特定实现方法:

reverseNames = names.sorted(by: >)

关于更多的运算符函数的内容,请看运算符函数

尾随闭包

当你将一个很长的闭包表达式作为函数的最后一个参数传递给函数时,此时使用尾随闭包会更好。尾随闭包写在函数调用的括号后面,即便该闭包仍然是函数的参数。在函数调用中使用尾随闭包时,不用写闭包在函数中的参数名。

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
}

上述闭包表达式章节中的字符串排序闭包可以作为尾随闭包写在sort(by:_)函数括号后面:

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

如果闭包表达式是函数的唯一参数,并且使用了尾随闭包,那么函数调用时的括号可以省略:

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

当闭包很长以至于不能一行写不完时,尾随闭包就显得很有用了。比如啊,Swift中的Array类型中有一个以闭包表达式作为唯一参数的map(_:)方法。数组中的每一个元素都调用一次这个闭包,返回该元素所映射的值(有可能是其它类型)。实际的映射规则和返回值类型由闭包来决定。

在为数组中每一个元素执行了传入的闭包后,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]

上面的代码创建了一个整数数字和相应的英文名字的映射字典,并且定义了一个准备转换成字符串数组的整数数组。

现在以尾随闭包的形式为map(_:)方法传入一个闭包表达式,用number数组创建一个String数组:

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_:)方法为数组中的每一个元素调用一次闭包。你不需要制定闭包参数number的类型,因为它的类型可以从数组中推断出来。

在这个栗子中,闭包内部的变量number被闭包的参数number初始化,所以才可以在闭包内被修改(函数和闭包的参数是常量)。这个闭包表达式同样指定了返回值类型String,来表面存储映射值的output数组类型为String。

这个闭包表达式每调用一次就创建一个output字符串,它通过取余运算符(number % 10)来计算number的最后一位数字,并在digitNames中查找相应的字符串。这个闭包表达式可以用来创建任何大于0的整数所表示的字符串。

NOTE
digitNames下标后带了一个感叹号(!),因为字典下标会返回一个可选值,来表面当key不存在时获取失败的情况。在上面的栗子中,它保证了number % 10对于digitNames字典来说总是一个有效的下标key值,因此使用感叹号从可选返回值类型中强制取值。

digitNames字典中获取的字符串被添加在output前面,有效的根据数字逆序来构建字符串。(表达式number % 10为16得到6,为58得到8,为510得到0。)

然后number除以10,因为它是一个整数,所以对结果进行向下取整,所以16得到1,58得到5,510得到51。

这个过程一直重复直到number等于0,此时闭包将output字符串输出,并通过map(_:)方法添加到output输出数组中。

上面栗子中尾随闭包的语法将闭包的具体功能整齐的封装在函数之后,不需要将完整的闭包包裹在map(_:)方法的括号中。


【关键知识点】

  1. 尾随闭包的定义。当闭包作为函数的最后一个参数时,将闭包写在函数调用的括号之后,不用写闭包在函数中的参数名。
  2. 特别地,当函数参数只有一个闭包时,可以省略函数调用的括号。

捕获值(Capturing Values)

一个闭包可以在它定义的上下文环境中捕获常量和变量。即使定义这些常量和变量的原作用域已经不存在,在闭包内部仍然持有这些常量和变量的引用,并能修改它们。

在Swift中,能够捕获值的最简单的闭包形式就是内嵌函数,即写在另一个函数内部。内嵌函数可以捕获外部函数的参数和外部函数中定义的常量和变量。

这有一个命名为makeIncrementer的函数,它的内部有一个名叫incrementer的内嵌函数。内嵌函数incrementer从它的上下文环境中捕获了两个值,runningTotalamount。在捕获这些值后,makeIncrementerincrementer作为闭包返回,每次调用这个闭包时,就会在runningTotal的基础上增加amount

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

makeIncrementer的返回类型是() -> Int,意味着它返回的不是一个简单值,而是一个函数。返回的函数没有参数,在调用时会返回一个’Int’值。想要了解更多关于函数如何返回另一函数,请参考函数类型作为返回类型

makeIncrementer(forIncrement:)函数定义了一个叫runningTotal的整型变量,用来存储最终将要返回的不断增加的总值。该变量的初始值为0。

makeIncrementer(forIncrement:)函数有一个外部参数名叫forIncrement,内部参数名叫acountInt参数。当返回函数incrementer被调用时,传入的参数指定了runningTotal的增加值。makeIncrementer函数定义了一个叫做incrementer的内嵌函数,用来进行实际的增加动作。这个函数简单的将amount增加到runningTotal上,并返回结果。

当单独看这个内嵌函数时,会发现它有些不同寻常:

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

incrementer函数没有任何参数,在它内部也没有任何runningTotalamount的引用,而是在外部函数中捕获了runningTotalamount并在内嵌函数内部使用。通过引用捕获runningTotalamount确保了在makeIncrementer调用结束时它们不会消失,也确保了在下一次调用incrementerrunningTotal依然是可用的。

NOTE
作为一个优化,当闭包不会改变捕获值时,Swift可能会存储一个值拷贝,而不是去捕获引用。

这有一个makeIncrementer运行的栗子:

let incrementByTen = makeIncrementer(forIncrementer: 10)

这个栗子定义了一个常量incrementByTen,指向了每调用一次,就会在它的runningTotal变量上增加10的函数。调用该函数多次得出以下结果:

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

当你创建第二个incrementer,它会有一个新的,独立的runningTotal变量的引用:

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

再次调用之前的incrementByTen会在它自己的runningTotal上继续增加,并不会影响incrementBySeven捕获 的变量值:

incrementByTen()
// returns a value of 40

NOTE
如果将闭包赋值给类的成员变量,闭包将会通过引用实例类的其他成员来捕获这个类实例,这样将会在闭包和类实例之间建立强引用循环关系。Swift使用捕获列表来打断这样的强引用循环。如果需要了解更多,参考闭包的强引用循环

闭包是引用类型(Closures Are Reference Types)

在上面的栗子中,incrementBySevenincrementByTen都是常量,但它们所指向的闭包仍然可以增加所捕获得变量runningTotal值。这是因为函数和闭包是引用类型。

无论何时你将一个函数或者闭包赋值给一个常量或变量,你真正做的其实是将该常量或变量设置成对函数或闭包的引用。在上面的栗子中,incrementByTen指向的闭包是一个常量,而不是闭包本身里面的内容是常量。

也就是说,当你将一个闭包分别赋值给两个不同的常量或变量时,它们所指向的将是同一个闭包。

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

【重要知识点】

  1. 捕获值含义。在闭包定义的上下文环境中捕获常量和变量的引用,在闭包内部使用,最简单的形式就是内嵌函数,捕获值不受原作用域消失与否的影响。
  2. 闭包是一种引用类型,当闭包被赋值给某个常量或变量时,该常量或变量将唯一的拥有该闭包所捕获的常量和变量的引用,可以唯一的,持续的对捕获值进行操作。

逃逸闭包

当一个闭包作为参数传入一个函数,但是这个闭包在函数返回之后才被调用,我们就说这个闭包从函数中逃逸了。当你声明了一个参数中有闭包的函数时,你可以在参数类型前写上@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"

【重要知识点】
1. 逃逸闭包含义。作为参数传递给函数的闭包在函数返回之后的某个时刻才被执行,此时必须在函数声明时,给闭包类型添加关键字@escaping,否则会报编译时错误。
2. 逃逸闭包的一种形式是将闭包保存在函数外部定义的变量中,在以后某个时刻进行调用。逃逸闭包可于函数中异步任务的回调。
3. 逃逸闭包内部必须显示的调用self.来调用它所捕获的值(个人理解是这样的),否则会报编译时错误。非逃逸闭包可以隐式的调用self.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值