《Pro Swift》 第四章:函数(Functions)

当编写代码在两个数字之间进行插值时,很容易默认为线性插值。然而,在两个值之间平稳过渡通常会更好。所以我的建议是避免步进,并使用函数(如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), SwiftCore 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()产生不同的输出:使用一次性调用将在一行中打印所有数字,而使用多次调用将逐行打印数字。

一旦添加了可选的额外参数:separatorterminatorprint()的可变参数特性将变得更加有用。第一个参数在传递的每个值之间放置一个字符串,第二个参数在打印完所有值后放置一个字符串。例如,这将打印 “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 ==,所以你要修改的内容非常清楚。你还可以看到,这个函数期望接收两个整数(左边和右边分别是lhsrhs),并返回一个布尔值,该值报告这两个数字是否相等。

除了完成实际工作的函数外,操作符还具有优先级和关联性,这两者都会影响操作的结果。当多个运算符一起使用而没有括号时,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, 2x23x3

*操作符已经存在,所以我们不需要声明它。相反,我们只需要创建一个新的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,以确保两个数组包含相同数量的项。

因为*操作符已经存在,所以重要的是lhsrhs参数,它们都是整数数组:当两个整数数组相乘时,这些参数确保选择这个新函数。

添加一个新的操作符(Adding a new operator)

当你添加一个新的操作符时,你需要提供足够的 Swift 信息来使用它。至少需要指定新操作符的位置(前缀、后缀或中缀),但如果不指定优先级或关联性 Swift 将提供默认值,使其成为低优先级、非关联操作符。

让我们添加一个新的操作符**,它返回一个值的幂。也就是说,2 ** 4 应该等于 2 * 2 * 2 * 2 ,即 16。我们将使用pow()函数,所以你需要导入Foundation框架:

import Foundation

一旦完成,我们需要告诉 Swift **将是一个中缀操作符,因为我们将在其左侧有一个操作数,在其右侧有另一个操作数:

infix operator **

它没有指定优先级或关联,因此将使用默认值。

最后,新的**函数本身。我已经让它接受双精度值以获得最大的灵活性,Swift足够聪明,当与这个操作符一起使用时,可以推断24是双精度值:

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,然后得到覆盖 110 的范围 。在默认情况下这是一个中缀操作符,范围的低端在左侧,高端在右侧,但我要修改它,以便它还接受左侧的范围和右侧的另一个整数,如下所示:

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...1,这将是我们的新操作。在此函数中,CountableClosedRange是左侧操作数,而Int 1是右侧操作数。

...函数需要做两件事:

  1. 计算一个新的区间,从右边的整数到左边区间的最高点,然后反转这个区间。
  2. 将左边的区间追加到新创建的递减区间,并作为函数的结果返回该区间。

在代码中,它看起来是这样的:

func ...(lhs: CountableClosedRange, 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, 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。但是,函数内的变量没有发生任何变化。相反,它返回一个闭包,该闭包将counter1 并打印出它的新值。它不调用那个闭包,它只返回它。

有趣的地方是函数之后:greetPerson被设置为testCapture()返回的函数,它被调用了三次。该闭包引用了在testCapture()中创建的counter值,现在显然超出了范围,因为该函数已经完成。因此,Swift 捕捉到了这个值:这个闭包现在有了自己对counter的独立引用,可以在调用它时使用。每次调用greetPerson()函数时,你将看到counter1

让事情变得加倍有趣的是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的字符串参数,并返回truefalse。然后闭包检查名称是否具有前缀 “ 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,然后是13 (得到总数: 4 ),然后是45 (9),然后是 97 (16),然后是 169,最终得到 25

这种方法非常好,但 Swift 有一个更简单、更有效的解决方案:

let numbers = [1, 3, 5, 7, 9]
let result = numbers.reduce(0, +)

当你思考它的时候,+是一个接受两个整数并返回它们的和的函数,所以我们可以移除整个闭包并用一个操作符替换它。

逃逸闭包(Escaping closures)

当你把一个闭包传递给一个函数时,Swift 默认认为它是不可逃逸的。这意味着闭包必须立即在函数内部使用,并且不能存储起来供以后使用。如果你试图在函数返回后使用闭包,Swift 编译器将拒绝构建,例如,如果要使用 GCDasyncAfter()方法在一段时间的延迟之后调用它。

这对于许多类型的函数都非常有用,比如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/catchthrowrethrow、运算符重载、三元运算符和@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

我认为~=是使用操作符重载来整理日常语法的一个很好的例子。

你可能感兴趣的:(《Pro Swift》 第四章:函数(Functions))