本章的重点三个关键字: 函数, 闭包,闭包表达式
首先来看一个简单的函数: func函数名(参数列表) ->返回类型 { 函数体 }
func greet(person: String, day: String) -> String {
return "Hello \(person), today is \(day)."
}
greet(person: "Bob", day: "Tuesday")
要理解 Swift 中的函数(Functions)和闭包(Closures),你需要切实弄明白三件事情,我们把这三件事按照重要程度进 行了大致排序:
1. 函数可以被赋值给变量,也可以作为另一个函数的输入参数,或者另一个函数的返回值来使用。
2. 函数能够捕获存在于其局部作用域之外的变量。
3. 有两种方法可以创建函数,一种是使用 func 关键字,另一种是 { }。在 Swift 中,后一种被称为闭包表达式。
有时候,新接触闭包的人会认为上面这三点的重要顺序是反过来的,并且会忽略其中的某一点, 或把闭包和闭包表达式混为一谈。尽管这些概念确实容易引起困惑,但这三点却是鼎足而立, 互为补充的,如果你忽视其中任何一条,终究会在函数的应用上,狠狠地摔上一跤。
1. 函数可以被赋值给变量,也能够作为函数的输入和输出
让我们从一个简单的函数开始,它会打印一个整数:
func printInt(i: Int) {
print("You passed \(i).")
}
printInt(i: 1) //You passed 1.
将函数赋值给一个变量 :
let funVar = printInt
funVar(2) //You passed 2.
这里值得注意的是,我们不能在 funVar 调用时包含参数标签,而在 printInt 的调用 (像是 printInt(i: 2)) 却要求有参数标签。Swift 只允许在函数声明中包含标签,这些标签不是函数类 型的一部分。也就是说,现在你不能将参数标签赋值给一个类型是函数的变量,不过这在未来 的 Swift 版本中可能会有改变。
我们也可以试试 在funcVar 包含参数标签:
funVar(i: 3) // Extraneous argument label 'i:' in call
我们也能够写出一个接受函数作为参数的函数:
func useFunction(function: (Int) -> () ) {
function(3)
}
useFunction(function: printInt)
// You passed 3.
useFunction(function: funVar) // You passed 3.
为什么函数可以作为变量使用的这种能力如此关键呢?因为它让你很容易写出 “高阶” 函数,高 阶函数将函数作为参数的能力使得它们在很多方面都非常有用,我们已经在内建集合类型中看 到过它的威力了。
函数也可以返回其他函数:
func returnFunc() -> (Int) -> String {
func innerFunc(i: Int) -> String {
return "you passed \(i)"
}
return innerFunc
}
returnFunc()(3)//you passed 3
let myFunc = returnFunc()
myFunc(3) // you passed 3
2. 函数可以捕获存在于它们作用域之外的变量 当函数引用了在其作用域之外的变量时,这个变量就被捕获了,它们将会继续存在,而不是在 超过作用域后被摧毁。
为了研究这一点,让我们修改一下 returnFunc 函数。这次我们添加一个计数器,每次调用这个 函数时,计数器将会增加:
func counterFunc() -> (Int) -> String {
var counter = 0
func innerFunc(i: Int) -> String {
counter += i // counter 被捕获
return "Running total: \(counter)”
}
return innerFunc
}
一般来说,因为 counter 是 counterFunc 的局部变量,它在 return 语句执行之后就应该离开作 用域并被摧毁。但因为 innerFunc 捕获了它,所以 Swift 运行时将一直保证它的存在,直到捕 获它的函数被销毁为止。我们可以多次调用 innerFunc,并且看到 running total 的输出在增加:
let f = counterFunc()
f(3)// Running total: 3
f(4) // Running total: 7
如果我们再次调用 counterFunc() 函数,将会生成并捕获一个新的 counter 变量:
let g = counterFunc()
g(2) // Running total: 2
g(2) // Running total: 4
这并不影响我们的第一个函数,它拥有属于自己的 counter:
f(2)// Running total: 9
你可以将这些函数以及它们所捕获的变量想象为一个类的实例,这个类拥有一个单一的方法 (也就是这里的函数) 以及一些成员变量 (这里的被捕获的变量)。
在编程术语里,一个函数和它所捕获的变量环境组合起来被称为闭包。上面 f 和 g 都是闭包的 例子,因为它们捕获并使用了一个在它们作用域之外声明的非局部变量 counter。
3. 函数可以使用{ } 来声明为闭包表达式
在 Swift 中,定义函数的方法有两种。下面这个简单的函数将会把数字翻倍:
一种是使用 func 关键字。
func doubler(i: Int) -> Int {
return i*2
}
[1, 2, 3, 4].map(doubler)
// [2, 4, 6, 8]
另一种方法是使用闭包表达式 {}。像之前那样将它传给 map:
let doublerAlt = { (i: Int) -> Int in return i*2 } // {(参数列表) -> 返回类型 in 函数体 }
[1, 2, 3, 4].map(doublerAlt) // [2, 4, 6, 8]
Note: 使用闭包表达式来定义的函数可以被想成函数的字面量 (function literals)
{} 与 func 相比,{} 的区别: 闭包表达式是匿名的,它们没 有被赋予一个名字
{} 有以下三种使用方法,
1.它们被创建时将其赋值给一个变量 (就像我们这 里对 doubler > 进行的赋值一样)
2. 或者是将它们传递给另一个函数或方法
3.你可以在定义一个表达式的同时,对它进行 调用。这个方法在定义那些初始化时代码多于一行的属性时会很有用 (我们将在下面 的延迟属性部分看到一个例子)
使用闭包表达式 {} 声明的 doubler,和之前使用 func 关键字声明的函数,除了在参数标签上的处 理上略有不同以外,其实是完全等价的。它们甚至存在于同一个 “命名空间” 中,这一点和有些 编程语言有所不同。
{} 与 func 相比, 优势: 闭包表达式可以简洁得多,特别 是在像是 map 这样的将一个快速实现的函数传递给另一个函数时,这个特点更为明显
我们将 doubler map 的例子用更短的形式进行了重写:
[1, 2, 3].map { $0 * 2 } // [2, 4, 6]
之所以看起来和原来很不同,是因为这里使用了
Swift 中的一些可以让代码更加简洁的特性:
1.如果你将闭包作为参数传递,并且你不再用这个闭包做其他事情的话,就没有必要先将 它存储到一个局部变量中。可以想象一下比如 5*i 这样的数值表达式,你可以把它直接 传递给一个接受 Int 的函数,而不必先将它计算并存储到变量里。
[1, 2, 3].map( { (i: Int) -> Int in return i * 2 } )
2.如果编译器可以从上下文中推断出类型的话,你就不需要指明它了。在我们的例子中, 从数组元素的类型可以推断出传递给 map 的函数接受 Int 作为参数,从闭包内的乘法结 果的类型可以推断出闭包返回的也是 Int。
[1, 2, 3].map( { i in return i * 2 } )
3.如果闭包表达式的主体部分只包括一个单一的表达式的话,它将自动返回这个表达式的 结果,你可以不写 return。
[1, 2, 3].map( { i in i * 2 } )
4.Swift 会自动为函数的参数提供简写形式,$0 代表第一个参数,$1 代表第二个参数,以 此类推。
[1, 2, 3].map( { $0 * 2 } )
5.如果函数的最后一个参数是闭包表达式的话,你可以将这个闭包表达式移到函数调用的 圆括号的外部。这样的尾随闭包语法(trailing closure syntax) 在多行的闭包表达式中 表现非常好,因为它看起来更接近于装配了一个普通的函数定义,或者是像 if (expr) { } 这样的执行块的表达形式。
[1, 2, 3].map() { $0 * 2 }
6.最后,如果一个函数除了闭包表达式外没有别的参数,那么调用的时候在方法名后面的 圆括号也可以一并省略。
[1,2,3].map{$0*2}
总结: 使用 { } 创建函数 配合 Swift 的6个 简洁的特性, 可以写出精简的函数表达。一旦你习惯了这样的语法以及函数式编程风格的话,它们很快就会看 起来很自然,移除这些杂乱的表达,可以让你对代码实际做的事情看得更加清晰, 你一定会为 语言中有这样的特性而心存感激。一旦你习惯了阅读这样的代码,你一眼就能看出这段代码做 了什么,而想在一个等效的 for 循环中做到这一点则要困难得多。
小白福利:
如果你还不够熟悉使用 { } +Swift 的6个 简洁的特性。你在尝试提供闭包表达式时遇到一些谜一样的错误的话,该如何定位问题呢?
方法:
将 闭包表达式写成上面例子中的第一种包括类型的完整形式,这有助于理清错误到底在哪儿。一旦完整版本可以编译通过,你就可以按6个 简洁的特性逐渐将类型移除,直到 编译无法通过, 就知道错误在哪儿了。如果造成错误的是你的代码的话,在这个过程中相信你已经修复好这些代码了。
还有一些时候,Swift 会要求你用更明确的方式进行调用。
例子:你要得到一个随机数数组,一种 快速的方法就是通过 Range.map 方法,并在 map 的函数中生成并返回随机数。
(0..<3).map { _ in Int.random(in: 1..<100) } // [53, 63, 88]
这里,无论如 何你都要为 map 的函数提供一个参数。或者明确使用 _ 告诉编译器你承认这里有一个参数,但 并不关心它究竟是什么:
当你需要显式地指定变量类型时,你不一定要在闭包表达式内部来设定。比如,让我们来定义 一个 isEven,它不指定任何类型:
let isEven = { $0 % 2 == 0 }
在上面,isEven 被推断为 Int -> Bool。这和 let i = 1 被推断为 Int 是一个道理,因为 Int 是整数 字面量的默认类型
Integer 字面量协议
这是因为标准库中的 IntegerLiteralType 有一个类型别名:
protocol ExpressibleByIntegerLiteral {
associatedtype IntegerLiteralType
/// 用`value` 创建一个实例。
init(integerLiteral value: Self.IntegerLiteralType)
}
/// 一个没有其余类型限制的整数字面量的默认类型。 typealias IntegerLiteralType = Int
如果你想要定义你自己的类型别名,你可以重写默认值来改变这一行为:
typealias IntegerLiteralType = UInt32
let i = 1 // i 的类型为UInt32.
显然,这不是一个什么好主意。
不过,如果你需要 isEven 是别的类型的话,
也可以在闭包表达式中为参数和返回值指定类型:
let isEvenAlt = { (i: Int8) -> Bool in i % 2 == 0 }
你也可以在闭包外部的上下文里提供这些信息:
let isEvenAlt2: (Int8) -> Bool = { $0 % 2 == 0 }
let isEvenAlt3 = { $0 % 2 == 0 } as (Int8) -> Bool
因为闭包表达式最常见的使用情景就是在一些已经存在输入或者输出类型的上下文中,所以这 种写法并不是经常需要,不过知道它还是会很有用。
当然了,如果能定义一个对所有整数类型都适用的 isEven 的泛用版本的计算属性会更好:
extension BinaryInteger {
var isEven: Bool { return self % 2 == 0 }
}
或者,我们也可以选择为所有的 Integer 类型定义一个全局函数:
func isEven
要把这个全局函数赋值给变量的话,你需要先决定它的参数类型。变量不能持有泛型函数,它 只能持有一个类型具体化之后的版本:
let int8isEven: (Int8) -> Bool = isEven
最后要说明的是关于命名的问题。要清楚,那些使用 func 声明的函数也可以是闭包,就和用 { } 声明的是一样的。记住,闭包指的是一个函数以及被它所捕获的所有变量的组合。而使用 { } 来 创建的函数被称为闭包表达式,人们常常会把这种语法简单地叫做闭包。但是不要因此就认为 使用闭包表达式语法声明的函数和其他方法声明的函数有什么不同。它们都是一样的,它们都 是函数,也都可以是闭包。