当知道不需要重写声明时,对属性和方法使用final
。 这允许编译器用直接调用替换动态派发调用。甚至可以通过将属性附加到类本身,将整个类标记为final
。
-- Wendy Lu (@wendyluwho),Pinterest 的 iOS 工程师
模式匹配
Switch/case
不是一个新概念:插入一个值,然后执行几个操作过程中的一个。Swift 对安全的关注增加了对混编的要求,所有可能的情况都要满足––如果没有启用特定的警告,你将无法在 C 中获得某些信息,但这是相当微不足道的。
Swift 的switch
语法之所以有趣,归功于它灵活、富有表现力的模式匹配。更有趣的是,自从 Swift 发布以来,大部分模式匹配都被扩展到了其他地方,所以在if
条件和for
循环中也可以使用同样灵活、有表现力的语法。
不可否认,如果你在深水区跳入水中,你更可能下沉而不是游泳,所以我想从基本的例子开始进行研究。为了刷新你的记忆,这里有一个基本的switch
语句:
let name = "twostraws"
switch name {
case "bilbo":
print("Hello, Bilbo Baggins!")
case "twostraws":
print("Hello, Paul Hudson!")
default:
print("Authentication failed")
}
当你处理一个简单的字符串时,这非常简单,但是当处理两个或多个值时,事情就变得更加复杂了。例如,如果我们想验证一个名称和密码,我们将把它们作为一个元组来计算:
let name = "twostraws"
let password = "fr0st1es"
switch (name, password) {
case ("bilbo", "bagg1n5"):
print("Hello, Bilbo Baggins!")
case ("twostraws", "fr0st1es"):
print("Hello, Paul Hudson!")
default:
print("Who are you?")
}
如果你愿意,可以将这两个值组合成一个元组,如下所示:
let authentication = (name: "twostraws", password: "fr0st1es")
switch authentication {
case ("bilbo", "bagg1n5"):
print("Hello, Bilbo Baggins!")
case ("twostraws", "fr0st1es"):
print("Hello, Paul Hudson!")
default:
print("Who are you?")
}
在这种情况下,元组的两个部分都必须与switch
匹配才能执行它。
部分匹配
在处理元组时,有时需要部分匹配:你关心某些值是什么,但不关心其他值。在这种情况下,使用下划线表示任何值都可以,如下所示:
let authentication = (name: "twostraws", password: "fr0st1es", ipAddress: "127.0.0.1")
switch authentication {
case ("bilbo", "bagg1n5", _):
print("Hello, Bilbo Baggins!")
case ("twostraws", "fr0st1es", _):
print("Hello, Paul Hudson!")
default:
print("Who are you?")
}
记住:Swift 将采用它发现的第一个匹配的案例,因此你需要确保首先查找最具体的内容。例如,下面的代码将打印 You could be anybody! 因为第一种情况会立即匹配,即使之后的案例会更好匹配,因为它们匹配的内容更多:
switch authentication {
case (_, _, _):
print("You could be anybody!")
case ("bilbo", "bagg1n5", _):
print("Hello, Bilbo Baggins!")
case ("twostraws", "fr0st1es", _):
print("Hello, Paul Hudson!")
default:
print("Who are you?")
}
最后,如果你只想匹配元组的一部分,但仍然想知道另一部分是什么,那么应该使用let
语法。
switch authentication {
case ("bilbo", "bagg1n5", _):
print("Hello, Bilbo Baggins!")
case ("twostraws", let password, _):
print("Hello, Paul Hudson: your password was \(password)!")
default:
print("Who are you?")
}
匹配计算元组
到目前为止,我们的介绍已经覆盖了大多数开发人员使用模式匹配语法的基本范围。从这里开始,我想举一些其他不太为人所知的有用的模式匹配技术的例子。
元组最常使用静态值创建,如下所示:
let name = ("Paul", "Hudson")
但元组和其他任何数据结构一样,都可以使用动态代码创建。当你希望将元组中的值范围缩小到较小的子集,以便只需要少数case
语句时,这一点特别有用。
为了给你一个实际的例子,考虑 fizz buzz 测试:编写一个接受任何数字的函数,如果数字被 3 整除,则返回 fizz
;如果数字被 5 整除,则返回 buzz
;如果数字被 3 和 5 整除,则返回fizzbuzz
;在其他情况下,返回原始输入数字。
我们可以计算一个元组来解决这个问题,然后将该元组传递到一个switch
中以创建正确的输出。代码如下:
func fizzbuzz(number: Int) -> String {
switch (number % 3 == 0, number % 5 == 0) {
case (true, false):
return "Fizz"
case (false, true):
return "Buzz"
case (true, true):
return "FizzBuzz"
case (false, false):
return String(number)
}
}
print(fizzbuzz(number: 15))
这种方法将大的输入空间(任何数字)分解为真和假的简单组合,然后在case
语句中使用元组模式匹配来选择正确的输出。
循环(Loops)
正如你所看到的,使用元组的一部分进行模式匹配非常简单:你可以告诉 Swift 应该匹配什么,使用let
将值绑定到局部常量,或者使用_
表示你不关心值是什么。
在处理循环时,我们可以使用相同的方法,这允许我们只在项与我们指定的条件匹配时遍历项。让我们再次从一个基本示例开始:
let twostraws = (name: "twostraws", password: "fr0st1es")
let bilbo = (name: "bilbo", password: "bagg1n5")
let taylor = (name: "taylor", password: "fr0st1es")
let users = [twostraws, bilbo, taylor]
for user in users {
print(user.name)
}
这将创建一个元组数组,然后循环遍历每个元组并打印其name
的值。
就像我们前面看到的switch
块一样,我们可以用case
和元组来匹配
元组中的特定值。在之前的循环下面添加以下代码:
for case ("twostraws", "fr0st1es") in users {
print("User twostraws has the password fr0st1es")
}
我们也有相同的语法来将局部常量绑定到每个元组的值,例如:
for case (let name, let password) in users {
print("User \(name) has the password \(password)")
}
不过,通常情况下,最好将let
重新放置为:
for case let (name, password) in users {
print("User \(name) has the password \(password)")
}
神奇的是,当你把这两种方法结合起来的时候,在语法上和我们已经看到的switch
例子是一样的:
for case let (name, "fr0st1es") in users {
print("User \(name) has the password \"fr0st1es\"")
}
它过滤users
数组,这样只有密码为fr0st1es
的项才会在循环中使用,然后在循环中创建一个name
常量供你使用。
如果你正盯着for case let
,看到三个完全不同的关键字混在一起,不要担心:除非有人向你解释它,否则它做什么并不明显,而且要花一点时间才能理解。但我们才刚刚开始...
匹配可选值(Matching optionals)
Swift 有两种匹配可选值的方法,你很可能会同时遇到这两种方法。首先是
使用.some
和.none
来匹配有值和没有值,在下面的代码中,它用于检查值并将它们绑定到本地常量:
let name: String? = "twostraws"
let password: String? = "fr0st1es"
switch (name, password) {
case let (.some(name), .some(password)):
print("Hello, \(name)")
case let (.some(name), .none):
print("Please enter a password.")
default:
print("Who are you?")
}
由于name
和password
用于输入常量和本地绑定的常量,这段代码变得更加容易混淆。但是,它们是不同的东西,这就是为什么 print("Hello,\(name)")
不会打印 Hello, Optional("twostraws") —— 这里所使用的name
是本地绑定展开的可选名称。
如果想更容易阅读,下面是相同的代码,不同的名称用于匹配的常量:
switch (name, password) {
case let (.some(matchedName), .some(matchedPassword)):
print("Hello, \(matchedName)")
case let (.some(matchedName), .none):
print("Please enter a password.")
default:
print("Who are you?")
}
Swift 匹配可选值的第二种方法是使用更简单的语法,尽管如果你害怕可选值,这只会让情况变得更糟:
switch (name, password) {
case let (name?, password?):
print("Hello, \(name)")
case let (username?, nil):
print("Please enter a password.")
default:
print("Who are you?")
}
这一次问号的工作方式与可选链类似:仅当找到值时才继续。
这两种方法在if case let
代码中都同样有效。下面的代码同时使用它们在循环中过滤掉的nil
值:
import Foundation
let data: [Any?] = ["Bill", nil, 69, "Ted"]
for case let .some(datum) in data {
print(datum)
}
for case let datum? in data {
print(datum)
}
匹配范围(Matching ranges)
你可能已经在使用与范围匹配的模式,通常使用如下代码:
let age = 36
switch age {
case 0 ..< 18:
print("You have the energy and time, but not the money")
case 18 ..< 70:
print("You have the energy and money, but not the time")
default:
print("You have the time and money, but not the energy")
}
对于常规条件语句,也可以使用非常相似的语法–我们可以这样重写代码:
if case 0 ..< 18 = age {
print("You have the energy and time, but not the money")
} else if case 18 ..< 70 = age {
print("You have the energy and money, but not the time")
} else {
print("You have the time and money, but not the energy")
}
在使用类似语法的情况下,这将产生与switch
块相同的结果,但我不太喜欢这种方法。我不喜欢它的原因很简单:我不认为如果0到18的范围等于年龄是合理的,如果你还不知道它这意味着什么。一个更好的方法是使用模式匹配操作符~=
,它看起来像这样:
if 0 ..< 18 ~= age {
print("You have the energy and time, but not the money")
} else if 18 ..< 70 ~= age {
print("You have the energy and money, but not the time")
} else {
print("You have the time and money, but not the energy")
}
现在条件是如果0到18的范围与年龄相匹配,我认为这更有意义。
当你记住0..<18
创建了一个具有自己方法集的Range
结构的实例时,一个更清晰的解决方案就浮出水面了。现在,它的contains()
方法特别有用:它的输入时间比~=
长,但更容易理解:
if (0 ..< 18).contains(age) {
print("You have the energy and time, but not the money")
} else if (18 ..< 70).contains(age) {
print("You have the energy and money, but not the time")
} else {
print("You have the time and money, but not the energy")
}
你可以将此范围匹配组合到现有的元组匹配代码中,如下所示:
let user = (name: "twostraws", password: "fr0st1es", age: 36)
switch user {
case let (name, _, 0 ..< 18):
print("\(name) has the energy and time, but no money")
case let (name, _, 18 ..< 70):
print("\(name) has the money and energy, but no time")
case let (name, _, _):
print("\(name) has the time and money, but no energy")
}
最后一种情况将用户名绑定到名为name
的本地常量,而不考虑元组中的其他两个值。这是一个匹配所有的case
,但因为 Swift 寻找第一个匹配的case
,这不会与switch
块中的其他两个冲突。
匹配枚举和关联值(Matching enums and associated values)
根据我的经验,相当多的人并不真正理解枚举和关联值,因此他们很难利用它们进行模式匹配。本书后面有一整章都是关于枚举的,所以如果你对枚举和相关的值还不熟悉的话,你可能需要在这里暂停并首先阅读该章节。
基本枚举匹配是这样的:
enum WeatherType {
case cloudy
case sunny
case windy
}
let today = WeatherType.cloudy
switch today {
case .cloudy:
print("It's cloudy")
case .windy:
print("It's windy")
default:
print("It's sunny")
}
你还将在基本条件语句中使用枚举,如下所示:
if today == .cloudy {
print("It's cloudy")
}
一旦你添加了关联值,事情就会变得更加复杂,因为你可以使用它们,过滤它们,或者根据你的目标忽略它们。
首先,最简单的选项:创建关联值,但忽略它:
enum WeatherType {
case cloudy(coverage: Int)
case sunny
case windy
}
let today = WeatherType.cloudy(coverage: 100)
switch today {
case .cloudy:
print("It's cloudy")
case .windy:
print("It's windy")
default:
print("It's sunny")
}
使用这种方法,实际上switch
代码是不变的。
第二:创建关联值并使用它。这使用了我们已经多次看到的相同的本地常量绑定:
enum WeatherType {
case cloudy(coverage: Int)
case sunny
case windy
}
let today = WeatherType.cloudy(coverage: 100)
switch today {
case .cloudy(let coverage):
print("It's cloudy with \(coverage)% coverage")
case .windy:
print("It's windy")
default:
print("It's sunny")
}
最后:创建一个关联类型,将一个本地常量绑定到该类型,同时使用该绑定来筛选特定的值。它使用where
关键字创建一个需求子句,以阐明你要查找的内容。在我们的例子中,下面的代码根据cloudy
使用的关联值打印了两条不同的消息:
enum WeatherType {
case cloudy(coverage: Int)
case sunny
case windy
}
let today = WeatherType.cloudy(coverage: 100)
switch today {
case .cloudy(let coverage) where coverage < 100:
print("It's cloudy with \(coverage)% coverage")
case .cloudy(let coverage) where coverage == 100:
print("You must live in the UK")
case .windy:
print("It's windy")
default:
print("It's sunny")
}
现在,正如我所承诺的,我将从基本示例开始构建,但是如果你已经准备好了,我想向你展示如何将这两种技术组合在一起:关联值和范围匹配。下面的代码现在打印了四条不同的消息:
enum WeatherType {
case cloudy(coverage: Int)
case sunny
case windy
}
let today = WeatherType.cloudy(coverage: 100)
switch today {
case .cloudy(let coverage) where coverage == 0:
print("You must live in Death Valley")
case .cloudy(let coverage) where (1...50).contains(coverage):
print("It's a bit cloudy, with \(coverage)% coverage")
case .cloudy(let coverage) where (51...99).contains(coverage):
print("It's very cloudy, with \(coverage)% coverage")
case .cloudy(let coverage) where coverage == 100:
print("You must live in the UK")
case .windy:
print("It's windy")
default:
print("It's sunny")
}
如果要匹配循环中的关联值,添加where
子句是错误的方法。事实上,这类代码甚至无法编译:
let forecast: [WeatherType] = [.cloudy(coverage:40), .sunny, .windy, .cloudy(coverage: 100), .sunny]
for day in forecast where day == .cloudy {
print(day)
}
如果没有关联值,该代码就可以了,但是因为关联值意味着where
子句不能胜任工作--它无法说并将关联的值绑定到本地常量。相反,你将回到case-let
语法,如下所示:
let forecast: [WeatherType] = [.cloudy(coverage:40), .sunny, .windy, .cloudy(coverage: 100), .sunny]
for case let .cloudy(coverage) in forecast {
print("It's cloudy with \(coverage)% coverage")
}
如果你想知道关联值,并想把它用作过滤条件,语法几乎是一样的:
let forecast: [WeatherType] = [.cloudy(coverage: 40), .sunny, .windy, .cloudy(coverage: 100), .sunny]
for case .cloudy(40) in forecast {
print("It's cloudy with 40% coverage")
}
匹配类型(Matching types)
你应该已经知道用于匹配的is
关键字,但是你可能不知道它可以在循环和switch
块中用于模式匹配。我觉得这个语法很好用,所以我想简单说明一下:
let view: AnyObject = UIButton()
switch view {
case is UIButton:
print("Found a button")
case is UILabel:
print("Found a label")
case is UISwitch:
print("Found a switch")
case is UIView:
print("Found a view")
default:
print("Found something else")
}
我以 UIKit 为例,因为你应该已经知道UIButton
继承自UIView
,我需要给你一个大大的警告…
记住:Swift 将接受它找到的第一个匹配的情况,如果对象是特定类型或其父类之一,则is
返回true
。因此,上面的代码将打印 Found a button,而下面的代码将打印 Found a view:
let view: AnyObject = UIButton()
switch view {
case is UIView:
print("Found a view")
case is UIButton:
print("Found a button")
case is UILabel:
print("Found a label")
case is UISwitch:
print("Found a switch")
default:
print("Found something else")
}
为了给你提供一个更有用的示例,你可以使用此方法循环数组中的所有子视图并筛选UILabel
:
for label in view.subviews where label is UILabel {
print("Found a label with frame \(label.frame)")
}
尽管where
确保在循环中只处理UILabel
对象,但实际上它不执行任何类型转换。这意味着,如果你想访问label
的特定属性,比如它的text
属性,你需要自己对它进行类型转换。在这种情况下,使用for case let
更容易,因为它在过滤的同时进行类型转换:
for case let label as UILabel in view.subviews {
print("Found a label with text \(label.text)")
}
使用WHERE关键词(Using the where keyword)
为了总结模式匹配,我想演示一些有趣的使用where
子句的方法,以便你了解它的功能。
首先,一个简单的方法:循环一组数字,只打印奇数。使用where
和取模运算这很简单,但它表明where
子句可以包含计算:
for number in numbers where number % 2 == 1 {
print(number)
}
也可以调用方法,如:
let celebrities = ["Michael Jackson", "Taylor Swift", "Michael Caine", "Adele Adkins", "Michael Jordan"]
for name in celebrities where !name.hasPrefix("Michael") {
print(name)
}
这将打印 Taylor Swift 和 Adele Adkins。如果要使where
子句更复杂,只需添加&&
等运算符即可。
let celebrities = ["Michael Jackson", "Taylor Swift", "Michael Caine", "Adele Adkins", "Michael Jordan"]
for name in celebrities where name.hasPrefix("Michael") &&
name.characters.count == 13 {
print(name)
}
这将打印 Michael Caine。
虽然可以使用where
子句剔除可选值,但我不推荐。请考虑下面的示例:
let celebrities: [String?] = ["Michael Jackson", nil, "Michael Caine", nil, "Michael Jordan"]
for name in celebrities where name != nil {
print(name)
}
这当然有用,但它对循环中的字符串的可选性没有任何影响,所以它输出如下:
Optional("Michael Jackson")
Optional("Michael Caine")
Optional("Michael Jordan")
相反,使用for case let
来处理可选值,并使用where
来过滤值。下面是编写循环的首选方法:
for case let name? in celebrities where name.hasSuffix("Jackson") {
print(name)
}
运行时,name
只包含具有值且后缀为Jackson
的字符串,因此其输出为:
Michael Jackson
空值合并(Nil coalescing)
Swift 可选值是保证程序安全的基本方法之一:只有在变量确实具有值时才能使用它。问题是可选值使代码更难读和写,因为你需要安全地展开它们。
一种替代方法是使用!
显式地展开可选值。这也被称为崩溃操作符,因为如果你在值为nil
的可选值上使用!
,你的程序将立即奔溃。
一个更聪明的选择是空合运算符??
,它允许你访问可选值,并在可选值为nil
时提供默认值。
考虑这个可选值:
let name: String? = "Taylor"
这是一个名为name
的常量,包含一个字符串或nil
。如果你试图用print(name)
打印它,你会看到Optional("Taylor")
,而不是"Taylor"
,这不是你真正想要的。
使用空值合并允许我们使用可选的值,或者提供一个默认值(如果为nil
)。所以,你可以这样写:
let name: String? = "Taylor"
let unwrappedName = name ?? "Anonymous"
print(unwrappedName)
这将打印"Taylor"
: name
是String?
,但是unwrappedName
保证是一个常量字符串(不是可选的),因为使用了空合运算符。要查看默认值的实际操作,请尝试以下操作:
let name: String? = nil
let unwrappedName = name ?? "Anonymous"
print(unwrappedName)
现在将打印"Anonymous"
,因为使用的是默认值。
当然,当使用空值合并时,你不需要一个单独的常量——你可以在内联中编写它,如下所示:
let name: String? = "Taylor"
print(name ?? "Anonymous")
正如您可以想象的那样,空值合并对于确保在使用前合理的值已经就位是非常非常有用的,但是它对于从代码中删除一些可选性特别有用。例如:
func returnsOptionalName() -> String? {
return nil
}
let returnedName = returnsOptionalName() ?? "Anonymous"
print(returnedName)
使用这种方法,returnedName
是String
而不是String?
, 因为它是有值的。
到目前为止,一切都很简单。然而,当你将空值合并与try?
关键字结合起来时,它会变得更加有趣。
考虑一个简单的应用程序,它允许用户键入并保存文本。当应用程序运行时,它希望加载用户以前键入的任何内容,因此它可能使用如下代码:
do {
let savedText = try String(contentsOfFile: "saved.txt")
print(savedText)
} catch {
print("Failed to load saved text.")
}
如果文件存在,它将加载到savedText
常量中。否则 contentsOfFile
初始化器将抛出异常,并打印未能加载保存的文本。在实际中,你想要扩展它,以便savedText
总是有一个值,所以你最终得到这样的结果:
let savedText: String
do {
savedText = try String(contentsOfFile: "saved.txt")
} catch {
print("Failed to load saved text.")
savedText = "Hello, world!"
}
print(savedText)
这是很多代码,但实际上并没有完成很多工作。幸运的是,有一种更好的方法:空值合并。记住,try
有三种变体:try
尝试一些代码,并可能引发异常,try!
尝试一些代码并在应用程序失败时崩溃,然后try?
尝试一些代码,如果调用失败,则返回nil
。
最后一个例子是空值合并的使用场景,因为这与前面的示例完全匹配:我们希望使用可选值,如果可选值为nil
,则提供合理的默认值。事实上,使用空值合并,我们可以将所有这些代码重写为两行代码:
let savedText = (try? String(contentsOfFile: "saved.txt")) ?? "Hello, world!"
print(savedText)
这意味着尝试加载文件,但如果加载失败,则使用此默认文本——更整洁的解决方案,可读性更高。
结合try?
和空值合并非常适合于失败的try
不是错误的情况,我认为你会发现这种模式在你自己的代码中非常有用。
Guard
从 Swift 2.0 开始,guard
这个关键字就一直伴随着我们,但是因为它一次性同时做了四件事,所以你没有充分使用它是值得原谅的。
第一种用法是最明显的:guard
用于早期返回,这意味着如果某些前提条件不满足,则退出函数。例如,我们可以编写一个带有偏见的函数来给一个指定的人颁奖:
func giveAward(to name: String) {
guard name == "Taylor Swift" else {
print("No way!")
return
}
print("Congratulations, \(name)!")
}
giveAward(to: "Taylor Swift")
使用giveAward(to:)
方法中的guard
声明可以确保只有Taylor Swift
赢得奖项。正如我所说,这是有偏见的,但是前提条件是明确的,并且只有在满足我设置的需求时,这段代码才会运行。
这个最初的示例看起来几乎与使用if
相同,但是guard
有一个巨大的优势:它使你的意图变得清晰,不仅对人们,而且对编译器也是如此。这是一个早期返回,也就是说,如果不满足前提条件,你希望退出该方法。使用guard
可以清楚地表明:这种情况不是方法功能的一部分,只是为了确保实际的代码可以安全运行。编译器也很清楚,这意味着如果删除return
语句,代码将不再生成—— Swift 知道这是一个早期返回,因此它不会让你忘记退出。
使用guard
的第二个好处是第一个好处的副作用:使用guard
和早期返回可以降低缩进级别。一些开发人员坚信不能使用早期的返回,相反,每个函数应该只从一个地方返回。会在函数代码的主体中强制进行额外的缩进,如下所示:
func giveAward(to name: String) -> String {
let message: String
if name == "Taylor Swift" {
message = "Congratulations, \(name)!"
} else {
message = "No way!"
}
return message
}
giveAward(to: "Taylor Swift")
有了guard
,你的先决条件立刻得到解决,并丢弃额外的缩进——为整齐的代码欢呼!
guard
带给我们的第三件事是提高主逻辑的明显性。这是软件设计和测试中的一个常见概念,指的是当没有异常或错误发生时,代码将采用的路径。由于有了guard
,常见的错误会立即被删除,代码的其余部分可能都是正确的路径。
这些都是很简单的事情,但是guard
还有一个我想要讨论的特性,它是guard
和if
之间的一个重要区别:当你使用guard
检查和解包一个可选值时,这个可选值会留在作用域中。
为了演示这一点,我将重写giveAward(To:)
方法,使它接受一个可选的字符串:
func giveAward(to name: String?) {
guard let winner = name else {
print("No one won the award")
return
}
print("Congratulations, \(winner)!")
}
如果使用常规的if-let
,winner
常量只能在属于guard
的大括号内使用。然而,guard
在作用域中保持其可选的展开,因此winner
将在第二个print()
语句中保留。这段代码读作尝试将name
解包到winner
,这样我就可以使用它了,但是如果你不能,那么就打印一条消息并退出。”
我想介绍一下guard
的最后一个特性,但它并不新鲜。相反,它只是一种使用你已经知道的东西的不同方式。特性是:如果前提条件失败,guard
允许你退出任何范围,而不仅仅是函数和方法。这意味着你可以使用guard
退出switch
块或循环,它具有相同的含义:只有当这些前提条件为真时,才应执行此范围的内容。
举个简单的例子,这个循环从 1 到 100,打印出所有能被 8 整除的数:
for i in 1...100 {
guard i % 8 == 0 else { continue }
print(i)
}
你能用where
重写一下吗?试一试——它比你想象的要容易!
// 答案
for i in 1...100 where i % 8 == 0 {
print(i)
}
懒加载(Lazy loading)
延迟加载是 Swift 编码人员进行的最重要的全系统性能优化之一。它在 iOS 中很普遍,任何试图在视图控制器的视图显示之前操纵它的人都能告诉你。Objective-C 没有惰性属性的概念,因此每次需要这种行为时,都必须编写自己的样板代码。令人欣慰的是,Swift 已经将它完全融入其中,所以你几乎不需要任何代码就可以立即获得性能优势。
但首先:提醒一下什么是惰性属性。考虑一下这个类:
class Singer {
let name: String
init(name: String) {
self.name = name
}
func reversedName() -> String {
return "\(name.uppercased()) backwards is \(String(name.uppercased().characters.reversed()))!"
}
}
let taylor = Singer(name: "Taylor Swift")
print(taylor.reversedName())
这将在运行时打印 TAYLOR SWIFT backwards is TFIWS ROLYAT!。
每个Singer
都有一个名为name
的属性,还有一个方法对这个属性进行少量处理。显然,在你自己的代码中,这些函数可能会做更重要的工作,但是我在这里尽量保持简单。
每次你想打印消息 TAYLOR SWIFT reverse is TFIWS ROLYAT! 时,你都需要调用reversedName()
方法——它所做的工作不会被存储,如果该工作不是琐碎的,那么重复调用该方法就是浪费。
另一种方法是创建一个额外的属性来存储reversedName
,这样它只计算一次,如下所示:
class Singer {
let name: String
let reversedName: String
init(name: String) {
self.name = name
reversedName = "\(name.uppercased()) backwards is \(String(name.uppercased().characters.reversed()))!"
} }
let taylor = Singer(name: "Taylor Swift")
print(taylor.reversedName)
对于经常使用reversedName
的情况,这是一种性能改进,但是如果从不使用reversedName
,则会导致代码运行得更慢——无论是否使用,都会计算它,而当reversedName()
是一个方法时,它只会在调用时计算。
惰性属性是中间地带:它们是只计算一次并存储的属性,但是如果不使用它们,就永远不会计算。因此,如果你的代码重复使用惰性属性,那么只需要付出一次性能代价,如果这些属性从未使用过,那么代码就永远不会运行。这是双赢的!
懒闭包(Lazy closures)
开始使用lazy
关键字的最简单方法是使用闭包。 是的,我知道在同一个句子中看到 closures(闭包) 和easiest(最简单的) 的情况很少见,但有一个原因,这本书不叫Newbie Swift(新手Swift)!
这里的语法一开始有点不寻常:
lazy var yourVariableName: SomeType = {
return SomeType(whatever: "foobar")
}()
是的,你需要显式地声明类型。是的,你需要那个=
号。是的,你需要在右大括号后面加上小括号。这有点不寻常,就像我说的,但它的存在是有原因的:你正在创建闭包,立即应用它(而不是稍后),并将其结果分配回variablename
。
使用这种方法,我们可以将reversedName()
方法转换成如下所示的惰性属性:
class Singer {
let name: String
init(name: String) {
self.name = name
}
lazy var reversedName: String = {
return "\(self.name.uppercased()) backwards is \(String(self.name.uppercased().characters.reversed()))!"
}()
}
let taylor = Singer(name: "Taylor Swift")
print(taylor.reversedName)
注意:由于它现在是一个属性而不是方法,所以我们需要使用
print(taylor.reversedName)
而不是print(taylor.reversedName())
来访问该值。
就是这样:属性现在是惰性的,这意味着只有在第一次读取reversedName
属性时才会执行闭包中的代码。
“但保罗,” 我听到你说,“你在一个由对象拥有的闭包中使用self
- 为什么你给我一个循环引用?” 别担心:这段代码非常安全。 Swift 聪明到足以意识到正在发生的事情,并且不会创建任何循环引用。
在引擎下,任何这样的闭包 - 立即应用的 - 被认为是 non-escaping(非逃逸),在我们的情况下意味着它不会在其他任何地方使用。 也就是说,此闭包不能存储为属性并稍后调用。 这不仅会自动确保self
被认为是unowned
,而且还使 Swift 编译器能够进行一些额外的优化,因为它有更多关于闭包的信息。
懒方法(Lazy methods)
在使用lazy
时,人们经常抱怨它会使代码变得混乱:lazy
属性不是属性和方法的简单分离,而是属性和功能混合在一起的灰色区域。对此有一个简单的解决方案:创建方法来将惰性属性从它们所依赖的代码中分离出来。
如果你想使用这种方法,我建议你将创建的单独方法标记为private
,这样就不会意外地使用它。类似这样的东西应该会奏效:
class Singer {
let name: String
init(name: String) {
self.name = name
}
lazy var reversedName: String = self.getReversedName()
private func getReversedName() -> String {
return "\(name.uppercased()) backwards is \(String(name.uppercased().characters.reversed()))!"
}
}
let taylor = Singer(name: "Taylor Swift")
print(taylor.reversedName)
懒单例(Lazy singletons)
单例模式是我不太喜欢的几种常见编程模式之一。如果你不熟悉它们,那么单例就是一个值或对象,它被设计(和编码)为只创建一次,并在整个程序中共享。例如,如果你的应用程序使用一个日志程序,你可以在应用程序运行时创建一个日志程序对象,并让所有其他代码使用该共享实例。
我不太喜欢单例的原因很简单:它们经常被用作全局变量。许多人会鼓吹全局变量是不好的,然后很高兴地以几乎相同的方式滥用单例,这是草率的。
话虽如此,使用单例的理由也很充分,而且苹果有时也会使用单例。如果你的对象只能存在一次——比如一个UIApplication
的实例——那么单例就有意义了。在 iOS系统中,像UIDevice
这样的东西作为单例是有意义的,因为它们只能存在一次。如果你想在使用单例时添加额外的代码,单例也很有用(至少与全局变量相比)。
所以:只要你仔细考虑它们的使用,单例还是有一席之地的。如果你认为单例是完美的选择,我有个好消息:Swift 让单例变得异常容易。
举个实际的例子,我们将创建一个Singer
类,它将有一个MusicPlayer
类作为属性。这需要是一个单例,因为无论我们的应用程序中有多少歌手,我们都希望他们所有的歌曲通过同一个音乐播放器播放,这样音乐就不会重叠。
下面是MusicPlayer
类:
class MusicPlayer {
init() {
print("Ready to play songs!")
}
}
它只在创建消息时打印消息。
下面是基本的Singer
类,它在创建消息时只打印消息:
class Singer {
init() {
print("Creating a new singer")
}
}
现在对于单例:如果我们想给Singer
类一个MusicPlayer
单例属性,我们只需要在Singer
类中添加一行代码:
static let musicPlayer = MusicPlayer()
就是这样。static
部分意味着这个属性由类共享,而不是由类的实例共享,这意味着你使用Singer.musicPlayer
而不是taylor.musicPlayer
。let
部分当然意味着它是一个常量。
你可能想知道所有这些与惰性属性有什么关系,现在是时候找出答案了——将这段代码放到一个playground
中:
class MusicPlayer {
init() {
print("Ready to play songs!")
}
}
class Singer {
static let musicPlayer = MusicPlayer()
init() {
print("Creating a new singer")
}
}
let taylor = Singer()
当它运行时,输出是 Creating a new singer — Ready to play songs! 消息将不会出现。如果你在playground
的结尾再加一行,只有这样,信息才会出现:
Singer.musicPlayer
是的:所有 Swift static let
单例都是自动懒加载的——它们只有在需要时才会被创建。这很容易做到,但也非常有效。谢谢, Swift 开发团队!
懒序列(Lazy sequences)
现在你已经了解了惰性属性,我想简要地解释一下惰性序列的用处。这些类似于延迟属性,因为它们将工作延迟到必要的时候,但是它们并不像你稍后将看到的那样高效。
让我们从一个简单的例子开始:斐波那契数列。提醒一下,这是从 0 和 1开始的数字序列,其中后面的每个数字都是前两个数字之和。序列是0 1 1 2 3 5 8 13 21 34 55
,以此类推。
我们可以编写一个函数,该函数计算特定点的斐波那契数,如下所示:
func fibonacci(of num: Int) -> Int {
if num < 2 {
return num
} else {
return fibonacci(of: num - 1) + fibonacci(of: num - 2)
}
}
这是一个递归函数:它调用自己。这是一个 naïve (幼稚的) 实现,因为它不会在运行时缓存结果,这这意味着fibonacci(of: num - 1
)所做的所有添加操作不会被fibonacci(of: num - 2)
重用,即使它可以被重用。但是,这个实现非常适合演示延迟序列的好处(和缺点!)
打开一个 Playground,并添加以下代码:
func fibonacci(of num: Int) -> Int {
if num < 2 {
return num
} else {
return fibonacci(of: num - 1) + fibonacci(of: num - 2)
}
}
let fibonacciSequence = (0...20).map(fibonacci)
print(fibonacciSequence[10])
它计算斐波那契序列的前 21 个数字,并打印出第 11个数字:55。我让你把它放在Playground上,因为 Xcode 会告诉你代码执行的频率,你会看到返回 num 行被调用 28656 次——这是一个巨大的工作量。如果你尝试使用 0…21 作为范围-只是一个数字的提高!你会看到这个数字上升到 46367 次。
就像我说的,这是一个 naïve (幼稚的) 的实现,它的伸缩性不是很好。你能想象用 0…199 吗?如果你只需要几个数字,而不是所有的数字怎么办?
这就是延迟序列发挥作用的地方:你给它一个要处理的序列,并告诉它你想要运行什么代码,就像你对普通序列所做的一样,但是现在,当你访问项时,代码是按需执行的。因此,我们可以准备生成斐波那契数列的前 200 个数字,然后使用序列的惰性属性只使用第 20 个值:
let lazyFibonacciSequence = Array(0...199).lazy.map(fibonacci)
print(lazyFibonacciSequence[19])
注意:你需要使用 Array,以确保 Swift 在数组上创建延迟映射,而不是在 0…199 范围内创建延迟映射。
新代码的运行需要少量的时间,因为所有其他计算都不会运行—不会浪费时间。
然而,尽管延迟序列很聪明,但是它们有一个延迟属性所没有的缺点:它们没有记忆。这是一种常见的优化技术,它存储计算代价高昂的代码的结果,因此不需要再次创建它。这本质上是常规惰性变量提供给我们的:它不仅保证了一个属性在不使用的情况下不会被创建,而且保证了它在一次又一次使用时不会被重复创建。
正如我所说,延迟序列没有记忆,这意味着两次请求相同的数据将需要做两次工作。试试这个:
let lazyFibonacciSequence = Array(0...199).lazy.map(fibonacci)
print(lazyFibonacciSequence[19])
print(lazyFibonacciSequence[19])
print(lazyFibonacciSequence[19])
你将看到代码现在运行所需的时间是以前的三倍。因此,在必要时使用惰性序列,但是请记住,在某些情况下,它们实际上可能会减慢你的速度!
析构(Destructuring)
析构(也称为分解)是将数据从元组传输到元组和从元组传输到元组的一种聪明的方法,当你开始理解它时,你将认识到析构和模式匹配是如何紧密地联系在一起的。析构有三种用途:将元组分成多个值,同时分配给多个对象,以及交换值。
考虑一下这个元组:
let data = ("one", "two", "three")
如果你想从这三个值中创建三个不同的常量,而不进行析构,你需要这样写:
let one = data.0
let two = data.1
let three = data.2
通过析构,你可以这样写:
let (one, two, three) = data
Swift 将data
元组分成三个单独的常量,所有这些都在一行代码中。
当你处理返回元组的函数时,这种技术尤其有用,当你希望返回多个值时,通常使用这种方法。通常需要分割这些返回值,以便你可以根据 terms(条款)引用它们,尤其是在元组中没有名称的情况下。例如:
func getPerson() -> (String, Int) {
return ("Taylor Swift", 26)
}
let (name, age) = getPerson()
print("\(name) is \(age) years old")
如果你想在析构过程中忽略值,请使用_
,如下所示:
let (_, age) = getPerson()
print("That person is \(age) years old")
你可以使用相同的技术同时分配多个内容,使用固定值或使用函数调用。例如:
let (captain, chef) = ("Janeway", "Neelix")
let (engineer, pilot) = (getEngineer(), getPilot())
这在处理密切相关的值时尤其有用,比如矩形的坐标,并且可以帮助提高可读性。
最后,元组析构适合于交换值。现在,我要诚实地说:这种技巧在面试之外很少有用,即使在面试中,它也是一个相当糟糕的选择。然而,我想向你们展示它,因为我认为它展示了 Swift 是多么优雅。
那么,这里开始:给定两个整数 A 和 B ,如何在不使用第三个变量的情况下交换它们? 想一想,甚至可以在 Playground 上尝试一些代码。下面是用大多数语言解决这个问题的方法:
var a = 10
var b = 20
a=a+b
b=a-b
a=a-b
print(a)
print(b)
在Swift中,多亏了析构,你可以把它写在一行:
(b, a) = (a, b)
我认为她优雅、高效,而且相当漂亮。如果你在面试中被问到这个问题,你应该能够把它答好!
带标签的语句(Labeled statements)
标签已经使用了很长时间,但当开发人员开始对goto
不满时,它们在很大程度上就不受欢迎了。Swift 把它们带回来,但没有goto
:相反,它们与循环一起使用,让你更容易退出它们。
这里有一些代码可以创建一个字符串网格,并标记其中一个带有 x 的正方形,其中有一些宝藏 - 这是一个硬编码的位置,但在真实的游戏中,你显然会将其随机化。然后代码有两个循环试图找到宝藏,一个循环嵌套在另一个循环中:循环遍历板中的所有行,然后循环遍历每一行中的每一列。
这是代码:
var board = [[String]](repeating: [String](repeating: "",count: 10), count: 5)
board[3][5] = "x"
for (rowIndex, cols) in board.enumerated() {
for (colIndex, col) in cols.enumerated() {
if col == "x" {
print("Found the treasure at row \(rowIndex) col \(colIndex)!")
}
}
}
考虑到宝藏可以出现在面板上出现一次,这段代码是相当浪费的:即使在搜索的早期就发现了宝藏,它也会继续查找。如果你认为是时候部署 break
了,那么你是对的,至少在一定程度上是对的。它可能是这样的:
for (rowIndex, cols) in board.enumerated() {
for (colIndex, col) in cols.enumerated() {
if col == "x" {
print("Found the treasure at row \(rowIndex) col \(colIndex)!")
break
}
}
}
但是,break
只退出一个循环级别,因此它将退出for(colIndex,colo)
循环,然后继续运行for(rowIndex,cols)
循环。是的,它浪费的时间更少,但它仍在浪费一些。你可以添加一个布尔变量,在找到宝藏时将其设置为true
,然后你可以使用它来打破外部循环,但 Swift 有一个更好的解决方案:带标签的语句。
带标签的语句允许你为任何循环命名,这允许你在使用break
或continue
时引用特定循环。要创建标签,只需在任何循环之前写一个名称然后写一个冒号。然后,你可以使用break yourLabelName
或continue yourLabelName
直接引用它。
因此,编写该代码的最不浪费的方式是这样的:
var board = [[String]](repeating: [String](repeating: "",count: 10), count: 5)
board[5][3] = "x"
rowLoop: for (rowIndex, cols) in board.enumerated() {
for (colIndex, col) in cols.enumerated() {
if col == "x" {
print("Found the treasure at row \(rowIndex) col \(colIndex)!")
break rowLoop
}
}
}
它会立即跳出两个循环,并在for(rowIndex, cols)
循环结束后继续执行——非常好。
标记循环是聪明的,但是 Swift 更进一步:它允许你标记 if
语句,然后像标记循环一样从中中断。并且想要立即摆脱困境时,这是非常有用的,如果没有这些条件,你可能会得到一个由越来越多的缩进条件组成的金字塔。
下面是一个很好的例子,你可以看到它的实际应用:
if userRequestedPrint() {
if documentSaved() {
if userAuthenticated() {
if connectToNetwork() {
if uploadDocument("resignation.doc") {
if printDocument() {
print("Printed successfully!")
}
}
}
}
}
}
这段代码要经过一系列检查才能允许用户打印文档:不要尝试运行它,因为那些函数不是真的!
如果所有条件都为true
,那么你将看到打印成功!
标记语句允许你为if
语句创建早期返回。它们正常运行,但在你认为有必要的任何时候,你都可以退出任何条件语句。例如,我们可以将上面的金字塔重写为:
printing: if userRequestedPrint() {
if !documentSaved() { break printing }
if !userAuthenticated() { break printing }
if !connectToNetwork() { break printing }
if !uploadDocument("work.doc") { break printing }
if !printDocument() { break printing }
print("Printed successfully!")
}
这样占用的行数更少,对读取代码的人的缩进也更少,而且很快就能找到满意的解决方案。
如果你愿意,你甚至可以使用guard
来使你的意图更加清晰,就像这样:
printing: if userRequestedPrint() {
guard documentSaved() else { break printing }
guard userAuthenticated() else { break printing }
guard connectToNetwork() else { break printing }
guard uploadDocument("work.doc") else { break printing }
guard printDocument() else { break printing }
print("Printed successfully!")
}
为了可读性,我更喜欢测试正面条件,而不是反面条件。也就是说,我宁愿测试if documentsaved()
而不是if !documentsaved()
是因为它更容易理解,而guard
就是这样做的。
嵌套函数、类和结构(Nested functions, classes and structs)
Swift 允许你将一种数据类型嵌套到另一种数据类型中,例如结构体中的结构体,类中的枚举或函数中的函数。这是最常用来帮助你按照逻辑行为在心理上将事物分组在一起的方法,但有时会附加访问语义,以防止不正确地使用嵌套数据类型。
让我们首先处理简单的情况:使用嵌套类型进行逻辑分组。考虑下面的代码,它定义了一个名为London
的枚举:
enum London {
static let coordinates = (lat: 51.507222, long: -0.1275)
enum SubwayLines {
case bakerloo, central, circle, district, elizabeth, hammersmithCity, jubilee, metropolitan, northern, piccadilly, victoria, waterlooCity
}
enum Places {
case buckinghamPalace, cityHall, oldBailey, piccadilly, stPaulsCathedral
}
}
该枚举有一个称为coordinates
(坐标)的常量,然后是两个嵌套枚举:SubwayLines(地铁线路) 和 Places(位置)。但是,值得注意的是,它没有自己的case
——它只是被用作其他数据的包装器。
这样做有两个直接的好处:首先,任何具有代码完成功能的 IDE 都可以通过在键入时列出可能的选项(例如London.Places.cityHall
)来快速、方便地深入到特定的项。其次,因为你实际上是在创建名称空间常量,所以你可以使用Piccadilly
这样的合理名称,而不必担心你指的是地铁线路还是那个地方,或者你指的是 London Piccadilly 还是 Manchester Piccadilly。
如果你进一步扩展此技术,你将认识到你可以将其用于故事板 ID、表视图单元 ID 、图像名称,以及更有效地处理在苹果平台上非常流行的字符形式的资源类型。例如:
enum R {
enum Storyboards: String {
case main, detail, upgrade, share, help
}
enum Images: String {
case welcome, home, about, button
}
}
如果你能理解为什么我用 R 来表示它,那就更好了。要使该技术适用于图像,只需将图像命名为与枚举case
相同的名称,并在末尾加上 .png ,例如 about.png。
嵌套类型也适用于其他数据类型,例如,你可以有一个结构体,其中包含它自己的枚举:
struct Cat {
enum Breed {
case britishShortHair, burmese, persian, ragdoll, russianBlue, scottishFold, siamese
}
var name: String
var breed: Breed
}
你也可以把结构体放在结构体中,当它们一起使用时,例如:
struct Deck {
struct Card {
enum Suit {
case hearts, diamonds, clubs, spades
}
var rank: Int
var suit: Suit
}
var cards = [Card]()
}
正如你在最后一个示例中所看到的,你可以根据需要多次嵌套,结构体嵌套结构体再嵌套枚举是完全合法的。
嵌套语义(Nesting with semantics)
逻辑分组的嵌套不会阻止你引用任何嵌套类型,但是如果嵌套太多,就会有点麻烦:
let home = R.Images.home
let burmese = Cat.Breed.burmese
let hearts = Deck.Card.Suit.hearts
但是,Swift 允许你为嵌套类型分配访问控制修饰符,以控制它们的使用方式。当嵌套类型被设计为专门在其父级内部工作时,这很有用:如果Card
结构体只能由Deck
体结构使用,那么你需要访问控制。
警告:如果属性使用私有类型,则属性本身必须是私有的。为了演示,让我们再次看一下
Deck
示例:
struct Deck {
struct Card {
enum Suit {
case hearts, diamonds, clubs, spades
}
var rank: Int
var suit: Suit
}
var cards = [Card]()
}
如果我们希望Suit
枚举是私有的,以便只有Card
实例可以使用它,我们需要使用private enum Suit
。然而,这具有连锁效应,要求Card
的suit
属性也是私有的,否则它将在Suit
枚举不可用的地方进入。所以,更新之后的代码是这样的:
struct Deck {
struct Card {
private enum Suit {
case hearts, diamonds, clubs, spades
}
var rank: Int
private var suit: Suit
}
var cards = [Card]()
}
嵌套函数(Nested functions)
嵌套函数是嵌套类型访问控制的一个有趣的小例子,因为除非你您另有指定,否则它们将自动限制在其封闭函数中。Swift 将嵌套函数实现为命名闭包,这意味着它们将自动从其封闭函数中捕获值。
为了演示嵌套函数,我将创建一个函数,它使用三种距离计算技术之一计算两点之间的距离: 欧几里德(使用毕达哥拉斯定理)、欧几里德平方(使用毕达哥拉斯定理,但出于性能原因避免调用sqrt()
和曼哈顿距离。如果你不熟悉这些术语,“欧几里得距离”基本上是在两点之间画一条直线,“曼哈顿距离”使用直线几何来计算两个笛卡尔坐标的绝对差。
首先,我们要使用的类型的代码:
import Foundation
struct Point {
let x: Double
let y: Double
}
enum DistanceTechnique {
case euclidean
case euclideanSquared
case manhattan
}
我创建了自己的Point
类,以避免依赖于CGPoint
和 Core Graphics。我们将创建三个函数,每个函数都嵌套在一个父函数中。本章的重点不是解释距离计算,所以让我们快速地把它们排除在外:
func calculateEuclideanDistanceSquared(start: Point, end: Point) -> Double {
let deltaX = start.x - end.x
let deltaY = start.y - end.y
return deltaX * deltaX + deltaY * deltaY
}
func calculateEuclideanDistance(start: Point, end: Point) -> Double {
return sqrt(calculateEuclideanDistanceSquared(start: start, end: end))
}
func calculateManhattanDistance(start: Point, end: Point) -> Double {
return abs(start.x - end.x) + abs(start.y - end.y)
}
第一个函数calculateEuclideanDistanceSquared()
使用毕达哥拉斯定理(勾股定理)计算两点之间的直线距离。如果你上学已经有一段时间了,这个函数认为两点之间的 X 和 Y 是三角形的两条边,然后计算出三角形的斜边就是两点之间的距离。
第二个函数calculateEuclideanDistance()
建立在calculateEuclideanDistanceSquared()
函数的基础上,通过计算结果的平方根来给出真实距离。如果需要非常频繁地计算距离,例如每次用户的手指移动时,调用sqrt()
可能会影响性能,这就是calculateEuclideanDistanceSquared()
函数存在的原因。
最后,第三个函数是calculateManhattanDistance()
,它计算两个点的 X 和 Y 坐标之间的绝对距离之和,就好像你坐在一辆出租车上围绕城市中一个正方形街区行驶。
有了这三个嵌套函数,现在只需根据所要求的技术选择正确的选项:
switch technique {
case .euclidean:
return calculateEuclideanDistance(start: start, end: end)
case .euclideanSquared:
return calculateEuclideanDistanceSquared(start: start, end: end)
case .manhattan:
return calculateManhattanDistance(start: start, end: end)
}
就是这样!以下是完整的代码:
import Foundation
struct Point {
let x: Double
let y: Double
}
enum DistanceTechnique {
case euclidean
case euclideanSquared
case manhattan
}
func calculateDistance(start: Point, end: Point, technique: DistanceTechnique) -> Double {
func calculateEuclideanDistanceSquared(start: Point, end: Point) -> Double {
let deltaX = start.x - end.x
let deltaY = start.y - end.y
return deltaX * deltaX + deltaY * deltaY
}
func calculateEuclideanDistance(start: Point, end: Point) -> Double {
return sqrt(calculateEuclideanDistanceSquared(start: start, end: end))
}
func calculateManhattanDistance(start: Point, end: Point) -> Double {
return abs(start.x - end.x) + abs(start.y - end.y)
}
switch technique {
case .euclidean:
return calculateEuclideanDistance(start: start, end: end)
case .euclideanSquared:
return calculateEuclideanDistanceSquared(start: start, end: end)
case .manhattan:
return calculateManhattanDistance(start: start, end: end)
}
}
let distance = calculateDistance(start: Point(x: 10, y: 10), end: Point(x: 100, y: 100), technique: .euclidean)
现在,所有这些代码都是完全有效的,但它也比需要的更冗长。提醒一下,函数只是命名闭包,因此它们从其封闭函数中捕获任何值。
在这个上下文中,这意味着我们不需要让这三个嵌套函数接受任何参数,因为它们与封闭函数接受的参数相同——如果我们删除它们,它们就会被自动捕获。这有助于使我们的意图更清楚:这些嵌套函数只是在相同数据上操作的不同方式,而不是使用特定的值。
下面是calculateDistance()
函数重写之后的代码,这样它就可以从嵌套函数中删除参数,而是依赖于捕获:
func calculateDistance(start: Point, end: Point, technique: DistanceTechnique) -> Double {
func calculateEuclideanDistanceSquared() -> Double {
let deltaX = start.x - end.x
let deltaY = start.y - end.y
return deltaX * deltaX + deltaY * deltaY
}
func calculateEuclideanDistance() -> Double {
return sqrt(calculateEuclideanDistanceSquared())
}
func calculateManhattanDistance() -> Double {
return abs(start.x - end.x) + abs(start.y - end.y)
}
switch technique {
case .euclidean:
return calculateEuclideanDistance()
case .euclideanSquared:
return calculateEuclideanDistanceSquared()
case .manhattan:
return calculateManhattanDistance()
}
}
返回嵌套函数(Returning nested functions)
嵌套函数被自动限制在它们的封闭函数中,除非你另有指定,即如果你返回它们。记住,函数在 Swift 中属于一等公民 (first-class data types),因此你可以使用一个函数根据特定条件返回另一个函数。在我们的例子中,我们可以将calculatedDistance()
函数转换为createdDistanceAlgorithm()
,它只接受一个技术参数,并根据请求的技术返回其三个嵌套函数之一。
我知道这是显而易见的,值得重复的是,当你使用这种方法时,嵌套函数将不再是私有的——它将作为返回值返回给任何人使用。
下面是重写calculateDistance()
的代码,使它返回以下三个函数之一:
func createDistanceAlgorithm(technique: DistanceTechnique) -> (Point, Point) -> Double {
func calculateEuclideanDistanceSquared(start: Point, end: Point) -> Double {
let deltaX = start.x - end.x
let deltaY = start.y - end.y
return deltaX * deltaX + deltaY * deltaY
}
func calculateEuclideanDistance(start: Point, end: Point) -> Double {
return sqrt(calculateEuclideanDistanceSquared(start: start, end: end))
}
func calculateManhattanDistance(start: Point, end: Point) -> Double {
return abs(start.x - end.x) + abs(start.y - end.y)
}
switch technique {
case .euclidean:
return calculateEuclideanDistance
case .euclideanSquared:
return calculateEuclideanDistanceSquared
case .manhattan:
return calculateManhattanDistance
}
}
注意,这三个函数现在都需要接受参数,因为稍后将像这样调用它们:
let distanceAlgorithm = createDistanceAlgorithm(technique: .euclidean)
let distance = distanceAlgorithm(Point(x: 10, y: 10), Point(x: 100, y: 100))
文档标记(Documentation markup)
Swift 有特殊的语法,允许你将 Markdown 格式的文本嵌入到源代码中,源代码由 Xcode 解析并显示在 Quick Help 系统面板中——在编码时按 Alt+Cmd+2 将其显示在 Xcode 窗口的右侧。使用特殊格式的代码注释,你可以记录应该传入哪些参数、返回值将包含哪些内容、可以抛出的任何错误等等。
此文档与添加到特定代码中的常规内联注释不同。这些特殊的注释放在函数和类之前,用于在 Quick Help 和代码提示弹出窗口中显示信息,并进行了格式化,以便人类和 Xcode 都能阅读它们。
让我们先把简单的事情解决掉:除非你使用后面介绍的特殊关键字之一,否则你在 Markdown 注释中编写的所有内容都将在 Quick Help 面板中显示为描述文本。如果你刚开始输入文本,你所写的内容将在代码提示弹出窗口中作为简要描述使用。Xcode 通常可以在自动补全的空间中填入 20-30 个单词,但这对于实际使用来说太长了——目标是大约 10 个单词的简洁描述。
Markdown注释以/**开头,以*/结尾,像这样:
/**
Call this function to grok some globs.
*/
func myGreatFunction() {
// do stuff
}
在本文中,你可以使用一系列 Markdown 格式,如下所示:
/**
将文本放在`反引号`中以标记代码; 在你的键盘上这通常与波浪号〜共享一个键。
* 你可以用星号和空格开头来写项目描述。
* 缩进星号来创建子列表
1. 你可以以1.开始编写编号列表
1. 后续条目也可以以1.开始,Xcode会自动重新编号。
如果你想写一个链接,[把你的文本放在中括号里](链接放在小括号里)
# 标题以#号开始
## 副标题以##开头
### 子副标题以###开头,是你会遇到的最常见的标题样式
在文本前后写一个*星号*,使它成为斜体
在文本前后写**两个星号**,使它加粗
*/
文档关键字(Documentation keywords)
除了使用文本来描述函数外,Swift 还允许你添加在 Quick Help 窗格中显示的特殊关键字。有很多这样的工具,但是大多数都只显示一个标题和一些文本。我通常推荐六种有用的方法,你可以在几分钟内学会它们。
首先: Returns
关键字允许你指定调用者在函数成功运行时期望返回的值。请记住代码自动补全提示已经显示了返回值的数据类型,所以这个字段用于描述数据的实际含义——我们知道它是一个字符串,但是它将如何格式化?
- Returns: A string containing a date formatted as RFC-822
接下来是Parameter
关键字。这允许你指定参数的名称并描述它包含的内容。同样,代码自动补全提示会显示必须使用的数据类型,所以这是你提供一些细节的机会:"The name of a Taylor Swift album"。你可以包含尽可能多的参数行。
- Parameter album: The name of a Taylor Swift album
- Parameter track: The track number to load
第三个是Throws
关键字,它允许你指定一个用逗号分隔的错误类型列表,该函数可以抛出:
- Throws: LoadError.networkFailed, LoadError.writeFailed
第四个是Precondition
,它应该用于在调用函数之前描述程序的正确状态。如果使用纯函数,这个先决条件应该只依赖于传递给函数的参数,例如inputArray.count > 0
:
- Precondition: inputArray.count > 0
第五是Complexity
,它在 Swift 标准库中很流行。在 Quick Help 中,这不是特别格式化的,但是对于使用代码的其他人来说,这是有用的信息。这应该用大O符号来写,例如:
- Complexity: O(1)
最后是Authors
关键字,一开始听起来很有用,但我表示怀疑。可以想象,这用于将函数作者的名称写入 Quick Help 面板,当你需要确定应该向谁抱怨或表扬他们的工作时,这将非常有用。但是由于 Xcode 将Authors
放在Returns
、Throws
和Parameter
之前,添加一个认证信息只会把重要的字段往下推。尝试一下,看看你是怎么想的,但是请记住,文档首先是有用的。
- Authors: Paul Hudson
如果你在文档关键字之间包含更多的自由格式文本,那么它将在 Quick Help 中被正确地放置。
第二章:类型(Types)