当编写代码在两个数字之间进行插值时,很容易默认为线性插值。然而,在两个值之间平稳过渡通常会更好。所以我的建议是避免步进,并使用函数(如smooterstep()
)进行插值:
func smootherStep(value: CGFloat) -> CGFloat {
let x = value < 0 ? 0 : value > 1 ? 1 : value
return ((x) * (x) * (x) * ((x) * ((x) * 6 - 15) + 10))
}
—— Simon Gladman (@flexmonkey), Swift 版 Core Image 的作者
可变参数函数(Variadic functions)
可变参数函数是具有不确定性的函数,这是一种奇特的说法,就是说它们接受的参数和发送的参数一样多。在一些基本函数(甚至print()
)中都使用了这种方法,以使代码编写更简单、更安全。
让我们使用print()
,因为它是一个你很熟悉的函数。你习惯看到这样的代码:
print("I'm Commander Shepard and this is my favorite book")
但是print()
是一个可变参数函数,这意味着您可以传递任意数量的要打印的内容:
print(1, 2, 3, 4, 5, 6)
这将与为每个数字调用一次print()
产生不同的输出:使用一次性调用将在一行中打印所有数字,而使用多次调用将逐行打印数字。
一旦添加了可选的额外参数:separator
和terminator
,print()
的可变参数特性将变得更加有用。第一个参数在传递的每个值之间放置一个字符串,第二个参数在打印完所有值后放置一个字符串。例如,这将打印 “1、2、3、4、5、6 ! ”:
print(1, 2, 3, 4, 5, 6, separator: ", ", terminator: "!")
这就是如何调用可变参数函数。现在我们来谈谈如何制作它们,我认为你会发现这在 Swift 中相当巧妙。
考虑下面的代码:
func add(numbers: [Int]) -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
add(numbers: [1, 2, 3, 4, 5])
该函数接受一个整数数组,然后将每个数字相加,得到一个总数。有更有效的方法可以做到这一点,但这不是本章的重点!
要使该函数拥有可变参数,即它接受任何数量的单个整数而不是单个数组,需要进行两次更改。首先,我们需要将参数写成Int...
,而不是写成[int]
。其次,我们不需要这样调用add(numbers: [1, 2, 3, 4, 5])
,而是应该这样调用add(numbers: 1, 2, 3, 4, 5)
。
就是这样。最后的代码是这样的:
func add(numbers: Int...) -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
add(numbers: 1, 2, 3, 4, 5)
你可以将可变参数放在函数的参数列表中的任何位置,但是每个函数只能有一个可变参数。
操作符重载(Operator overloading)
这是一个人们既爱又恨的话题。操作符重载是实现你自己的操作符甚至调整现有操作符(如+
或 *
)的能力。
使用操作符重载的主要原因是它提供了非常清晰、自然和富有表现力的代码。你已经理解了 5 + 5 = 10 ,因为你了解基础数学,所以允许 myShoppingList + yourShoppingList 是一个逻辑扩展,即将两个自定义结构相加。
操作符重载有几个缺点。首先,它的含义可能是不透明的:如果我说henrytheeight + AnneBoleyn
,结果是一对幸福的夫妇(暂时!)、一个未来伊丽莎白女王( Queen Elizabeth )形状的婴儿,还是某个四肢相连的人类?
其次,它没有做任何方法不能做的事情:HenryTheEighth.marry(AnneBoleyn)
也会有同样的结果,而且明显更清晰。第三,它隐藏了复杂性:5 + 5 是一个微不足道的操作,但是 Person + Person 可能涉及到安排一个仪式、找到一件婚纱等等。
第四,可能也是最严重的,操作符重载可能会产生意想不到的结果,特别是因为你可以不受惩罚地调整现有的操作符。
基础操作符(The basics of operators)
为了演示操作符重载是多么令人困惑,我想先给出一个重载==
操作符的基本示例。考虑以下代码:
if 4 == 4 {
print("Match!")
} else {
print("No match!")
}
就像你想得那样,它会打印 “Match! ” 因为 4 总是等于 4。还是……?
进入操作符重载。只需三行代码,我们就可以对几乎所有应用程序造成严重损害:
func ==(lhs: Int, rhs: Int) -> Bool {
return false
}
if 4 == 4 {
print("Match!")
} else {
print("No match!")
}
当代码运行时,它将输出 “ No match ! ”,因为我们重载了==
操作符,所以它总是返回false
。正如你所看到的,函数的名称是操作符本身,即func ==
,所以你要修改的内容非常清楚。你还可以看到,这个函数期望接收两个整数(左边和右边分别是lhs
和rhs
),并返回一个布尔值,该值报告这两个数字是否相等。
除了完成实际工作的函数外,操作符还具有优先级和关联性,这两者都会影响操作的结果。当多个运算符一起使用而没有括号时,Swift 首先使用优先级最高的运算符——你可能学习过PEMDAS(括号、指数、乘除、加减)、BODMAS 或类似的运算符,取决于你在哪里上学。如果仅凭优先级不足以决定操作的顺序,则使用结合律。
Swift 允许你控制优先级和关联性。现在让我们尝试一个实验:下面操作的结果是什么?
let i = 5 * 10 + 1
根据 PEMDAS ,应该首先执行乘法(5 * 10 = 50 ),然后执行加法(50 + 1 = 51 ),因此结果是 51 。这个优先级被直接写入了 Swift ——以下是来自 Swift 标准库的确切代码:
precedencegroup AdditionPrecedence {
associativity: left
higherThan: RangeFormationPrecedence
}
precedencegroup MultiplicationPrecedence {
associativity: left
higherThan: AdditionPrecedence
}
infix operator * : MultiplicationPrecedence
infix operator + : AdditionPrecedence
infix operator - : AdditionPrecedence
这将声明两个操作符优先组,然后声明 *
、+
和 -
操作符位于这些组中。你可以看到,MultiplicationPrecedence
被标记为高于AdditionPrecedence
,这就是 *
在 +
之前被计算的原因。
这三个操作符被称为中缀操作符,因为它们被放在两个操作数中,即 5 + 5 ,而不是像!
这样的前缀操作符,例如:!loggedIn
。
Swift 允许我们通过将现有操作符分配给新的组来重新定义它们的优先级。如果需要,可以创建自己的优先组,或者重用现有的优先组。
在上面的代码中,你可以看到顺序是乘法优先级(用于*
、/
、%
和更多),然后是加法优先级(用于+
、-
、|
和更多),然后是范围优先级(用于...
和 ..<
。)
在我们的小算术中,我们可以通过像这样重写*
运算符来引起各种奇怪的行为:
infix operator * : RangeFormationPrecedence
这就重新定义了*
的优先级比+
低,这意味着这段代码现在将返回55
:
let i = 5 * 10 + 1
这是与之前相同的代码行,但现在将执行加法(10 + 1 = 11),然后乘法(5 * 11) 得到 55。
当两个操作符具有相同的优先级时,就会发挥结合律的作用。例如,考虑以下问题:
let i = 10 - 5 - 1
再看看 Swift 自己的代码是如何声明 AdditionPrecedence 组的,-
运算符属于这个组:
precedencegroup AdditionPrecedence {
associativity: left
higherThan: RangeFormationPrecedence
}
如你所见,它被定义为具有左结合性,这意味着 10 - 5 - 1 被执行为 (10 - 5) - 1,而不是 10 - (5 - 1)。
这种差别很细微,但很重要:除非我们改变它,否则 10 - 5 - 1 将得到 4 。当然,如果你想造成一点破坏,你可以这样做:
precedencegroup AdditionPrecedence {
associativity: right
higherThan: RangeFormationPrecedence
}
infix operator - : AdditionPrecedence
let i = 10 - 5 - 1
这将修改现有的加法优先组,然后随着改变被更新,式子将被解释为 10 - (5 - 1),即结果等于 6。
添加到现有操作符(Adding to an existing operator)
现在你已经了解了操作符的工作原理,让我们修改*
操作符,使它可以像这样对整数数组进行乘法操作:
let result = [1, 2, 3] * [1, 2, 3]
完成之后,将返回一个包含[1,4,9]
的新数组,即1x1
, 2x2
和3x3
。
*
操作符已经存在,所以我们不需要声明它。相反,我们只需要创建一个新的func *
,它接受我们的新数据类型。这个函数将创建一个新数组,该数组由所提供的两个数组中的每一项相乘组成。这是代码:
func *(lhs: [Int], rhs: [Int]) -> [Int] {
guard lhs.count == rhs.count else { return lhs }
var result = [Int]()
for (index, int) in lhs.enumerated() {
result.append(int * rhs[index])
}
return result
}
注意,我在开头添加了一个guard
,以确保两个数组包含相同数量的项。
因为*
操作符已经存在,所以重要的是lhs
和rhs
参数,它们都是整数数组:当两个整数数组相乘时,这些参数确保选择这个新函数。
添加一个新的操作符(Adding a new operator)
当你添加一个新的操作符时,你需要提供足够的 Swift 信息来使用它。至少需要指定新操作符的位置(前缀、后缀或中缀),但如果不指定优先级或关联性 Swift 将提供默认值,使其成为低优先级、非关联操作符。
让我们添加一个新的操作符**
,它返回一个值的幂。也就是说,2 ** 4 应该等于 2 * 2 * 2 * 2 ,即 16。我们将使用pow()
函数,所以你需要导入Foundation
框架:
import Foundation
一旦完成,我们需要告诉 Swift **
将是一个中缀操作符,因为我们将在其左侧有一个操作数,在其右侧有另一个操作数:
infix operator **
它没有指定优先级或关联,因此将使用默认值。
最后,新的**
函数本身。我已经让它接受双精度值以获得最大的灵活性,Swift
足够聪明,当与这个操作符一起使用时,可以推断2
和4
是双精度值:
func **(lhs: Double, rhs: Double) -> Double {
return pow(lhs, rhs)
}
如你所见,由于pow()
,函数本身非常简单。自己试试:
let result = 2 ** 4
到目前为止,一切顺利。然而,像这样的表达是行不通的:
let result = 4 ** 3 ** 2
事实上,甚至像这样的东西也不会奏效:
let result = 2 ** 3 + 2
这是因为我们使用的是默认优先级和结合性。为了解决这个问题,我们需要决定与其他操作符相比**
应该排在什么位置,为此,你可以返回到 PEMDAS (它是 E !),或者查看其他语言的功能。例如,Haskell 把它放在乘法和除法之前,在PEMDAS 之后。Haskell还声明幂运算右结合性,这意味着 4 ** 3 ** 2 将被解析为 *4 *(3 ** 2) 。
我们可以使我们自己的**
操作符的行为相同的方式,修改其声明如下:
precedencegroup ExponentiationPrecedence {
higherThan: MultiplicationPrecedence
associativity: right
}
infix operator **: ExponentiationPrecedence
有了这个更改,你现在可以在同一个表达式中使用**
两次,还可以将它与其他操作符组合使用——这样做会更好!
修改现有的操作符(Modifying an existing operator)
现在来看一些更复杂的东西:修改现有的操作符。我选择了一个稍微复杂一点的例子,因为如果你能看到我在这里解决它,我希望它能帮助你解决你自己的操作符重载问题。
我要修改的运算符是...
,它已经作为闭区间运算符存在。所以,你可以写1...10
,然后得到覆盖 1 到10 的范围 。在默认情况下这是一个中缀操作符,范围的低端在左侧,高端在右侧,但我要修改它,以便它还接受左侧的范围和右侧的另一个整数,如下所示:
let range = 1...10...1
当代码运行时,它将返回一个数组,其中包含数字1、2、3、4、5、6、6、7、8、9、10、9、8、7、6、5、4、3、2、1
——它先递增再递减。这是可能的,因为运算符出现了两次:第一次它将看到1...10
,这是一个闭合范围运算符,第二次它将看到CountableClosedRange<Int>...1
,这将是我们的新操作。在此函数中,CountableClosedRange<Int>
是左侧操作数,而Int 1
是右侧操作数。
新...
函数需要做两件事:
- 计算一个新的区间,从右边的整数到左边区间的最高点,然后反转这个区间。
- 将左边的区间追加到新创建的递减区间,并作为函数的结果返回该区间。
在代码中,它看起来是这样的:
func ...(lhs: CountableClosedRange<Int>, rhs: Int) -> [Int] {
let downwards = (rhs ..< lhs.upperBound).reversed()
return Array(lhs) + downwards
}
如果你尝试使用该代码,你将看到它无法工作—至少目前还不能。要知道为什么,看看Swift对...
操作符的定义:
infix operator ... : RangeFormationPrecedence
precedencegroup RangeFormationPrecedence {
higherThan: CastingPrecedence
}
现在再来看看我们的代码:
let range = 1...10...1
你可以看到我们用到了...
操作符两次,这意味着 Swift 需要知道我们想要(1...10)...1
还是1...(10...1)
。正如你在上面看到的,Swift 的定义的...
操作符没有提到它的结合律,所以 Swift 不知道在这种情况下该怎么做。所以,就目前情况来看,我们的新操作符只能处理这样的代码:
let range = (1...10)...1
如果我们想要相同的行为而不需要用户添加括号,我们需要告诉 Swift ...
操作符有左结合性,像这样:
precedencegroup RangeFormationPrecedence {
associativity: left
higherThan: CastingPrecedence
}
infix operator ... : RangeFormationPrecedence
就是这样:现在代码在没有括号的情况下可以正常工作,并且我们有了一个有用的新操作符。不要忘记,在 Playground ,你的代码顺序很重要——你的最终代码应该是这样的:
precedencegroup RangeFormationPrecedence {
associativity: left
higherThan: CastingPrecedence
}
infix operator ... : RangeFormationPrecedence
func ...(lhs: CountableClosedRange<Int>, rhs: Int) -> [Int] {
let downwards = (rhs ..< lhs.upperBound).reversed()
return Array(lhs) + downwards
}
let range = 1...10...1
print(range)
闭包(Closures)
和元组一样,闭包在 Swift 中是特有的:全局函数是闭包,嵌套函数是闭包,sort()
和map()
等函数方法接受闭包,惰性属性使用闭包,这只是冰山一角。在你的 Swift 开发职业生涯中,你将需要使用闭包,如果你想晋升到高级开发职位,那么你也需要轻松地创建闭包。
我知道有些人对闭包有不同寻常的理解,所以让我们从一个简单的定义开始:闭包是一段代码,可以像变量一样传递和存储,它还能够捕获它使用的任何值。这种捕获确实使闭包难以理解,所以我们稍后再讨论它。
创建简单的闭包(Creating simple closures)
让我们创建一个简单的闭包来让事情运行起来:
let greetPerson = {
print("Hello there!")
}
它创建一个名为greetPerson
的闭包,然后可以像函数一样使用:
greetPerson()
因为闭包是第一类数据类型——也就是说,就像整数、字符串和其他类型一样——所以你可以复制它们并将它们用作其他函数的参数。以下是实际复制:
let greetCopy = greetPerson
greetCopy()
复制闭包时,请记住闭包是引用类型——这两个“副本”实际上指向同一个共享闭包。
要将闭包作为参数传递给函数,请指定闭包自己的参数列表并将返回值作为其数据类型。也就是说,你不需要编写param: String
,而是编写类似param: () -> Void
这样的东西来接受没有参数且没有返回值的闭包。是的,-> Void
是必需的,否则param:()
将意味着一个空元组。
如果我们想将greetPerson
闭包传递给一个函数并在那里调用它,我们将使用如下代码:
func runSomeClosure(_ closure: () -> Void) {
closure()
}
runSomeClosure(greetPerson)
为什么需要闭包?在那个例子中不是,但是如果我们想在 5 秒后调用闭包呢?或者我们只是想偶尔调用它?或者是否满足某些条件?这就是闭包变得有用的地方:它们是一些功能,你的应用程序可以将它们存储起来,以便以后需要时使用。
闭包开始变得混乱的地方是当它们接受自己的参数时,部分原因是它们的参数列表放在一个不寻常的位置,还因为这些闭包的类型语法可能看起来非常混乱!
首先:如何使闭包接受参数。要做到这一点,请在闭包的括号内写入参数列表,然后输入关键字in
:
let greetPerson = { (name: String) in
print("Hello, \(name)!")
}
greetPerson("Taylor")
如果需要,还可以在这里指定捕获列表。这是最常用的,以避免self
引用循环,通过使它unowned
,像这样的:
let greetPerson = { (name: String) [unowned self] in
print("Hello, \(name)!")
}
greetPerson("Taylor")
现在,讨论如何使用闭包将参数传递给函数。这很复杂,有两个原因:1)它可能看起来像一个冒号和括号的海洋,2)调用约定根据你做的事情而变化。
让我们回到runSomeClosure()
函数。为了让它接受一个参数——一个本身接受一个参数的闭包——我们需要这样定义它:
func runSomeClosure(_ closure: (String) -> Void)
闭包是一个函数,它接受一个字符串,但什么也不返回。这是一个新的功能:
let greetPerson = { (name: String) in
print("Hello, \(name)!")
}
func runSomeClosure(_ closure: (String) -> Void) {
closure("Taylor")
}
runSomeClosure(greetPerson)
闭包捕获(Closure capturing)
我已经讨论了闭包是如何作为引用类型的,它对捕获的值有巧妙的含义:当两个变量指向同一个闭包时,它们都使用相同的捕获数据。
让我们从基础开始:当一个闭包引用一个值时,它需要确保该值在运行闭包时仍然存在。这看起来像是闭包在复制数据,但实际上它比这更微妙。这个过程称为捕获,它允许闭包引用和修改它引用的值,即使原始值不再存在。
区别很重要:如果闭包复制了它的值,那么就会应用值类型语义,并且闭包内的值类型的任何更改都将发生在一个惟一的副本上,不会影响原来的调用方。相反,闭包捕获数据。
我知道这一切听起来都是假设,所以让我给你一个实际的例子:
func testCapture() -> () -> Void {
var counter = 0
return {
counter += 1
print("Counter is now \(counter)")
}
}
let greetPerson = testCapture()
greetPerson()
greetPerson()
greetPerson()
let greetCopy = greetPerson
greetCopy()
greetPerson()
greetCopy()
这段代码声明了一个名为testCapture()
的函数,该函数的返回值为()-> Void
,即它返回一个不接受任何参数且什么也不返回的函数。在testCapture()
中,我创建了一个名为counter
的新变量,初始值为0
。但是,函数内的变量没有发生任何变化。相反,它返回一个闭包,该闭包将counter
加 1 并打印出它的新值。它不调用那个闭包,它只返回它。
有趣的地方是函数之后:greetPerson
被设置为testCapture()
返回的函数,它被调用了三次。该闭包引用了在testCapture()
中创建的counter
值,现在显然超出了范围,因为该函数已经完成。因此,Swift 捕捉到了这个值:这个闭包现在有了自己对counter
的独立引用,可以在调用它时使用。每次调用greetPerson()
函数时,你将看到counter
加 1 。
让事情变得加倍有趣的是greetCopy
。这就是我所说的闭包是引用,并且使用相同的捕获数据。当调用greetCopy()
时,它将增加与greetPerson
相同的counter
值,因为它们都指向相同的捕获数据。这意味着在一次又一次地调用闭包时counter
值将从 1 增加到 6。这个怪癖我已经讲过两次了,所以如果它伤害了你的大脑,不要担心:它不会再被覆盖了!
闭包简写语法(Closure shorthand syntax)
在讨论更高级的内容之前,我想快速地全面介绍一下闭包简写语法,这样我们就完全处于同一种思路。当你把一个内联闭包传递给一个函数时,Swift 有几种技术,所以你不需要写太多的代码。
为了给你提供一个好例子,我将使用数组的filter()
方法,它接受一个带有一个字符串参数的闭包,如果该字符串应该在一个新的数组中,则返回true
。下面的代码过滤一个数组,这样我们就得到了一个新的数组,每个人的名字都以Michael
开头:
let names = ["Michael Jackson", "Taylor Swift", "Michael Caine", "Adele Adkins", "Michael Jordan"]
let result1 = names.filter({ (name: String) -> Bool in
if name.hasPrefix("Michael") {
return true
} else {
return false
}
})
print(result1.count)
从中可以看出filter()
希望接收一个闭包,该闭包接受一个名为name
的字符串参数,并返回true
或false
。然后闭包检查名称是否具有前缀 “ Michael ” 并返回一个值。
Swift 知道传递给filter()
的闭包必须接受一个字符串并返回一个布尔值,所以我们可以删除它,只使用一个变量的名称,该变量将用于对每个条目进行过滤:
let result2 = names.filter({ name in
if name.hasPrefix("Michael") {
return true
} else {
return false
}
})
接下来,我们可以直接返回hasPrefix()
的结果,如下:
let result3 = names.filter({ name in
return name.hasPrefix("Michael")
})
尾随闭包允许我们删除一组括号,这总是受欢迎的:
let result4 = names.filter { name in
return name.hasPrefix("Michael")
}
因为我们的闭包只有一个表达式——即现在我们已经删除了很多代码,它只做一件事——我们甚至不再需要return
关键字。Swift 知道我们的闭包必须返回一个布尔值,因为我们只有一行代码,Swift 知道它必须是返回值的那一行。代码现在看起来是这样的:
let result4 = names.filter { name in
return name.hasPrefix("Michael")
}
许多人在此止步,理由很充分:下一步开始可能会相当混乱。你看,当这个闭包被调用时,Swift 会自动创建匿名参数名,这些匿名参数名由一个美元符号和一个从 0 开始计数的数字组成。$0, $1, $2,以此类推。你不允许在自己的代码中使用这样的名称,所以这些名称很容易脱颖而出!
这些简写参数名映射到闭包接受的参数。在本例中,这意味着name
可用为$0
。不能混合显式参数和匿名参数:要么声明入参列表,要么使用 $0 系列。这两者做的是完全一样的:
let result6 = names.filter { name in
name.hasPrefix("Michael")
}
let result7 = names.filter {
$0.hasPrefix("Michael")
}
注意到在使用匿名时必须删除name in
部分吗?是的,这意味着更少的输入,但同时你也放弃了一点可读性。我喜欢在我自己的代码中使用简写名称,但是只有在需要时才应该使用它们。
如果你选择使用简写名称,通常会将整个方法调用放在一行上,如下所示:
let result8 = names.filter { $0.hasPrefix("Michael") }
当你将其与原始闭包的大小进行比较时,你必须承认这是一个很大的改进!
函数作为闭包(Functions as closures)
Swift 确实模糊了函数、方法、操作符和闭包之间的界限,这非常棒,因为它向你隐藏了所有编译器的复杂性,并让开发人员做我们最擅长的事情:制作出色的应用程序。这种模糊的行为一开始很难理解,在日常编码中更难使用,但是我想向你展示两个示例,我希望它们能展示 Swift 是多么聪明。
我的第一个例子是这样的:给定一个名为 words
的字符串数组,如何查明这些单词是否存在于名为 input
的字符串中? 一种可能的解决方案是将input
分解为它自己的数组,然后遍历两个数组以寻找匹配项。但是 Swift 给了我们一个更好的解决方案:如果导入 Foundation 框架, String 会得到一个名为contains()
的方法,该方法接受另一个字符串并返回一个布尔值。因此,这段代码将返回true
:
let input = "My favorite album is Fearless"
input.contains("album")
String 数组还有两个contains()
方法:一个方法直接指定一个元素(在我们的例子中是字符串),另一个方法使用where
参数接受闭包。该闭包需要接受一个字符串并返回一个布尔值,如下所示:
words.contains { (str) -> Bool in
return true
}
Swift 编译器的出色设计让我们把这两件事放在一起:即使字符串的contains()
是一个来自 NSString 的基础方法,我们也可以将它传递到数组的contains(where:)
中,而不是传递闭包。所以,整个代码变成这样:
import Foundation
let words = ["1989", "Fearless", "Red"]
let input = "My favorite album is Fearless"
words.contains(where: input.contains)
最后一行是关键。contains(where:)
将对数组中的每个元素调用一次闭包,直到找到一个返回true
的元素。传入input.contains
意味着 Swift 将调用 input.contains("1989")
并返回 false
,然后它将调用input.contains("Fearless")
并返回true
——然后停止。因为contains()
具有与contains(where:)
所期望的(接受一个字符串并返回一个布尔值)完全相同的签名,所以这就像一个魔咒。
我的第二个例子使用了数组的reduce()
方法:提供一个初始值,然后给它一个函数来应用于数组中的每一项。每次调用该函数时,都会给你两个参数:调用该函数时的前一个值(这将是初始值)和要使用的当前值。
为了演示这一点,下面是一个调用reduce()
对一个整型数组来计算它们的和的例子:
let numbers = [1, 3, 5, 7, 9]
numbers.reduce(0) { (int1, int2) -> Int in
return int1 + int2
}
当代码运行时,它将初始值和 1 相加得到 1,然后是1和 3 (得到总数: 4 ),然后是4 和 5 (9),然后是 9 和 7 (16),然后是 16 和 9,最终得到 25 。
这种方法非常好,但 Swift 有一个更简单、更有效的解决方案:
let numbers = [1, 3, 5, 7, 9]
let result = numbers.reduce(0, +)
当你思考它的时候,+
是一个接受两个整数并返回它们的和的函数,所以我们可以移除整个闭包并用一个操作符替换它。
逃逸闭包(Escaping closures)
当你把一个闭包传递给一个函数时,Swift 默认认为它是不可逃逸的。这意味着闭包必须立即在函数内部使用,并且不能存储起来供以后使用。如果你试图在函数返回后使用闭包,Swift 编译器将拒绝构建,例如,如果要使用 GCD 的 asyncAfter()
方法在一段时间的延迟之后调用它。
这对于许多类型的函数都非常有用,比如sort()
,在这些函数中,你可以确定闭包将在方法中使用,然后就再也不会使用闭包了。sort()
方法接受非逃逸闭包作为其惟一的参数,因为sort()
不会尝试存储该闭包的副本供以后使用——它会立即使用闭包,然后结束。
另一方面,逃逸闭包是在方法返回后调用的闭包。它们存在于许多需要异步调用闭包的地方。例如,可能会给你一个闭包,该闭包只应该在用户做出选择时调用。你可以将该闭包存储起来,提示用户作出决定,然后在准备好用户的选择后调用闭包。
逃逸闭包和非逃逸闭包之间的区别可能听起来很小,但这很重要,因为闭包是引用类型。一旦 Swift 知道函数一旦完成就不会使用闭包——它是非逃逸的——它就不需要担心引用计数,因此它可以节省一些工作。因此,非逃逸闭包速度更快,并且是 Swift 的默认闭包。也就是说,除非另外指定,否则所有闭包参数都被认为是非逃逸的。
如果希望指定逃逸闭包,需要使用@escaping
关键字。最好的方法是在需要的时候演示一个场景。考虑下面的代码:
var queuedClosures: [() -> Void] = []
func queueClosure(_ closure: () -> Void) {
queuedClosures.append(closure)
}
queueClosure({ print("Running closure 1") })
queueClosure({ print("Running closure 2") })
queueClosure({ print("Running closure 3") })
这将创建要运行的闭包数组和接受要排队的闭包的函数。该函数除了将它被赋予的闭包追加到队列闭包数组之外,什么都不做。最后,它使用三个简单的闭包调用queueClosure()
三次,每个闭包打印一条消息。
为了完成这段代码,我们只需要创建一个名为executequeuedclosure()
的新方法,它遍历队列并执行每个闭包:
func executeQueuedClosures() {
for closure in queuedClosures {
closure()
}
}
executeQueuedClosures()
让我们更仔细地研究queueClosure()
方法:
func queueClosure(_ closure: () -> Void) {
queuedClosures.append(closure)
}
它只接受一个参数,这是一个没有参数或返回值的闭包。然后将该闭包添加到queuedclosure
数组中。这意味着我们传入的闭包可以稍后使用,在本例中,当调用executequeuedclosure()
函数时使用。
因为闭包可以稍后调用,Swift 认为它们是逃逸闭包,所以它将拒绝构建这段代码。请记住,出于性能考虑,非逃逸闭包是默认的,所以我们需要显式地添加@escape
关键字,以明确我们的意图:
func queueClosure(_ closure: @escaping () -> Void) {
queuedClosures.append(closure)
}
所以:如果你写了一个函数,它会立即调用闭包,然后不再使用它,它在默认情况下是非逃逸的,你可以忘记它。但是,如果你打算存储闭包供以后使用,则需要
@escape
关键字。
自动闭包(@autoclosure)
@autoclosure
属性类似于@escaping
,因为你将它应用于函数的闭包参数,但是它的使用要少得多。嗯,不,严格来说不是这样的:调用使用@autoclosure
的函数是很常见的,但是用它编写函数则不常见。
当你使用此属性时,它会根据传入的表达式自动创建闭包。当你调用使用此属性的函数时,你编写的代码不是闭包,当它会变成闭包,这可能有点令人困惑——甚至官方的 Swift 参考指南也警告说,过度使用自动闭包会使代码更难理解。
为了帮助你理解它是如何工作的,这里有一个简单的例子:
func printTest(_ result: () -> Void) {
print("Before")
result()
print("After")
}
printTest( { print("Hello") } )
该代码创建了printTest()
方法,该方法接受闭包并调用它。如你所见,print(“Hello”)
位于一个闭包中,该闭包在 “ Before ” 和 “ After ” 之间调用,因此最终的输出是 “ Before ”、“ Hello ”和 “ After ”。
如果我们使用@autoclosure
,它将允许我们重写代码printTest()
调用,这样它就不需要大括号,如下所示:
func printTest(_ result: @autoclosure () -> Void) {
print("Before")
result()
print("After")
}
printTest(print("Hello"))
由于@autoclosure
,这两段代码产生了相同的结果。在第二个代码示例中,print("Hello")
不会立即执行,因为它被包装在一个闭包中,以便稍后执行。
这种行为看起来很简单:所有这些工作只是删除了一对大括号,使代码更难理解。但是,有一个特定的地方需要使用它们:assert()
。这是一个 Swift 函数,用于检查条件是否为真,如果不为真,则会导致应用程序停止。
这听起来可能非常极端:为什么你希望你的应用程序崩溃?显然,你不会这样做,但是在测试应用程序时,添加assert()
调用有助于确保代码的行为符合预期。你真正想要的是,你的断言在 debug 模式下是活动的,而在 release 模式下是禁用的,这正是assert()
的工作方式。
请看下面三个例子:
assert(1 == 1, "Maths failure!")
assert(1 == 2, "Maths failure!")
assert(myReallySlowMethod() == false, "The slow method returned false!")
第一个例子返回true
,所以什么也不会发生。第二个将返回false
,因此应用程序将停止。第三个例子是assert()
的强大功能:因为它使用@autoclosure
将代码封装在闭包中,所以 Swift 编译器在 release 模式下不会运行闭包。这意味着你可以在调试时获得所有断言的安全性,而不需要在 release 模式中付出任何性能代价。
你可能有兴趣知道,自动闭包还用于处理&&
和||
操作符。以下是在官方编译器中找到&&
完整的 Swift 源代码:
public static func && (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
return lhs ? try rhs() : false
}
是的,它包含try/catch
、throw
和rethrow
、运算符重载、三元运算符和@autoclosure
,所有这些都在一个小函数中。尽管如此,我还是希望你能够理解代码的全部功能:如果lhs
为真,则返回rhs()
的结果,否则返回false
。这是实际的短路评估:如果lhs
代码已经返回false
, Swift 不需要运行rhs
闭包。
关于@autoclosure
的最后一件事:如果你想要进行逃逸闭包,你应该将这两个属性组合起来。例如,我们可以像这样重写前面的queueClosure()
函数:
func queueClosure(_ closure: @autoclosure @escaping () -> Void) {
queuedClosures.append(closure)
}
queueClosure(print("Running closure 1"))
提醒:小心使用自动闭包。它们会使代码更难理解,所以不要仅仅因为想避免键入一些花括号就使用它们。
~=操作符(The ~= operator)
我知道有一个喜欢的运算符听起来很奇怪,但是我确实喜欢,它是~=
。我喜欢它,因为它简单。我爱它,即使它不是真的需要。我甚至喜欢它的形状——只要看看它的美丽就行了!所以我希望你能原谅我花了几分钟时间给你看这个。
我已经对两个简单的符号流口水了:这到底是做什么的?我很高兴你这么问! ~=
是模式匹配操作符,它允许你这样编写代码:
let range = 1...100
let i = 42
if range ~= i {
print("Match!")
}
正如我所说,不需要这个操作符,因为你可以使用区间内置的contains()
方法编写代码。但是,它确实比contains()
有一点语法上的优势,因为它不需要额外的一组括号:
let test1 = (1...100).contains(42)
let test2 = 1...100 ~= 42
我认为~=
是使用操作符重载来整理日常语法的一个很好的例子。