精通-Swift2-全-
精通 Swift2(全)
原文:
zh.annas-archive.org/md5/af15f76d088cc2b66a28353180db35da译者:飞龙
前言
Swift 是苹果公司在 2014 年全球开发者大会(WWDC)上推出的新编程语言,与 Xcode 6 和 iOS 8 集成开发环境一同发布。Swift 可以说是 2014 年 WWDC 上最重要的公告,在此之前,包括苹果内部人士在内的很少有人知道这个项目的存在。
在 2015 年的 WWDC 大会上,苹果公司宣布了 Xcode 7 和 Swift 2,这是对 Swift 语言的重大增强。在 WWDC 期间,克里斯·拉特纳表示,许多增强功能都是基于苹果从开发社区直接收到的反馈。
Swift 可以被视为使用现代概念和安全编程模式重新构思的 Objective-C。用苹果自己的话说,Swift 就像是“没有 C 的 Objective-C”。Swift 的创造者克里斯说,Swift 从 Objective-C、Rust、Haskell、Ruby、Python、C#、CLU 以及太多其他语言中汲取了语言理念。
苹果公司还表示,“Swift 是 C 和 Objective-C 语言的继承者”。因此,对于希望保持技能更新的 iOS 和 OS X 开发者来说,不仅需要学习,还需要精通 Swift 编程语言。
本书的前五章将向读者介绍 Swift 编程语言。这些章节将使读者对 Swift 编程语言有一个坚实的基础理解。本书的其余部分将涵盖更高级的主题,如并发、网络开发、协议扩展和设计模式,这些将帮助读者掌握这门语言。
本书采用基于示例的方法编写,每个涵盖的主题都配有示例,这些示例旨在加强主题并展示如何在读者的代码中实现它。
由于 Swift 不断变化和发展,我已在masteringswift.blogspot.com/开设了一个博客,以保持读者对 Swift 最新动态的了解。该博客还将用于增强和扩展书中的材料。
本书涵盖内容
第一章,使用 Swift 迈出第一步,将向您介绍 Swift 编程语言,并讨论是什么启发了苹果公司创造 Swift。我们还将介绍 Swift 的基本语法以及如何使用 Playgrounds 进行实验和测试 Swift 代码。
第二章,学习变量、常量、字符串和运算符,将向您介绍 Swift 中的变量和常量以及何时使用它们。将简要概述最常见的变量类型,并提供如何使用它们的示例。我们将通过展示如何使用 Swift 语言中最常见的运算符来结束本章。
第三章,使用集合和 Cocoa 数据类型,将解释 Swift 的数组、集合和字典集合类型,并展示如何使用它们的示例。我们还将展示如何使用 Cocoa 和 Foundation 数据类型与 Swift 一起使用。
第四章,控制流和函数,将向您展示如何使用 Swift 的控制流语句。这包括循环、条件和控制转移语句。本章的后半部分全部关于函数,以及如何定义和使用它们。
第五章,类和结构体,专注于 Swift 的类和结构体。我们将探讨它们相似之处和不同之处。我们还将探讨访问控制和面向对象设计。我们将通过探讨 Swift 中的内存管理来结束本章。
第六章,使用协议和协议扩展,将详细介绍协议和协议扩展,因为协议对于 Swift 语言非常重要,对它们的深入了解将帮助我们编写灵活和可重用的代码。
第七章,使用可用性和错误处理编写更安全的代码,将深入探讨 Apple 在 Swift 2 中包含的新错误处理方法以及新的可用性功能。错误处理是响应和从错误条件中恢复的过程。
第八章,处理 XML 和 JSON 数据,将讨论 XML 和 JSON 数据是什么以及它们的用途。然后我们将看到几个使用 Apple 的框架解析和构建 XML 和 JSON 数据的示例。
第九章,自定义下标,将讨论我们如何在类、结构和枚举中使用自定义下标。Swift 中的下标可以用来访问集合中的元素。我们还可以为我们的类、结构和枚举定义自定义下标。
第十章,使用可选类型,将解释可选类型到底是什么,以及各种解包它们的方法和可选链。对于刚开始学习 Swift 的开发者来说,可选类型可能是更难以理解的项目之一。
第十一章,使用泛型,将解释 Swift 如何实现泛型。泛型允许我们编写非常灵活和可重用的代码,从而避免代码重复。
第十二章, 使用闭包,将教会我们如何在代码中定义和使用闭包。Swift 中的闭包与 Objective-C 中的块类似,但它们有更干净、更简单的语法使用方式。我们将以一个关于如何避免闭包中的强引用循环的章节来结束本章。
第十三章, 使用混合匹配,将解释混合匹配并演示我们如何在 Objective-C 项目中包含 Swift 代码,在 Swift 项目中包含 Objective-C 代码。鉴于有大量使用 Objective-C 编写的应用程序和框架,允许 Swift 和 Objective-C 代码协同工作是很重要的。
第十四章, Swift 中的并发与并行,将展示如何使用 Grand Central Dispatch 和操作队列来为我们的应用程序添加并发和并行性。理解和知道如何为我们的应用程序添加并发和并行性可以显著提升用户体验。
第十五章, Swift 格式化和风格指南,将为 Swift 语言定义一个风格指南,可以作为企业开发者创建风格指南的模板,因为大多数企业都有他们开发的各种语言的风格指南。
第十六章, 使用 Swift 进行网络开发,将解释如何使用 Apple API 连接到远程服务器以及如何最佳地使用它们。网络开发既可以有趣又具有挑战性。
第十七章, 在 Swift 中采用设计模式,将展示如何在 Swift 中实现一些更常见的设计模式。设计模式识别一个常见的软件开发问题并提供了解决该问题的策略。
你需要这本书
要跟随本书中的示例,你需要一台安装了 OS X 10.10 或更高版本的 Apple 电脑。你还需要安装 7.0 或更高版本的 Xcode,以及 2.0 或更高版本的 Swift。
这本书面向的对象
这本书旨在为寻找一本不仅能够为他们提供 Swift 编程语言的坚实基础,而且还会深入探讨高级主题,如自动引用计数(ARC)、设计模式、协议扩展和并发等主题的个人。这本书是为那些通过查看和操作代码来学习效果最好的开发者所写的,因为书中涵盖的每个概念都有示例代码支持,以帮助读者更好地理解当前主题并展示如何正确实现它。
术语
在这本书中,你会发现许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“如果我们有一个 OS X 游乐场打开,我们会使用一个NSColor对象来表示颜色。”
代码块设置如下:
var name = "Jon"
var language = "Swift"
var message1 = " Welcome to the wonderful world of "
var message2 = "\(name) Welcome to the wonderful world of \(language)!"
print(name, message1, language, "!")
print(message2)
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“在本书的多数示例中,您可以安全地假设您可以选择iOS或OS X,除非有其他说明。”
注意
警告或重要提示会以这样的框显示。
小贴士
小技巧和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大价值的标题。
要向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及书籍标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大价值。
下载示例代码
您可以从您在www.packtpub.com的账户下载所有已购买 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/6034OT_ColorImages.pdf。
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。
盗版
在互联网上对版权材料的盗版是一个横跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接mailto:copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。
我们感谢您的帮助,以保护我们的作者和为您提供有价值内容的能力。
问题
如果您对本书的任何方面有问题,您可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决问题。
第一章. 使用 Swift 的第一步
自从我 12 岁时用基本编程语言编写我的第一个程序以来,编程一直是我的一项热情。即使编程成为了我的职业,它始终更多的是一种热情,而不是工作,但过去几年,这种热情已经减弱。我不确定为什么我会失去这种热情。我试图通过一些我的副项目来重新找回它,但没有什么真正能让我找回曾经拥有的那种兴奋。然后,发生了一件美妙的事情!苹果宣布了 Swift。Swift 是如此令人兴奋且进步的语言,它重新激发了许多那种热情,并让我再次享受编程的乐趣。
在本章中,你将学习:
-
什么是 Swift
-
Swift 的一些特性
-
什么是游乐场
-
如何使用游乐场
-
Swift 语言的基本语法是什么
什么是 Swift?
Swift 是苹果在 2014 年的全球开发者大会(WWDC)上推出的新编程语言,与 Xcode 6 和 iOS 8 一起推出。Swift 可以说是 WWDC 2014 上最具影响力的公告,在此之前,包括苹果内部人士在内的很少有人知道这个项目的存在。
即使按照苹果的标准来看,他们能够将 Swift 保密这么长时间,而且没有人怀疑他们将要宣布一种新的开发语言,这本身就是令人难以置信的。在 2015 年的 WWDC 上,苹果再次引起轰动,当他们宣布了 Xcode 7 和 Swift 2。Swift 2 是 Swift 语言的重大增强。在这次会议上,克里斯·拉特纳表示,许多增强都是基于苹果从开发社区直接收到的反馈。
Swift 可以被看作是使用现代概念和安全编程模式重新构思的 Objective-C。用苹果自己的话说,Swift 就是没有 C 的 Objective-C。Swift 的创造者克里斯·拉特纳说,Swift 从 Objective-C、Rust、Haskell、Ruby、Python、C#、CLU 以及太多其他语言中汲取了语言理念。在 2014 年的 WWDC 上,苹果强调 Swift 默认是安全的。Swift 被设计用来消除许多常见的编程错误,使应用程序更加安全,更不容易出现错误。Swift 2 为语言增加了两个额外的核心功能——可用性和错误处理,这些功能旨在使编写安全代码变得更加容易。
Swift 的发展始于 2010 年,由克里斯发起。他仅用少数人意识到其存在的情况下,实现了大部分基本语言结构。直到 2011 年晚些时候,其他开发者才开始真正为 Swift 做出贡献,并在 2013 年 7 月,它成为了苹果开发者工具组的主要关注点。
克里斯于 2005 年夏天开始在苹果公司工作。他在开发者工具组中担任过多个职位,目前是该组的总监和架构师。在他的主页([www.nondot.org/sabre/](http://www.nondot.org/sabre/))上,他提到 Xcode 的 Playgrounds(本章稍后将对 Playgrounds 进行更多介绍)成为了他个人的热情所在,因为它使编程更加互动和易于接近。本书中将大量使用 Playgrounds 作为测试和实验平台。
Swift 和 Objective-C 之间有很多相似之处。Swift 采用了 Objective-C 的命名参数和动态对象模型的可读性。当我们说 Swift 拥有动态对象模型时,我们指的是类型在运行时可以改变的能力。这包括添加新的(自定义)类型以及修改/扩展现有类型。
Swift 还提供了对现有 Cocoa 和 Cocoa Touch 框架的无缝访问。这使得 Objective-C 开发者在开始学习 Swift 时感到一定的熟悉感,因为这些框架与 Objective-C 的功能方式与它们与 Swift 的功能方式相同。
虽然 Swift 和 Objective-C 之间有很多相似之处,但它们之间也存在一些显著的区别。Swift 的语法和格式与 Python 相比更接近,但苹果公司仍然保留了花括号。我知道 Python 的人可能会不同意我,这完全可以,因为我们都持有不同的观点,但我喜欢花括号。Swift 实际上要求控制语句(如 if 和 while)使用花括号,这消除了像苹果 SSL 库中的 goto fail 这样的错误。
Swift 还被设计成快速。在 2014 年的 WWDC 上,苹果展示了一系列基准测试,证明了 Swift 在性能上显著优于 Objective-C。Swift 使用 LLVM 编译器,该编译器包含在 Xcode 7 中,将 Swift 代码转换为高度优化的本地代码,以充分利用苹果的现代硬件。
如果你是一名 iOS 或 OS X 开发者,并且你仍然没有确信学习 Swift 是一个好主意,那么苹果 Swift 页面([https://developer.apple.com/swift/](https://developer.apple.com/swift/))上的这一段话可能会帮助你信服:
"Swift 是 C 和 Objective-C 语言的继承者。它包括低级原语,如类型、流程控制和运算符。它还提供了面向对象的功能,如类、协议和泛型,为 Cocoa 和 Cocoa Touch 开发者提供了他们所需求的高性能和强大功能。"
那段话的第一行,说 Swift 是 C 和 Objective-C 语言的继承者,是最重要的一行。这一行和其他苹果公司的文档告诉我们,苹果公司认为 Swift 语言是其未来的应用程序和系统编程语言。虽然 Objective-C 在不久的将来不会消失,但它听起来在不久的将来将让位给 Swift。
Swift 功能
当苹果公司说 Swift 是没有 C 的 Objective-C 时,他们实际上只告诉我们了一半的故事。Objective-C 是 C 的超集,并为 C 语言提供了面向对象的能力和动态运行时。这意味着苹果公司需要与 C 语言保持兼容性,这限制了它对 Objective-C 语言的增强。例如,苹果公司不能改变 switch 语句的功能,同时还要保持与 C 的兼容性。
由于 Swift 不需要像 Objective-C 那样保持相同的 C 兼容性,苹果公司可以自由地向语言添加任何功能/增强。这使得苹果公司能够包含当今许多最受欢迎和现代语言的最佳功能,例如 Objective-C、Python、Java、Ruby、C#、Haskell 以及许多其他语言。
以下图表显示了 Swift 包含的一些最令人兴奋的增强功能列表:
| Swift 功能 | 描述 |
|---|---|
| 类型推断 | Swift 可以根据初始值自动推断变量或常量的类型。 |
| 泛型 | 泛型允许我们只编写一次代码,为不同类型的对象执行相同任务。 |
| 集合可变性 | Swift 没有为可变或不可变容器提供单独的对象。相反,你通过定义容器为常量或变量来定义可变性。 |
| 闭包语法 | 闭包是包含功能性的自包含块,可以在我们的代码中传递和使用。 |
| 可选类型 | 可选类型定义了一个可能没有值的变量。 |
| Switch 语句 | Switch 语句得到了大幅改进。这是我最喜欢的改进之一。 |
| 多重返回类型 | 函数可以使用元组来拥有多个返回类型。 |
| 运算符重载 | 类可以提供现有运算符的自己的实现。 |
| 带关联值的枚举 | 在 Swift 中,我们可以使用枚举做很多不仅仅是定义一组相关值的事情。 |
在前面的图表中,我没有提到的一个功能是因为它从技术上讲不是 Swift 的功能;它是 Xcode 和编译器的功能。这个功能是 混合匹配,它允许我们创建包含 Objective-C 和 Swift 文件的应用程序。它还允许我们系统地使用 Swift 类更新当前的 Objective-C 应用程序,并在我们的 Swift 应用程序中使用当前的 Objective-C 库/框架。
小贴士
混合匹配让 Objective-C 和 Swift 文件可以在同一个项目中共存。这允许我们开始使用 Swift,而无需丢弃现有的 Objective-C 代码库或项目。
在我们开始探索 Swift 开发的奇妙世界之前,让我们绕道访问一个自从我还是个孩子就喜欢的地点——操场。
Playgrounds
当我还是个孩子的时候,学校一天中最美好的时光就是去操场。我们玩什么并不重要;只要我们在操场上,我就知道那会很有趣。当苹果公司在 Xcode 6 中引入 Playgrounds 作为其一部分时,单是这个名字就让我兴奋不已,但我怀疑苹果能否让它的 Playgrounds 和我童年的操场一样有趣。虽然苹果的 Playgrounds 可能不如我 9 岁时踢足球那么有趣,但它确实让实验和代码玩耍变得很有趣。
开始使用 Playgrounds
Playgrounds 是交互式的工作环境,让我们能够编写代码并立即看到结果。随着代码的更改,结果也会实时更改。这意味着 Playgrounds 是学习和实验 Swift 的绝佳方式。
Playgrounds 还使得尝试新的 API、原型化新的算法以及展示代码的工作方式变得极其简单。我们将在这本书中使用 Playgrounds 来展示我们的示例代码是如何工作的。因此,在我们真正开始 Swift 开发之前,让我们花些时间学习和熟悉 Playgrounds。
如果现在 Swift 代码看起来没有太多意义,请不要担心,随着我们阅读本书,这段代码将会变得有意义。我们只是现在试图对 Playgrounds 有一个感觉。
一个 Playgrounds 可以有多个部分,但在这本书中我们将使用的是以下三个:
-
编码区域:这是你输入 Swift 代码的地方。
-
结果侧边栏:这是显示你代码结果的地方。每次你输入一行新代码时,结果都会重新评估,并且结果侧边栏会更新为新结果。
-
调试区域:这个区域显示代码的输出,这对于调试非常有用。
下面的截图显示了 Playgrounds 中部分的排列方式:

让我们开始一个新的 Playgrounds。我们首先需要做的是启动 Xcode。一旦 Xcode 启动,我们可以选择开始使用 Playgrounds选项,如下面的截图所示:

或者,我们可以通过从顶部菜单栏的文件 | 新建进入 Playgrounds,如下面的截图所示:

接下来,我们应该看到一个类似于以下截图的屏幕,允许我们命名我们的 Playgrounds 并选择它是一个iOS还是OS X Playgrounds。
在这本书的大部分示例中,除非另有说明,否则可以安全地假设你可以选择iOS或OS X:

最后,我们会询问保存 Playground 的位置。在选择了位置之后,Playground 将打开并看起来类似于以下屏幕截图:

在前面的屏幕截图中,我们可以看到 Playground 的编码区域看起来与 Xcode 项目的编码区域相似。这里不同的是右侧的侧边栏。这个侧边栏是我们代码结果显示的地方。前一个屏幕截图中的代码导入了 iOS 的 UIKit 框架,并将一个名为str的变量设置为字符串Hello, playground。你可以在代码右侧的侧边栏中看到str字符串的内容。
默认情况下,新的 Playground 不会打开调试区域。你可以通过同时按下shift + command + Y键来手动打开它。在章节的后面,我们将看到为什么调试区域如此有用。
iOS 和 OS X Playgrounds
当你启动一个新的 iOS Playground 时,Playground 会导入 UIKit(Cocoa Touch)。这使我们能够访问为 iOS 应用程序提供核心基础设施的 UIKit 框架。当我们启动一个新的 OS X Playground 时,Playground 会导入 Cocoa。这使我们能够访问 OS X Cocoa 框架。
最后一段的意思是,如果我们想实验 UIKit 或 Cocoa 的特定功能,我们需要打开正确的 Playground。例如,如果我们有一个 iOS Playground 打开,并且我们想创建一个表示颜色的对象,我们会使用一个UIColor对象。如果我们有一个 OS X playground 打开,我们会使用一个NSColor对象来表示颜色。
在 Playground 中显示图片
正如你将在整本书中看到的那样,Playgrounds 擅长在结果侧边栏中以文本形式显示代码的结果。然而,它们还能做很多不仅仅是文本的事情,比如图片、图表和显示视图。让我们看看如何在 Playground 中显示一个图片。首先,我们需要做的是将图片加载到 Playground 的资源目录中。
以下步骤展示了如何将图片加载到资源目录中:
-
让我们从显示项目导航器侧边栏开始。要做到这一点,在顶部菜单栏中,导航到视图 | 导航器 | 显示项目导航器或使用command + 1键盘快捷键。项目导航器看起来像这样:
![在 Playground 中显示图片]()
-
一旦我们打开了项目导航器,我们可以将图片拖入
Resources文件夹,这样我们就可以在代码中访问它。一旦我们将图片文件拖到上面并放下,它就会出现在Resources文件夹中,如图所示:![在 Playground 中显示图片]()
-
现在,我们可以访问代码中
Resources文件夹内的图片。以下截图显示了如何进行操作。我们使用来访问图片的实际代码在此阶段并不那么重要,重要的是要知道如何在沙盒中访问资源:![在沙盒中显示图片]()
-
要查看图片,我们需要将鼠标光标悬停在结果侧边栏中显示图片宽度和高度的章节上。在我们的例子中,宽度和高度部分显示为 w 256 h 256。一旦我们将鼠标指针悬停在宽度和高度上,我们应该看到两个符号,如下面的截图所示:
![在沙盒中显示图片]()
-
我们可以按任意一个符号来显示图片。形状像带加号的圆圈的符号将在沙盒的代码部分显示图片,而看起来像眼睛的符号则会在沙盒外部弹出图片。以下截图显示了按下带加号的圆圈时显示的内容:
![在沙盒中显示图片]()
当我们想要看到代码的进展时,能够创建和显示图表非常有用。让我们看看如何在沙盒中创建和显示图表。
在沙盒中创建和显示图表
我们还可以在时间上绘制数值变量的值。当我们在原型设计新算法时,这个特性非常有用,因为它允许我们查看变量在整个计算过程中的值。
要了解图表是如何工作的,请查看以下沙盒:

在这个沙盒中,我们将变量 j 设置为 1。接下来,我们创建一个 for 循环,将数字 1 到 5 分配给变量 i。在 for 循环的每个步骤中,我们将变量 j 的值设置为当前 j 的值乘以 i。图表显示了 for 循环每个步骤中变量 j 的值。我们将在本书的后面详细讲解 for 循环。
要显示图表,请点击形状像带点的圆圈的符号。然后我们可以移动时间轴滑块来查看变量 j 在 for 循环的每个步骤的值。
什么是沙盒不是
我们可以用沙盒做很多事情,而我们在这里的快速介绍中只是触及了皮毛。随着我们继续阅读本书,我们将几乎在所有示例代码中使用沙盒,并展示沙盒的其他功能,正如它们被使用时一样。
在我们结束这个简短的介绍之前,让我们看看沙盒不是什么,这样我们就可以了解何时不使用沙盒:
-
沙盒不应该用于性能测试:在沙盒中运行的任何代码的性能并不能代表代码在项目中的运行速度
-
Playgrounds 不支持用户交互:用户不能与在 Playground 中运行的代码进行交互
-
Playgrounds 不支持设备上的执行:你不能将 Playground 中存在的代码作为外部应用程序或在外部设备上运行
Swift 语言语法
如果你是一个 Objective-C 开发者,并且你对现代语言如 Python 或 Ruby 不熟悉,那么之前截图中的代码可能看起来相当奇怪。Swift 语言的语法与基于 Smalltalk 和 C 的 Objective-C 有很大的不同。
Swift 语言使用非常现代的概念和语法来创建非常简洁和可读的代码。还非常强调消除常见的编程错误。在我们深入了解 Swift 语言本身之前,让我们看看 Swift 语言的一些基本语法。
注释
在 Swift 代码中编写注释与在 Objective-C 代码中编写注释略有不同。我们仍然使用双斜杠//进行单行注释,使用/*和*/进行多行注释。
发生变化的是我们如何记录参数和返回值。要记录任何参数,我们使用:parm:字段,而对于返回值,我们使用:return:字段。
下面的 Playground 展示了如何正确注释函数的示例,包括单行和多行注释:

要编写好的注释,我建议在函数内部使用单行注释,以快速给出代码的一行解释。然后我们将使用函数和类外部多行注释来解释函数和类的作用。前面的 Playground 展示了注释的良好使用。通过使用适当的文档,就像我们在前面的截图中所做的那样,我们可以在 Xcode 中使用文档功能。如果我们按住option键然后点击代码中的任何地方的功能名,Xcode 将显示一个弹出窗口,显示函数的描述。
下一个截图显示了该弹出窗口可能看起来类似的样子:

这张截图显示了如果我们按住option键然后点击myAdd()方法,Xcode 的文档功能。我们可以看到文档包含五个字段。这些字段是:
-
声明:这是函数的声明
-
描述:这是函数在注释中出现的描述
-
参数:参数描述在注释部分以
:param:标签开头 -
返回:返回描述在注释部分以
:return:标签开头 -
声明于:这是函数声明的文件,这样我们就可以轻松找到它
分号
你可能已经注意到了,到目前为止的代码示例中,我们没有在行尾使用分号。在 Swift 中,分号是可选的;因此,以下 Playground 中的两行在 Swift 中都是有效的。你可以在结果侧边栏中看到代码的结果,如下面的截图所示:

为了风格上的考虑,强烈建议你在 Swift 代码中不要使用分号。如果你真的想在代码中使用分号,那么请保持一致性,并在每一行代码中使用它们;然而,Swift 不会警告你如果忘记了分号。我再次强调,建议你在 Swift 中不要使用分号。
括号
在 Swift 中,条件语句周围的括号是可选的,例如,以下 Playground 中的两个 if 语句都是有效的。你可以在侧边栏中看到代码的结果,如下面的截图所示:

为了风格上的考虑,建议你除非在同一行上有多个条件语句,否则不要在代码中包含括号。为了可读性,将括号放在同一行上的单个条件语句周围是一个好的做法。
请参阅以下 Playground 中的示例:

大括号
在 Swift 中,与大多数其他语言不同,语句后面需要大括号。这是 Swift 中内置的安全特性之一。可以说,如果开发者使用了大括号,可能已经避免了无数的安全漏洞。一个很好的例子是苹果的 goto fail 漏洞。这些漏洞也可以通过其他方式预防,例如单元测试和代码审查,但在我看来,要求开发者使用大括号是一个好的安全标准。
以下 Playground 显示了如果你忘记包含大括号会得到什么错误:

赋值运算符不返回值
在大多数其他语言中,以下代码行是有效的,但这可能不是开发者想要执行的操作:
if (x = 1) {}
小贴士
下载示例代码
你可以从你购买的所有 Packt 出版物书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问 www.packtpub.com/support 并注册以直接将文件通过电子邮件发送给你。
在 Swift 中,这个语句是无效的。在条件语句(if 和 while)中使用赋值运算符(=)会引发错误。这是 Swift 中内置的另一个安全特性。它防止开发者忘记比较语句中的第二个等号(=)。以下 Playground 中显示了此错误:

条件语句和赋值语句中的空白是可选的
对于条件(if和while)和赋值(=)语句,空白是可选的。因此,在下面的 Playground 中,代码的The i block和The j block都是有效的:

注意
为了风格上的考虑,我建议添加空白(例如,为了可读性,添加The j block),但只要您选择一种风格并保持一致,任何风格都应该是可以接受的。
Hello World
所有旨在教授计算机语言的优秀计算机书籍都有一个部分,展示了用户如何编写 Hello World 应用程序。这本书也不例外。在本节中,我们将向您展示如何编写两个不同的 Hello World 应用程序。
我们的第一款 Hello World 应用程序将是传统的 Hello World 应用程序,它只是简单地打印 Hello World 到控制台。让我们首先创建一个新的 Playground,并将其命名为Chapter_1_Hello_World。这个 Playground 可以是 iOS 或 OS X 的 Playground。
在 Swift 中,要将消息打印到控制台,我们使用print()函数。print()函数在 Swift 2 中得到了极大的增强。在 Swift 2 之前,我们有两个独立的 print 函数:print()和println()。现在这两个函数都被合并成了单个print()函数。
在最基本的形式中,要打印一条单独的消息,我们会使用如下所示的 print 函数:
print("Hello World")
通常,当我们使用print()函数时,我们不仅想打印静态文本。我们可以使用特殊的字符序列\( ),或者通过在print()函数中用逗号分隔值,来包含变量的值和/或常量。以下代码展示了如何做到这一点:
var name = "Jon"
var language = "Swift"
var message1 = " Welcome to the wonderful world of "
var message2 = "\(name) Welcome to the wonderful world of \(language)!"
print(name, message1, language, "!")
print(message2)
我们也可以在 print 函数中定义两个参数,这两个参数会改变消息在控制台中的显示方式。这些参数是分隔符和终止符参数。分隔符参数定义了一个字符串,用于在print()函数中分隔变量/常量的值。默认情况下,print()函数使用空格分隔每个变量/常量。终止符参数定义了在行尾放置的字符。默认情况下,会在行尾添加换行符。
以下代码展示了如何创建一个没有换行符结尾的逗号分隔列表:
var name1 = "Jon"
var name2 = "Kim"
var name3 = "Kailey"
var name4 = "Kara"
print(name1, name2, name3, name4, separator:", ", terminator:"")
我们还可以向我们的print()函数添加另一个参数。这是toStream参数。这个参数将允许我们将print()函数的输出重定向。在以下示例中,我们将输出重定向到名为line的变量:
var name1 = "Jon"
var name2 = "Kim"
var name3 = "Kailey"
var name4 = "Kara"
var line = ""
print(name1, name2, name3, name4, separator:", ", terminator:"", toStream:&line)
print()函数过去只是一个有用的基本调试工具,但现在随着新的增强print()函数,我们可以用它做更多的事情。
摘要
在本章中,我们向您展示了如何启动和使用 Playgrounds 来进行 Swift 编程的实验。我们还介绍了 Swift 语言的基本语法,并讨论了合适的语言风格。本章以两个“Hello World”示例结束。
在下一章中,我们将了解如何在 Swift 中使用变量和常量。我们还将探讨各种数据类型以及如何在 Swift 中使用运算符。
第二章:学习变量、常量、字符串和操作符
我写的第一个程序是用 BASIC 编程语言编写的,是一个典型的 Hello World 应用程序。这个应用程序一开始非常令人兴奋,但打印静态文本的兴奋感很快就消失了。对于我的第二个应用程序,我使用了 BASIC 的输入命令来提示用户输入姓名,然后打印出包含用户姓名的定制问候消息。12 岁时,显示“Hello Han Solo”非常酷。这个应用程序引导我创建了无数类似 Mad Lib 的应用程序,这些应用程序会提示用户输入各种单词,然后在用户输入所有必需的单词后,将这些单词放入显示的故事中。这些应用程序让我了解了变量的重要性。从那时起,我创建的每个有用的应用程序都使用了变量。
在本章中,我们将涵盖以下主题:
-
变量和常量的定义
-
显式类型和隐式类型之间的区别
-
解释数字、字符串和布尔类型
-
定义可选类型
-
解释 Swift 中枚举的工作原理
-
解释 Swift 操作符的工作原理
常量和变量
常量和变量将一个标识符(如 myName 或 currentTemperature)与特定类型的值(如 String 或 Int)关联起来,其中标识符可以用来检索值。常量和变量之间的区别在于,变量可以被更新或更改,而一旦为常量分配了值,它就不能被更改。
常量适用于定义那些你知道永远不会改变的值,例如水的冰点或光速。常量也适用于定义我们在整个应用程序中多次使用的值,例如标准字体大小或缓冲区中的最大字符数。本书中将有多个常量示例。
变量在软件开发中比常量更常见。这主要是因为开发者倾向于更喜欢变量而不是常量。在 Swift 2 和 Xcode 7 中,如果我们声明了一个永远不会改变的变量,我们会收到警告。这应该会增加常量的使用。我们可以不使用常量来创建有用的应用程序(尽管使用它们是一种好习惯);然而,没有变量几乎不可能创建一个有用的应用程序。
注意
在 Swift 中鼓励使用常量。如果我们不期望或不想值发生变化,我们应该将其声明为常量。这为我们的代码添加了一个非常重要的安全约束,确保值永远不会改变。
你几乎可以在变量或常量的标识符中使用任何字符(甚至 Unicode 字符);然而,你必须遵循一些规则:
-
标识符不能包含任何空白字符
-
标识符不能包含任何数学符号
-
标识符不能包含任何箭头
-
标识符不能包含私有用途或无效的 Unicode 字符
-
标识符不能包含行或框绘制字符
-
标识符不能以数字开头,但可以包含数字
-
如果你将 Swift 关键字用作标识符,请用反引号包围它
注意
关键字是 Swift 编程语言使用的单词。在本章中,你将看到的几个关键字示例是 var 和 let。你应该避免使用 Swift 关键字作为标识符,以避免阅读代码时产生混淆。
定义常量和变量
常量和变量在使用之前必须定义。要定义一个常量,你使用关键字 let,要定义一个变量,你使用关键字 var。以下是一些常量和变量的示例:
// Constants
let freezingTemperatureOfWaterCelsius = 0
let speedOfLightKmSec = 300000
// Variables
var currentTemperature = 22
var currentSpeed = 55
我们可以在一行中声明多个常量或变量,通过逗号分隔它们。例如,我们可以将前面的四行代码缩减为两行,如下所示:
// Constants
let freezingTempertureOfWaterCelsius = 0, speedOfLightKmSec = 300000
// Variables
var currentTemperture = 22, currentSpeed = 55
我们可以将变量的值更改为兼容类型的另一个值;然而,正如我们之前提到的,我们不能更改常量的值。让我们看看以下 Playgrounds。你能从以下截图显示的错误消息中告诉代码有什么问题吗?

你弄清楚代码哪里出问题了吗?任何物理学家都会告诉你,我们无法改变光速,在我们的代码中,speedOfLightKmSec 变量是一个常量,所以我们也不能改变它。因此,当我们尝试改变 speedOfLightKmSec 常量时,会报告一个错误。我们能够不报错地改变 highTemperture 变量的值,因为它是一个变量。我们提到变量和常量的区别好几次,因为这是一个非常重要的概念,尤其是在我们定义可变和不可变集合类型时,这在第三章,使用集合和 Cocoa 数据类型中尤为重要。
类型安全
Swift 是一种类型安全的语言。在类型安全的语言中,我们必须清楚地了解存储在变量中的值的类型。如果我们尝试将错误类型的值分配给变量,我们将得到一个错误。以下 Playgrounds 展示了如果我们尝试将字符串值放入期望整数值的变量中会发生什么;注意,我们将在本章稍后讨论最流行的类型:

Swift 在编译代码时执行类型检查;因此,它将用错误标记任何不匹配的类型。这个 Playgrounds 中的错误消息清楚地解释了我们在尝试将字符串字面量插入到整数变量中。
所以问题是,Swift 如何知道 integerVar 是 Int 类型?Swift 使用类型推断来确定合适的类型。让我们看看什么是类型推断。
类型推断
类型推断允许我们在定义变量时省略变量类型。编译器将根据初始值推断类型。例如,在 Objective-C 中,我们会这样定义一个整数:
int myInt = 1
这告诉编译器 myInt 变量是 Int 类型,初始值是数字 1。在 Swift 中,我们会这样定义相同的整数:
var myInt = 1
Swift 推断变量类型是整数,因为初始值是整数。让我们看看几个更具体的例子:
var x = 3.14 // Double type
var y = "Hello" // String type
var z = true // Boolean type
在前面的例子中,编译器将正确推断变量 x 是 Double,变量 y 是 String,变量 z 是 Boolean,基于初始值。
显式类型
类型推断是 Swift 中一个非常棒的功能,你可能会很快习惯它;然而,有时我们可能希望显式定义变量的类型。例如,在前面的例子中,变量 x 被推断为 Double,但如果我们想将变量类型设置为 Float 呢?我们可以像这样显式定义变量类型:
var x : Float = 3.14
注意变量标识符后面的 : Float(冒号和单词 Float);这告诉编译器将变量定义为 Float 类型,并给它一个初始值 3.14。当我们以这种方式定义变量时,我们需要确保初始值与我们要定义的变量类型相同。如果我们尝试给变量一个初始值,即与我们定义的类型不同的类型,我们将收到一个错误。
如果我们没有设置初始值,我们也会显式定义变量类型。例如,以下代码行是无效的,因为编译器不知道将变量 x 设置为哪种类型:
var x
如果我们在应用程序中使用此代码,我们将收到一个 Type annotation missing in pattern 错误。如果我们没有为变量设置初始值,我们必须像这样定义类型:
var x: Int
现在我们已经看到了如何显式定义变量类型,让我们来看看一些最常用的类型。
数值类型
Swift 包含许多适合存储各种整数和浮点值的标准数值类型。
整数
整数是一个整数。整数可以是带符号的(正数、负数或零)或无符号的(正数或零)。Swift 提供了多种不同大小的整数类型。以下图表显示了不同整数类型的值范围:
| 类型 | 最小值 | 最大值 |
|---|---|---|
| Int8 | -128 | 127 |
| Int16 | -32,768 | 32,767 |
| Int32 | -2,147,483,648 | 2,147,483,647 |
| Int64 | - 9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
| Int | - 9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
| UInt8 | 0 | 255 |
| UInt16 | 0 | 65,535 |
| UInt32 | 0 | 4,294,967,295 |
| UInt64 | 0 | 18,446,744,073,709,551,615 |
| UInt | 0 | 18,446,744,073,709,551,615 |
提示
除非有特定的理由来定义整型的大小,否则我建议使用标准的 Int 或 UInt 类型。这将让您免于需要在不同的整型类型之间进行转换。
在 Swift 中,Int(以及其他数值类型)实际上是命名类型,使用 Swift 标准库中的结构体实现。这为我们提供了对所有数据类型以及我们可以访问的属性进行内存管理的一致机制。对于前面的图表,我使用 min 和 max 属性检索了每种整型类型的最大和最小值。请查看以下 Playground,看看我是如何检索这些值的:

整数也可以表示为二进制、八进制和十六进制数。我们只需在数字前添加一个前缀,告诉编译器数字应该使用哪种基数。以下图表显示了每种数值基数的前缀:
| 基数 | 前缀 |
|---|---|
| 十进制 | 无 |
| 二进制 | 0b |
| 八进制 | 0o |
| 十六进制 | 0x |
以下 Playground 展示了数字 95 在每种数值基数中的表示方式:

Swift 还允许我们在数值字面量中插入任意下划线。这可以提高我们代码的可读性。例如,如果我们正在定义光速,这是一个常数,我们可以这样定义它:
let speedOfLightKmSec = 300_000
Swift 会忽略这些下划线;因此,它们不会以任何方式影响数值字面量的值。
浮点数
浮点数是带有小数部分的数字。Swift 中有两种标准的浮点数类型:Float 和 Double。Float 表示 32 位浮点数,而 Double 表示 64 位浮点数。Swift 还支持扩展的浮点数类型,即 Float80。Float80 类型是一个 80 位浮点数。
建议我们使用 Double 类型而不是 Float 类型,除非有特定的理由使用后者。Double 类型至少有 15 位十进制数字的精度,而 Float 类型的精度可能低至六位十进制数字。让我们看看一个例子,看看这如何在不为我们所知的情况下影响我们的应用程序。以下截图显示了如果我们将两个十进制数字相加并将结果放入 Float 类型和 Double 类型中的结果:

如从截图所示,我们相加的两个十进制数字在十进制点后有九位数字;然而,Float 类型的结果只包含七位数字,而 Double 类型的结果包含完整的九位数字。
如果我们在处理货币或其他需要精确计算的数字时精度丢失,可能会引发问题。浮点精度问题并不仅限于 Swift;所有实现 IEEE 754 浮点标准的语言都有类似的问题。最佳实践是,除非有特定原因不这样做,否则使用 Double 来表示浮点数。
如果我们有两个变量,一个是 Int 类型,另一个是 Double 类型?你认为我们能否像以下代码所示那样将它们相加:
var a : Int = 3
var b : Double = 0.14
var c = a + b
如果我们将前面的代码放入 Playground 中,我们会收到以下错误:binary operator '+' cannot be applied to operands of type 'Int' and 'String'
这个错误告诉我们我们正在尝试将两种不同类型的数字相加,这是不允许的。要将 Int 和 Double 相加,我们需要将 Int 值转换为 Double 值。以下代码展示了如何将 Int 值转换为 Double 值以便我们可以将它们相加:
var a : Int = 3
var b : Double = 0.14
var c = Double(a) + b
注意我们是如何使用Double()函数将 Int 值转换为 Double 值的。Swift 中的所有数值类型都有一个类似于前面代码示例中的Double()函数的便利初始化器。例如,以下代码展示了如何将Int变量转换为Float和UInt16变量:
var intVar = 32
var floatVar = Float(intVar)
var uint16Var = UInt16(intVar)
布尔类型
布尔值通常被称为逻辑值,因为它们可以是true或false。Swift 有一个内置的布尔类型叫做 Bool,它接受两个内置布尔常量之一。这些常量是true和false。
布尔常量和变量可以定义为如下所示:
let swiftIsCool = true
let swiftIsHard = false
var itIsWarm = false
var itIsRaining = true
布尔值在处理条件语句,如if和while时特别有用。例如,你认为以下代码会做什么:
let isSwiftCool = true
let isItRaining = false
if (isSwiftCool) {
print("YEA, I cannot wait to learn it")
}
if (isItRaining) {
print("Get a rain coat")
}
如果你回答说这段代码会打印出YEA, I cannot wait to learn it,那么你就对了。因为isSwiftCool被设置为 true,所以会打印出YEA, I cannot wait to learn it信息,但isItRaining是 false;因此,不会打印出Get a rain coat信息。
你也可以像这样从比较运算符中分配布尔值:
var x = 2, y = 1
var z = x > y
在前面的代码中,z是一个包含布尔true值的布尔变量,因为2大于1。
字符串类型
字符串是一个有序字符集合,例如Hello或Swift。在 Swift 中,字符串类型表示字符串。我们已经在这本书中看到了几个字符串的例子,所以以下代码应该看起来很熟悉。此代码展示了如何定义两个字符串:
var stringOne = "Hello"
var stringTwo = " World"
由于字符串是一个字符的有序集合,我们可以遍历字符串中的每个字符。以下代码展示了如何实现这一点:
varstringOne = "Hello"
for char in stringOne.characters {
print(char)
}
以下代码将显示以下截图所示的结果:

有两种方法可以将一个字符串添加到另一个字符串中。我们可以连接它们或内联包含它们。要连接两个字符串,我们使用+或+=运算符。以下代码展示了如何连接两个字符串。第一个示例将stringB追加到stringA的末尾,并将结果放入新的stringC变量中。第二个示例直接将stringB追加到stringA的末尾,而不创建新的字符串:
var stringC = stringA + stringB
stringA += stringB
要将一个字符串内联包含在另一个字符串中,我们使用一个特殊的字符序列\( )。以下代码展示了如何将字符串内联包含在另一个字符串中:
var stringA = "Jon"
var stringB = "Hello \(stringA)"
在上一个示例中,stringB将包含消息Hello Jon,因为 Swift 会将\(stringA)字符序列替换为stringA的值。
在 Swift 中,我们使用var和let关键字来定义变量和集合的可变性。如果我们使用var定义一个字符串为变量,则该字符串是可变的;这意味着我们可以更改和编辑字符串的值。如果我们使用let定义一个字符串为常量,则该字符串是不可变的,这意味着一旦设置,我们无法更改或编辑其值。以下代码展示了可变字符串和不可变字符串之间的区别:
var x = "Hello"
let y = "HI"
var z = " World"
//This is valid, x is mutable
x += z
//This is invalid, y is not mutable.
y += z
Swift 中的字符串有三个计算属性,可以将字符串的字母大小写进行转换。这些属性是capitalizedString、lowercaseString和uppercaseString。以下示例演示了这些属性:
var stringOne = "hElLo"
print("capitalizedString: " + stringOne.capitalizedString)
print("lowercaseString: " + stringOne.lowercaseString)
print("uppercaseString: " + stringOne.uppercaseString)
如果我们运行此代码,结果将如下所示:
capitalizedString: Hello
lowercaseString: hello
uppercaseString: HELLO
Swift 提供了四种比较字符串的方法;这些是string equality(字符串相等性)、prefix equality(前缀相等性)、suffix equality(后缀相等性)和isEmpty(是否为空)。以下示例演示了这些方法:
var stringOne = "Hello Swift"
var stringTwo = ""
stringOne.isEmpty //false
stringTwo.isEmpty //true
stringOne == "hello swift" //false
stringOne == "Hello Swift" //true
stringOne.hasPrefix("Hello") //true
stringOne.hasSuffix("Hello") //false
我们可以用另一个字符串替换目标字符串的所有出现。这是通过stringByReplacingOccurrencesOfString()方法完成的。以下代码演示了这一点:
var stringOne = "one,to,three,four"
print(stringOne.stringByReplacingOccurrencesOfString("to", withString: "two"))
上述示例将打印one,two,three,four到屏幕上,因为我们正在将所有to的出现替换为two。
我们也可以从我们的字符串中检索子字符串和单个字符。以下示例展示了各种实现方式:
var path = "/one/two/three/four"
//Create start and end indexes
var startIndex = path.startIndex.advancedBy(4)
var endIndex = path.startIndex.advancedBy(14)
path.substringWithRange(Range(start:startIndex, end:endIndex)) //returns the String /two/three
path.substringToIndex(startIndex) //returns the String /one
path.substringFromIndex(endIndex) //returns the String /four
path.characters.last //returns the last character in the String which is r
path.characters.first //returns the first character in the String which is /
在上述示例中,我们使用substringWithRange()函数来检索从起始索引到结束索引之间的子字符串。索引是通过startIndex.advanceBy()函数创建的。startIndex属性返回字符串中第一个字符的索引,然后我们使用advancedBy()方法将索引推进到所需的位置数。
substringToIndex()函数从字符串的开始创建到索引的子字符串。substringFromIndex()函数从索引创建到字符串末尾的子字符串。然后我们使用last属性获取字符串的最后一个字符,使用first属性获取第一个字符。
我们可以通过使用count属性来检索字符串中的字符数。以下示例展示了如何使用此功能:
var path = "/one/two/three/four"
var length = path.characters.count
这完成了我们对字符串的快速浏览。我知道我们非常快地浏览了这些属性和函数,但在这本书中我们将广泛使用字符串,所以我们将有大量时间来熟悉它们。
可选变量
我们到目前为止所查看的所有变量都被认为是非可选变量。这意味着变量必须有一个非 nil 值;然而,有时我们希望或需要我们的变量包含nil值。这可能会发生在我们从一个操作失败的函数返回nil值或找不到值时。
在 Swift 中,可选变量是一个我们可以将其赋值为nil(无值)的变量。可选变量和常量使用?(问号)定义。让我们看看以下 Playground;它展示了如何定义Optional以及如果我们给一个Non-Optional变量赋值nil会发生什么:

注意当我们尝试将nil值赋给非可选变量时收到的错误。这个错误信息告诉我们stringTwo变量不符合NilLiteralConvertible协议。当我们看到这个错误时,请记住这意味着我们正在将nil值赋给一个未定义为可选类型的变量或常量。
可选变量被添加到 Swift 语言中作为一个安全特性。它们提供了一个编译时检查,以验证我们的变量是否包含一个有效值。除非我们的代码明确将变量定义为可选的,否则我们可以假设变量包含一个有效值,我们不需要检查nil值。由于我们能够在初始化之前定义变量,这可能会在非可选变量中产生nil值;然而,编译器会检查这一点。以下 Playground 展示了如果我们尝试在初始化之前使用非可选变量时收到的错误:

为了验证一个可选变量或常量是否包含一个有效的(非 nil)值,我们首先可能想到使用!=(不等于)运算符来验证变量不等于nil,但还有其他方法。这些其他方法包括Optional Binding和Optional Chaining。在我们介绍可选绑定和可选链之前,让我们看看如何使用!=(不等于)运算符以及什么是强制解包。
要使用强制解包,我们首先必须确保可选变量包含一个非 nil 值,然后我们可以使用感叹号来访问该值。以下示例展示了我们如何做到这一点:
var name: String?
Name = "Jon"
if name != nil {
var newString = "Hello " + name!
}
在这个例子中,我们创建了一个名为 name 的可选变量,并将其值设置为 Jon。然后我们使用 != 运算符来验证可选值不等于 nil。如果不等于 nil,我们就可以使用解释点来访问其值。虽然这是一个完全可行的选项,但建议我们使用接下来讨论的可选绑定方法,而不是强制展开。
我们使用可选绑定来检查可选变量或常量是否有非 nil 值,如果有,则将该值赋给一个临时变量。对于可选绑定,我们使用 if-let 或 if-var 关键字。如果我们使用 if-let,则临时值是一个常量,不能更改,而 if-var 关键字将临时值放入一个变量中,允许我们更改该值。以下代码说明了如何使用可选绑定:
if let temp = myOptional {
print(temp)
print("Can not use temp outside of the if bracket")
} else {
print("myOptional was nil")
}
在前面的例子中,我们使用 if let 关键字来检查 myOptional 变量是否为 nil。如果不是 nil,我们将值赋给 temp 变量并执行括号内的代码。如果 myOptional 变量为 nil,则执行 else 括号内的代码,输出信息 myOptional was nil。需要注意的是,temp 变量仅在条件块的作用域内有效,不能在条件块外部使用。
使用可选绑定将值赋给同名的变量是完全可接受的。以下代码说明了这一点:
if let myOptional = myOptional {
print(myOptional)
print("Can not use temp outside of the if bracket")
} else {
print("myOptional was nil")
}
为了说明临时变量的作用域,让我们看看以下代码:
var myOptional: String?
myOptional = "Jon"
print("Outside: \(myOptional)")
if var myOptional = myOptional {
myOptional = "test"
print("Inside: \(myOptional)")
}
print("Outside: \(myOptional)")
在这个例子中,打印到控制台的第一行是 Inside: Optional( test),因为我们处于将 test 的值赋给 myOptional 变量的 if-var 语句的作用域内。打印到控制台的第二行将是 Outside: Optional(Jon),因为我们处于 if-var 语句的作用域之外,此时 myOptional 变量被设置为 Jon。
我们也可以在一行中测试多个可选变量。我们通过在每个可选检查之间用逗号分隔来实现这一点。以下示例展示了如何这样做:
if let myOptional = myOptional, myOptional2 = myOptional2, myOptional3 = myOptional3 {
// only reach this if all three optionals
// have non-nil values
}
可选链允许我们在可能为 nil 的可选对象上调用属性、方法和下标。如果链中的任何值返回 nil,则返回值将是 nil。以下代码通过一个虚构的 car 对象示例了可选链的使用。在这个例子中,如果 car 或 tires 中的任何一个为 nil,变量 s 将为 nil;否则,s 将等于 tireSize 属性:
var s = car?.tires?.tireSize
以下 Playground 展示了在使用可选值之前验证它们是否包含有效值的三种方法:

在前面的游乐场中,我们首先定义了一个可选字符串变量stringOne。然后我们通过使用!=运算符显式检查nil。如果stringOne不等于nil,我们将stringOne的值打印到控制台。如果stringOne是nil,我们将打印Explicit Check: stringOne is nil消息到控制台。正如我们在结果控制台中看到的那样,因为尚未给stringOne赋值,所以打印了Explicit Check: stringOne is nil。
然后,我们使用可选绑定来验证stringOne不是nil。如果stringOne不是nil,则将stringOne的值放入临时变量tmp中,并将tmp的值打印到控制台。如果stringOne是nil,我们打印Optional Binding: stringOne is nil消息到控制台。正如我们在结果控制台中看到的那样,因为尚未给stringOne赋值,所以打印了Optional Binding: stringOne is nil。
如果stringOne不是nil,我们使用可选链将stringOne变量的characters.count属性的值赋给charCount1变量。正如我们所看到的,charCount1变量是nil,因为我们尚未给stringOne赋值。
然后,我们将www.packtpub.com/all的值赋给stringOne变量,并再次运行所有三个测试。这次stringOne有一个非nil的值;因此,charCount2的值被打印到控制台。
注意
很可能会说,我可能需要将此变量设置为nil,所以让我将其定义为可选的,但那将是一个错误。对于可选的思维方式应该是,只有当变量有nil值的具体原因时才使用它们。
枚举
枚举(也称为枚举)是一种特殊的数据类型,它使我们能够将相关的类型分组在一起,并以类型安全的方式使用它们。对于那些熟悉来自其他语言(如 C 或 Java)的枚举的人来说,Swift 中的枚举并不依赖于整数值。我们可以定义一个具有类型(字符串、字符、整数或浮点数)的枚举,然后它的实际值(称为原始值)将是分配的值。枚举还支持传统上只有类才支持的功能,如计算属性和实例方法。我们将在第五章类和结构中深入讨论这些高级功能。在本节中,我们将查看传统的枚举功能。
我们将定义一个枚举,其中包含类似这样的Planets列表:
enum Planets {
case Mercury
case Venus
case Earth
case Mars
case Jupiter
case Saturn
case Uranus
case Neptune
}
枚举中定义的值被认为是枚举的成员值(或简单地称为成员)。在大多数情况下,你将看到成员值像前面的示例那样定义,因为这样更容易阅读;然而,还有一个简短版本。这个简短版本允许我们在一行中定义多个成员,成员之间用逗号分隔,如下面的示例所示:
enum Planets {
case Mercury, Venus, Earth, Mars, Jupiter
case Saturn, Uranus, Neptune
}
我们可以使用Planets枚举如下:
var planetWeLiveOn = Planets.Earth
var furthestPlanet = Planets.Neptune
当我们使用Planets枚举的一个成员值初始化变量planetWeLiveOn和furthestPlanet时,这些变量的类型会被推断出来。一旦变量类型被推断出来,我们就可以在不使用Planets前缀的情况下分配新值,如下所示:
planetWeLiveOn = .Mars
我们可以使用传统的等于(==)运算符或使用switch语句来匹配枚举值。以下示例展示了如何使用equals运算符和switch语句与枚举一起使用:
// Using the traditional == operator
if planetWeLiveOn == .Earth {
print("Earth it is")
}
// Using the switch statement
switch planetWeLiveOn {
case .Mercury:
print("We live on Mercury, it is very hot!")
case .Venus:
print("We live on Venus, it is very hot!")
case .Earth:
print("We live on Earth, just right")
case .Mars:
print("We live on Mars, a little cold")
default:
print("Where do we live?")
}
枚举可以预先填充原始值,这些原始值必须是同一类型。这意味着我们可以定义我们的枚举以包含字符串、字符、整数或浮点值,但所有成员都必须定义为同一类型。以下示例展示了如何定义一个包含字符串值的枚举:
enum Devices: String {
case iPod = "iPod"
case iPhone = "iPhone"
case iPad = "iPad"
}
print("We are using an " + Devices.iPad.rawValue)
前面的例子创建了一个包含三个苹果设备的枚举。然后我们使用rawValue属性来检索Devices枚举中iPad成员的原始值。此示例将打印一条消息,表示We are using an iPad。
让我们创建另一个Planets枚举,但这次给成员分配数字,如下所示:
enum Planets: Int {
case Mercury = 1
case Venus
case Earth
case Mars
case Jupiter
case Saturn
case Uranus
case Neptune
}
print("Earth is planet number \(Planets.Earth.rawValue)")
两个枚举示例之间的主要区别在于,在第二个示例中,我们只给第一个成员(Mercury)赋值。如果枚举的原始值使用整数,我们就不必为每个成员赋值。如果没有值存在,原始值将自动递增。
在 Swift 中,枚举也可以有关联值。关联值允许我们在成员值之外存储额外的信息。这些额外信息每次使用成员时都可能不同。它也可以是任何类型,并且每个成员的类型可以不同。让我们通过定义一个包含两种产品的Product枚举来看看我们如何使用关联类型:
enum Product {
case Book(Double, Int, Int)
case Puzzle(Double, Int)
}
var masterSwift = Product.Book(49.99, 2015, 310)
var worldPuzzle = Product.Puzzle(9.99, 200)
switchmasterSwift {
case .Book(let price, let year, let pages):
print("Mastering Swift was published in \(year) for the price of \(price) and has \(pages) pages")
case .Puzzle(let price, let pieces):
print("Master Swift is a puzze with \(pieces) and sells for \(price)")
}
switchworldPuzzle {
case .Book(let price, let year, let pages):
print("World Puzzle was published in \(year) for the price of \(price) and has \(pages) pages")
case .Puzzle(let price, let pieces):
print("World Puzzle is a puzze with \(pieces) and sells for \(price)")
}
在前面的例子中,我们首先定义了一个包含两个成员——Book和Puzzle的Product枚举。Book成员关联的值类型为Double, Int, Int,而Puzzle成员关联的值类型为Double, Int。然后我们创建了两个产品masterSwift和worldPuzzle。我们将masterSwift变量赋值为Product.Book,并关联值为49.99, 2015, 310。然后我们将worldPuzzle变量赋值为Product.Puzzle,并关联值为9.99, 200。
我们可以使用switch语句来检查Products枚举,就像我们在前面的枚举例子中所做的那样。我们还在switch语句中提取了关联值。在先前的例子中,我们使用let关键字将关联值作为常量提取出来,但您也可以使用var关键字将关联值作为变量提取出来。
如果将前面的代码放入 Playground 中,将显示以下结果:
"Master Swift was published in 2015 for the price of 49.99 and has 310 pages"
"World Puzzle is a puzzle with 200 and sells for 9.99"
运算符
运算符是一个符号或符号的组合,我们可以用它来检查、更改或组合值。我们在本书中的大多数示例中都使用了运算符;然而,我们并没有特别称它们为运算符。在本节中,我们将展示如何使用 Swift 支持的大多数基本运算符。
Swift 支持大多数标准 C 运算符,并对其进行改进以消除几个常见的编码错误。例如,赋值运算符不返回值,以防止在打算使用相等运算符(==)时被误用。
让我们来看看 Swift 中的运算符。
赋值运算符
赋值运算符初始化或更新变量。
原型:
varA = varB
示例:
let x = 1
var y = "Hello"
a = b
比较运算符
比较运算符如果语句为真,则返回布尔 true;如果语句不为真,则返回布尔 false。
原型:
Equality: varA == varB
Not equal: varA != varB
Greater than: varA > varB
Less than: varA < varB
Greater than or equal to: varA >= varB
Less than or equal to: varA <= varB
示例:
2 == 1 //false, 2 does not equal 1
2 != 1 //true, 2 does not equal 1
2 > 1 //true, 2 is greater than 1
2 < 1 //false, 2 is not less than 1
2 >= 1 //true, 2 is greater or equal to 1
2 <= 1 //false, 2 is not less or equal to 1
算术运算符
算术运算符执行四种基本的数学运算。
原型:
Addition: varA + varB
Subtraction: varA – varB
Multiplication: varA * varB
Division: varA / varB
示例:
var x = 4 + 2 //x will equal 6
var x = 4 – 2 //x will equal 2
var x = 4 * 2 //x will equal 8
var x = 4 / 2 //x will equal 2
var x = "Hello " + "world" //x will equal "Hello World"
取模运算符
取模运算符计算第一个操作数除以第二个操作数的余数。
原型:
varA % varB
示例:
var x = 10 % 3 //x will equal 1
var x = 10 % 2.6 //x will equal 2.2
自增和自减运算符
自增和自减运算符是用于将变量增加或减少 1 的快捷方式。
原型:
++varA - Increments the value of varA and then returns the value
varA++ - Returns the values of varA and then increments varA
--varA – Decrements the value of varA and then returns the value
varA-- - Returns the value of varA and then decrements varA
示例:
var x = 5
var y = ++x //Both x and y equals 6
var y = x++ //x equals 6 but y equals 5
var y = --x //Both x and y equals 4
var y = x-- //x equals 4 but y equals 5
复合赋值运算符
复合赋值运算符将算术运算符与赋值运算符结合。
原型:
varA += varB
varA -= varB
varA *= varB
varA /= varB
示例:
var x = 6
x += 2 //x is equal to 8
x -= 2 //x is equal to 4
x *= 2 //x is equal to 12
x /= 2 //x is equal to 3
三元条件运算符
三元条件运算符根据比较运算符或布尔值的评估将值赋给变量。
原型:
(boolValue ? valueA : valueB)
示例:
var x = 2
var y = 3
var z = (y > x ? "Y is greater" : "X is greater") //z equals "Y is greater"
逻辑非运算符
逻辑非运算符反转布尔值。
原型:
varA = !varB
示例:
var x = true
var y = !x //y equals false
逻辑与运算符
逻辑与运算符如果两个操作数都为真,则返回 true,否则返回 false。
原型:
varA && varB
示例:
var x = true
var y = false
var z = x && y //z equals false
逻辑或运算符
逻辑或运算符如果任一操作数为真,则返回 true。
原型:
varA || varB
示例:
var x = true
var y = false
var z = x || y //z equals true
对于熟悉 C 语言及其类似语法的语言的人来说,这些运算符应该看起来相当熟悉。对于那些不太熟悉 C 运算符的人来说,请放心,你会足够多地使用它们,它们将变得习以为常。
概述
在本章中,我们涵盖了大量的不同主题。这些主题从变量和常量到数据类型和运算符不等。本章中的内容将成为你编写每个应用程序的基础;因此,理解这里讨论的概念非常重要。
在下一章中,我们将探讨如何使用 Swift 的集合类型来存储相关数据。这些集合类型包括字典和数组类型。我们还将探讨如何在 Swift 中使用 Cocoa 和 Foundation 数据类型。
第三章。使用集合和 Cocoa 数据类型
一旦我超越了基本的 Hello World 初学者应用程序,我很快就开始意识到变量的不足,尤其是在我刚开始编写的 Mad Libs 风格的应用程序中。这些应用程序要求用户输入多个字符串,我为每个用户输入的字段创建了一个单独的变量。拥有所有这些单独的变量很快就变得非常繁琐。我记得和一个朋友谈论过这件事,他问我为什么不用数组。当时,我对数组不熟悉,所以我让他给我展示一下它们是什么。尽管他有一个 TI-99/4A,而我有一个 Commodore Vic-20,但数组的概念是相同的。即使今天,现代开发语言中的数组与我在 Commodore Vic-20 上使用的数组具有相同的基本概念。虽然确实可以不使用集合创建一个有用的应用程序,但正确使用集合确实可以使应用程序开发变得更加容易。
本章将涵盖以下主题:
-
Swift 中的数组是什么以及如何使用它
-
Swift 中的字典是什么以及我们如何使用它
-
Swift 中的集合是什么以及我们如何使用它
-
Swift 中的元组是什么以及我们如何使用它
-
如何在 Swift 中使用 Cocoa 数据类型
-
如何在 Swift 中使用 Foundation 数据类型
Swift 集合类型
集合是一组或存储具有共同意义的数据。Swift 提供了三种原生集合类型来存储数据。这些集合类型是数组、集合和字典。数组按顺序存储数据,集合是无序的唯一数据集合,字典是无序的键/值对集合。在数组中,我们通过数组中的位置(索引)访问数据;在集合中,我们倾向于遍历集合;而字典通常使用唯一键来访问。
存储在 Swift 集合中的数据必须具有相同的类型。这意味着,例如,我们无法存储一个字符串值和一个整数数组。由于 Swift 不允许我们在集合中混合数据类型,当我们从集合中检索数据时,我们可以确定数据类型。这是另一个表面上看可能像是一个缺点,但实际上是一个设计特性,有助于消除常见的编程错误。在本章中,我们将看到如何通过使用 AnyObject 和 Any 别名来绕过这个特性。
可变性
对于熟悉 Objective-C 的人来说,你们会知道存在不同的类来表示可变和不可变集合。例如,要定义一个可变数组,我们使用 NSMutableArray 类,而要定义一个不可变数组,我们使用 NSArray 类。Swift 与之不同,因为它没有为可变和不可变集合提供单独的类。相反,我们通过使用 let 和 var 关键字来定义集合是常量(不可变)还是变量(可变)。这在 Swift 中应该看起来很熟悉,因为,在 Swift 中,我们使用 let 关键字定义常量,使用 var 关键字定义变量。
注意
除非有特定的需求需要更改集合中的对象,否则创建不可变集合是一种良好的实践。这允许编译器优化性能。
让我们开始我们的集合之旅,先看看最常见的集合类型——数组类型。
数组
数组是现代编程语言中非常常见的组件,几乎可以在所有现代编程语言中找到。在 Swift 中,数组是相同类型对象的有序列表。这与 Objective-C 中的 NSArray 类不同,后者可以包含不同类型的对象。
当创建数组时,我们必须通过显式类型声明或通过类型推断来声明要存储在其中的数据类型。通常,我们只在创建空数组时显式声明数组的数据类型。如果我们用数据初始化数组,我们应该让编译器使用类型推断来推断最合适的数组数据类型。
数组中的每个对象称为一个元素。这些元素按照一定的顺序存储,可以通过其在数组中的位置(索引)来访问。
创建和初始化数组
我们可以使用数组字面量来初始化一个数组。数组字面量是一组我们预先填充到数组中的值。以下示例展示了如何使用 let 关键字定义一个不可变的整数数组:
let arrayOne = [1,2,3]
正如我们提到的,如果我们需要创建一个可变数组,我们将使用 var 关键字来定义数组。以下示例展示了如何定义一个可变数组:
var arrayTwo = [4,5,6]
在前面的两个示例中,编译器通过查看数组字面量中存储的值的类型来推断数组中存储的值的类型。如果我们需要创建一个空数组,我们需要显式声明要存储在数组中的值的类型。以下示例展示了如何声明一个可以用来存储整数的空数组:
var arrayThree = [Int]()
在前面的示例中,我们使用整数值创建了数组,本章中的大多数数组示例也将使用整数值;然而,我们可以在 Swift 中使用任何类型创建数组。唯一的规则是,一旦定义了一个数组包含特定类型,数组中的所有元素都必须是那种类型。以下示例展示了如何创建各种数据类型的数组:
var arrayOne = [String]()
var arrayTwo = [Double]()
var arrayThree = [MyObject]()
Swift 确实提供了用于处理非特定类型的特殊类型别名。这些别名是AnyObject和Any。我们可以使用这些别名来定义元素类型不同的数组,如下所示:
var myArray: [AnyObject] = [1,"Two"]
我们应该只在明确需要这种行为时才使用Any和AnyObject别名。始终明确我们集合中包含的数据类型是更好的做法。
我们也可以初始化一个数组,使其具有特定的大小,并且将数组中的所有元素都设置为预定义的值。如果我们想创建一个数组并预先填充默认值,这会非常有用。以下示例定义了一个包含七个元素的数组,每个元素都包含数字3:
var arrayFour = Int
虽然最常见的数组是一维数组,但我们也可以创建多维数组。多维数组实际上不过是一个数组的数组。例如,二维数组是一个数组的数组,而三维数组是一个数组的数组的数组。以下示例展示了在 Swift 中创建二维数组的两种方式:
var multiArrayOne = [[1,2],[3,4],[5,6]]
var multiArrayTwo = [[Int]]()
访问数组元素
我们使用下标语法来从数组中检索值。数组的下标语法是,一个数字出现在两个方括号之间,这个数字指定了我们想要检索的元素在数组中的位置(索引)。以下示例展示了如何使用下标语法从数组中检索元素:
let arrayOne = [1,2,3,4,5,6]
print(arrayOne[0]) //Displays '1'
print(arrayOne[3]) //Displays '4'
在前面的代码中,我们首先创建了一个包含六个整数的数组。然后我们打印出索引0和3的值。
如果我们想要从多维数组中检索单个值,我们需要为每个维度提供一个下标。如果我们没有为每个维度提供下标,我们将返回一个数组,而不是数组中的单个值。以下示例展示了我们如何定义一个二维数组,并在两个维度中检索单个值:
var multiArray = [[1,2],[3,4],[5,6]]
var arr = multiArray[0] //arr contains the array [1,2]
var value = multiArray[0][1] //value contains 2
在前面的代码中,我们首先定义了一个二维数组。当我们检索第一维索引0的值(multiArray[0])时,我们检索到的是数组[1,2]。当我们检索第一维索引0和第二维索引1的值(multiArray[0][1])时,我们检索到的是整数2。
我们可以使用first和last属性来检索数组的第一个和最后一个元素。由于数组可能为空,first和last属性返回的是可选值。以下示例展示了如何使用first和last属性来检索单维和多维数组的第一和最后一个元素:
let arrayOne = [1,2,3,4,5,6]
var first = arrayOne.first //first contains 1
var last = arrayOne.last //last contains 6
let multiArray = [[1,2],[3,4],[5,6]]
var arrFirst1 = multiArray[0].first //arrFirst1 contains 1
var arrFirst2 = multiArray.first //arrFirst2 contains[1,2]
var arrLast1 = multiArray[0].last //arrLast1 contains 2
var arrLast2 = multiArray.last //arrLast2 contains [5,6]
计算数组的元素
有时,知道数组中的元素数量是至关重要的。要获取元素数量,我们会使用只读的count属性。以下示例展示了如何使用此属性来检索单维和多维数组中的元素数量:
let arrayOne = [1,2,3]
let multiArrayOne = [[3,4],[5,6],[7,8]]
print(arrayOne.count) //Displays 3
print(multiArrayOne.count) //Displays 3 for the three arrays
print(multiArrayOne[0].count) //Displays 2 for the two elements
count属性返回的是数组中的元素数量,而不是数组的最大有效索引。对于非空数组,最大有效索引是数组元素数量减一。这是因为数组的第一个元素索引号为零。例如,如果一个数组有两个元素,有效的索引是0和1,而count属性将返回2。以下代码说明了这一点:
let arrayOne = [0,1]
print(arrayOne[0]) //Displays 0
print(arrayOne[1]) //Displays 1
print(arrayOne.count) //Displays 2
如果我们尝试使用下标语法从数组中检索一个元素,而该索引超出了数组的范围,应用程序将抛出Array index out of range错误。因此,如果我们不确定数组的大小,验证索引是否不在数组范围之外是一种良好的做法。以下示例说明了这个概念:
//This example will throw an array index out of range error
var arrayTwo = [1,2,3,4]
print(arrayTwo[6])
//This example will not throw an array index out of range error
var arrayOne = [1,2,3,4]
if (arrayOne.count> 6) {
print(arrayOne[6])
}
在前面的代码中,第一段代码会抛出一个数组索引超出范围错误异常,因为我们试图从arrayTwo数组中访问索引为6的值;然而,该数组中只有四个元素。第二个例子不会抛出数组索引超出范围错误异常,因为我们正在检查arrayOne数组是否包含超过六个元素,如果没有,我们就不尝试访问索引6的值。
数组是否为空?
要检查数组是否为空(不包含任何元素),我们使用isEmpty属性。如果数组为空,该属性将返回true;如果它包含元素,则返回false。以下示例展示了如何检查数组是否为空:
var arrayOne = [1,2]
var arrayTwo = [Int]()
arrayOne.isEmpty //Returns false because the array is not empty
arrayTwo.isEmpty //Returns true because the array is empty
追加到数组
静态数组有些有用,但能够动态地添加元素才是数组真正有用的地方。要向数组的末尾添加一个项目,我们可以使用append方法。以下示例展示了如何将项目追加到数组的末尾:
var arrayOne = [1,2]
arrayOne.append(3) //arrayOne will now contain 1, 2 and 3
Swift 还允许我们使用加法赋值运算符(+=)将一个数组追加到另一个数组中。以下示例展示了如何使用加法赋值运算符将数组追加到另一个数组的末尾:
var arrayOne = [1,2]
arrayOne += [3,4] //arrayOne will now contain 1, 2, 3 and 4
将元素追加到数组末尾的方式完全取决于你。我个人更喜欢赋值运算符,因为它对我来说更容易阅读,但在这本书中我们将使用两种方式。
向数组中插入值
我们可以通过使用insert方法将值插入到数组中。insert方法将移动从指定索引开始的所有项目,向上移动一个位置,为新元素腾出空间,然后将值插入到指定索引。以下示例展示了如何使用insert方法将新值插入到数组中:
var arrayOne = [1,2,3,4,5]
arrayOne.insert(10, atIndex: 3) //arrayOne now contains 1, 2, 3, 10, 4 and 5
注意
您不能插入超出数组当前范围的值。尝试这样做将抛出索引超出范围异常。例如,在先前的代码中,如果我们尝试在索引 10 处插入一个新的整数,我们将收到一个索引超出范围异常错误,因为arrayOne只包含五个元素。这个例外是我们能够直接在最后一个元素之后插入一个项;因此,我们可以将项插入到索引6。然而,建议我们使用append函数来追加项以避免错误。
替换数组中的元素
我们使用下标语法来替换数组中的元素。使用下标,我们选择要更新的数组元素,然后使用赋值运算符分配新值。以下示例展示了我们如何在数组中替换一个值:
var arrayOne = [1,2,3]
arrayOne[1] = 10 //arrayOne now contains 1,10,3
注意
您不能更新超出数组当前范围的值。尝试这样做将抛出我们在尝试在数组范围之外插入值时抛出的相同的索引超出范围异常。
从数组中删除元素
我们可以使用三种方法来从数组中删除一个或所有元素。这些方法是removeLast()、removeAtIndex()和removeAll()。以下示例展示了如何使用这三种方法从数组中删除元素:
var arrayOne = [1,2,3,4,5]
arrayOne.removeLast() //arrayOne now contains 1, 2, 3 and 4
arrayOne.removeAtIndex(2) //arrayOne now contains 1, 2 and 4
arrayOne.removeAll() //arrayOne is now empty
removeLast()和removeAtIndex()方法也会返回它正在删除的元素的值。因此,如果我们想了解被删除项的值,我们可以重写removeAtIndex和removeLast行来捕获该值,如下面的示例所示:
var arrayOne = [1,2,3,4,5]
var removed1 = arrayOne.removeLast() //removed1 contains the value 5
var removed = arrayOne.removeAtIndex(2) //removed contains the value 3
添加两个数组
要通过将两个数组相加来创建一个新数组,我们使用加法(+)运算符。以下示例展示了如何使用加法(+)运算符创建一个包含两个其他数组所有元素的新数组:
let arrayOne = [1,2]
let arrayTwo = [3,4]
var combine = arrayOne + arrayTwo //combine contains 1, 2, 3 and 4
在先前的代码中,arrayOne和arrayTwo保持不变,而组合数组包含来自arrayOne的元素,然后是来自arrayTwo的元素。
反转数组
我们可以使用reverse()方法从原始数组中创建一个新数组,其元素顺序相反。reverse方法不会改变原始数组。以下示例展示了如何使用reverse()方法:
var arrayOne = [1,2,3]
var reverse = arrayOne.reverse() //reverse contains 3,2 and 1
在先前的代码中,arrayOne的元素保持不变,而reverse数组将包含来自arrayOne的所有元素,但顺序相反。
从数组中检索子数组
我们可以通过使用带有范围的下标语法从现有数组中检索子数组。以下示例展示了如何从现有数组中检索一系列元素:
let arrayOne = [1,2,3,4,5]
var subArray = arrayOne[2…4] //subArray contains 3, 4 and 5
… 运算符(三个点)被称为范围运算符。在前面代码中,范围运算符表示我想要所有元素,从 2 到 4(包括元素 2 和 4 以及它们之间的所有元素)。还有一个范围运算符,即 ..<,它与 … 范围运算符相同,但它排除了最后一个元素。以下示例展示了如何使用 .< 运算符。
let arrayOne = [1,2,3,4,5]
var subArray = arrayOne[2..<4] //subArray contains 3 and 4
在前面的示例中,subArray 将包含两个元素 3 和 4。
对数组进行批量更改
我们可以使用下标语法和范围运算符来更改多个元素的值。以下示例展示了如何使用下标语法来更改一系列元素的值。
var arrayOne = [1,2,3,4,5]
arrayOne[1…2] = [12,13]//arrayOne contains 1,12,13,4 and 5
在前面的代码中,索引 1 和 2 处的元素将被更改为数字 12 和 13。之后,当代码运行时,arrayOne 将包含 1、12、13、4 和 5。
在范围运算符中更改的元素数量不需要与传递的值的数量相匹配。Swift 会进行批量更改——它首先移除由范围运算符定义的元素,然后插入新的值。以下示例演示了这一概念:
var arrayOne = [1,2,3,4,5]
arrayOne[1…3] = [12,13]
//arrayOne now contains 1, 12, 13 and 5 (four elements)
在前面的代码中,arrayOne 有五个元素。然后我们说我们想要替换从元素 1 到 3(包括 1 和 3)的范围。这导致数组中从元素 1 到 3(共三个元素)被移除。然后我们在索引 1 处向数组中添加两个元素(12 和 13)。完成这些操作后,arrayOne 将包含以下四个元素:1、12、13 和 5。让我们看看如果我们尝试添加比移除更多的元素会发生什么:
var arrayOne = [1,2,3,4,5]
arrayOne[1...3] = [12,13,14,15]
//arrayOne now contains 1, 12, 13, 14, 15 and 5 (six elements)
在前面的代码中,arrayOne 有五个元素。然后我们说我们想要替换从元素 1 到 3(包括 1 和 3)的范围。这导致数组中从元素 1 到 3(共三个元素)被移除。然后我们在索引 1 处向数组中添加四个元素(12、13、14 和 15)。完成这些操作后,arrayOne 将包含以下六个元素:1、12、13、14、15 和 5。
数组算法
Swift 数组有几种方法,这些方法接受一个闭包作为参数。这些方法会转换数组,而闭包会影响数组如何被转换。闭包是包含代码的独立块,可以传递,类似于 Objective-C 中的 blocks 和其他语言中的 lambdas。我们将在第十二章使用闭包中深入讨论闭包。现在,我们只想熟悉 Swift 中算法的工作方式。
sortInPlace
sortInPlace算法在原地排序数组。这意味着当使用sortInPlace()方法时,原始数组将被排序后的数组替换。闭包接受两个参数(分别由$0和$1表示),并且它应该返回一个布尔值,表示第一个元素是否应该放在第二个元素之前。以下代码展示了如何使用排序算法:
var arrayOne = [9,3,6,2,8,5]
arrayOne.sortInPlace(){ $0 < $1 }
//arrayOne contains 2,3,5,6,8 and 9
上述代码将数组按升序排序。我们可以通过我们的规则来判断,如果第一个数字($0)小于第二个数字($1),则返回true。因此,当排序算法开始时,它比较前两个数字(9和3),如果第一个数字(9)小于第二个数字(3),则返回true。在我们的例子中,规则返回false,所以数字被反转。算法以这种方式继续排序,直到所有数字都排序完成。
上述示例按数值升序排序了数组;如果我们想反转顺序,我们会在闭包中反转参数。以下代码展示了如何反转排序顺序:
var arrayOne = [9,3,6,2,8,5]
arrayOne.sortInPlace(){ $1 < $0 }
//arrayOne contains 9,8,6,5,3 and 2
当我们运行此代码时,arrayOne将包含元素9、8、6、5、3和2。
sort
虽然sortInPlace算法在原地排序数组(替换原始数组),但sort算法不会改变原始数组,它而是创建一个新的数组,包含原始数组中的排序元素。以下示例展示了如何使用排序算法:
var arrayOne = [9,3,6,2,8,5]
let sorted = arrayOne.sort(){ $0 < $1 }
//sorted contains 2,3,5,6,8 and 9
//arrayOne contains 9,3,6,2,8 and 5
当我们运行此代码时,arrayOne将包含原始未排序数组(9、3、6、2、8和5),而排序后的数组将包含新的排序数组(2、3、5、6、8和9)。
filter
filter算法将通过过滤原始数组来返回一个新的数组。这是最强大的数组算法之一,最终可能会成为我们使用最多的算法之一。如果我们需要根据一组规则从数组中检索子集,我建议使用此算法,而不是尝试编写自己的方法来过滤数组。闭包接受一个参数,并且如果元素应该包含在新数组中,它应该返回布尔值true,如下所示:
var arrayOne = [1,2,3,4,5,6,7,8,9]
let filtered = arrayFiltered.filter{$0 > 3 && $0 < 7}
//filtered contains 4,5 and 6
在前面的代码中,我们传递给算法的规则返回true,如果数字大于3或小于7;因此,任何大于3或小于7的数字都包含在新的过滤数组中。
让我们来看另一个例子;这个例子展示了如何从一个城市数组中检索出名字中包含字母 o 的子集:
var city = ["Boston", "London", "Chicago", "Atlanta"]
let filtered = city.filter{$0.rangeOfString("o") != nil}
//filtered contains "Boston", "London" and "Chicago"
在前面的代码中,我们使用rangeOfString()方法来返回true,如果字符串包含字母 o。如果方法返回true,则字符串包含在过滤数组中。
map
map 算法返回一个新数组,该数组包含将闭包中的规则应用于数组中每个元素的结果。以下示例展示了如何使用 map 算法将每个数字除以10:
var arrayOne = [10, 20, 30, 40]
let applied = arrayOne.map{ $0 / 10}
//applied contains 1,2,3 and 4
在前面的代码中,新数组包含数字1、2、3和4,这是通过将原始数组中的每个元素除以10得到的结果。
通过 map 算法创建的新数组不需要包含与原始数组相同的元素类型;然而,新数组中的所有元素必须属于同一类型。在以下示例中,原始数组包含整数值,但通过 map 算法创建的新数组包含字符串元素:
var arrayOne = [1, 2, 3, 4]
let applied = arrayOne.map{ "num:\($0)"}
//applied contains "num:1", "num:2", "num:3" and "num:4"
在前面的代码中,我们创建了一个字符串数组,它将原始数组中的数字追加到num:字符串中。
forEach
我们可以使用forEach来遍历一个序列。以下示例展示了我们如何这样做:
var arrayOne = [10, 20, 30, 40]
arrayOne.forEach{ print($0) }
此示例将在控制台打印以下结果:
10
20
30
40
虽然使用forEach方法非常简单,但它确实有一些限制。推荐遍历数组的方式是使用for-in循环,我们将在下一节中看到。
遍历数组
我们可以使用for-in循环按顺序遍历数组的所有元素。我们将在第四章中更详细地讨论for-in循环,控制流和函数。for-in循环将为数组中的每个元素执行一个或多个语句。以下示例展示了我们如何遍历数组的元素:
var arr = ["one", "two", "three"]
for item in arr {
print(item)
}
在前面的示例中,for-in循环遍历arr数组,并为数组中的每个元素执行print(item)行。如果我们运行此代码,它将在控制台显示以下结果:
one
two
three
有时候,我们希望像在前面示例中那样遍历数组,但同时也想了解元素的索引和值。为此,我们可以使用enumerate方法,该方法为数组中的每个元素返回一个元组(稍后在本章的元组部分将详细介绍),其中包含元素的index和value。以下示例展示了如何使用enumerate函数:
var arr = ["one", "two", "three"]
for (index,value) in arr.arr.enumerate() {
print"\(index) \(value)")
}
上述代码将在控制台显示以下结果:
0 one
1 two
2 three
现在我们已经介绍了 Swift 中的数组,让我们来看看什么是字典。
字典
虽然字典不如数组常用,但它们具有额外的功能,这使得它们非常强大。字典是一个容器,存储多个键值对,其中所有键都是同一类型,所有值也都是同一类型。键用作值的唯一标识符。由于我们是通过键来查找值,而不是通过值的索引,因此字典不能保证键值对存储的顺序。
字典适用于存储映射到唯一标识符的项目,其中唯一标识符应用于检索项目。例如,国家和它们的缩写是字典中可以存储的项目的好例子。在以下图表中,我们展示了国家和它们的缩写作为键值对:
| 键 | 值 |
|---|---|
| US | 美国 |
| IN | 印度 |
| UK | 英国 |
创建和初始化字典
我们可以使用字典字面量来初始化字典,类似于我们使用数组字面量初始化数组。以下示例展示了如何使用前面图表中的键值对创建字典:
let countries = ["US":"UnitedStates","IN":"India","UK":"UnitedKingdom"]
前面的代码创建了一个包含前面图表中每个键值对的不可变字典。就像数组一样,要创建可变字典,我们将使用 var 关键字而不是 let。以下示例展示了如何创建包含国家的可变字典:
var countries = ["US":"UnitedStates","IN":"India","UK":"United Kingdom"]
在前面的两个例子中,我们创建了一个键和值都是字符串的字典。编译器推断键和值是字符串,因为这是我们放入的类型。如果我们想创建一个空字典,我们需要告诉编译器键和值的类型。以下示例创建具有不同键值类型的各种字典:
var dic1 = [String:String]()
var dic2 = [Int:String]()
var dic3 = [String:MyObject]()
备注
如果我们想在字典中使用自定义对象作为键,我们需要让我们的自定义对象符合 Swift 标准库中的 Hashable 协议。我们将在第五章类和结构中讨论协议和类,但就目前而言,只需了解可以使用自定义对象作为字典的键。
访问字典值
我们使用下标语法来检索特定键的值。如果字典不包含我们正在寻找的键,字典将返回 nil;因此,从这个查找返回的变量是一个可选变量。以下示例展示了如何使用下标语法从字典中检索值:
let countries = ["US":"United States", "IN":"India","UK":"United Kingdom"]
var name = countries["US"]
在前面的代码中,变量名将包含字符串,United States。
在字典中计数键或值
我们使用字典的 count 属性来获取字典中键值对的数量。以下示例展示了如何使用 count 属性来检索字典中键值对的数量:
let countries = ["US":"United States", "IN":"India","UK":"United Kingdom"];
var cnt = countries.count //cnt contains 3
在前面的代码中,cnt 变量将包含数字 3,因为 countries 字典中有三个键值对。
字典是否为空?
要测试字典是否包含任何键值对,我们可以使用 isEmpty 属性。如果字典包含一个或多个键值对,则 isEmpty 属性将返回 false;如果它是空的,则返回 true。以下示例显示了如何使用 isEmpty 属性来确定我们的字典是否包含任何键值对:
let countries = ["US":"United States", "IN":"India","UK":"United Kingdom"]
var empty = countries.isEmpty
在前面的代码中,isEmpty 属性为 false,因为国家字典中有三个键值对。
更新键的值
要更新字典中键的值,我们可以使用下标语法或 updateValue(value:, forKey:) 方法。updateValue(value:, forKey:) 方法有一个下标语法没有的附加功能——它在更改值之前返回与键关联的原始值。以下示例显示了如何使用下标语法和 updateValue(value:, forKey:) 方法来更新键的值:
var countries = ["US":"United States", "IN":"India","UK":"United Kingdom"]
countries["UK"] = "Great Britain"
//The value of UK is now set to "Great Britain"
var orig = countries.updateValue("Britain", forKey: "UK")
//The value of UK is now set to "Britain" and orig now contains "Great Britain"
在前面的代码中,我们使用下标语法将与键 UK 关联的值从 United Kingdom 更改为 Great Britain。在替换之前,我们没有保存 United Kingdom 的原始值,因此我们无法看到原始值是什么。然后我们使用 updateValue(value:, forKey:) 方法将与键 UK 关联的值从 Great Britain 更改为 Britain。使用 updateValue(value:, forKey:) 方法,在字典中更改值之前,将 Great Britain 的原始值分配给 orig 变量。
添加键值对
要向字典中添加一个新的键值对,我们可以使用下标语法或与更新键值值相同的 updateValue(value:, forKey:) 方法。如果我们使用 updateValue(value:, forKey:) 方法,并且键当前不在字典中,则 updateValue(value:, forKey:) 方法将添加一个新的键值对并返回 nil。以下示例显示了如何使用下标语法以及 updateValue(value:, forKey:) 方法向字典中添加一个新的键值对:
var countries = ["US":"United States", "IN":"India","UK":"United Kingdom"]
countries["FR"] = "France" //The value of "FR" is set to "France"
var orig = countries.updateValue("Germany", forKey: "DE")
//The value of "DE" is set to "Germany" and orig is nil
在前面的代码中,国家字典开始时有三个键值对,然后我们使用下标语法向字典中添加第四个键值对 (FR/France)。然后我们使用 updateValue(value:, forKey:) 方法向字典中添加第五个键值对 (DE/Germany)。orig 变量被设置为 nil,因为国家字典中没有与 DE 键关联的值。
删除键值对
有时候我们需要从字典中移除值。我们可以使用下标语法,使用 removeValueForKey() 方法或 removeAll() 方法来完成此操作。removeValueForKey() 方法返回在移除之前键的值。removeAll() 方法从字典中移除所有元素。以下示例展示了如何使用下标语法、removeValueForKey() 方法和 removeAll() 方法从字典中移除键值对:
var countries = ["US":"United States", "IN":"India","UK":"United Kingdom"];
countries["IN"] = nil //The "IN" key/value pair is removed
var orig = countries.removeValueForKey("UK")
//The "UK" key value pair is removed and orig contains "United Kingdom"
countries.removeAll() //Removes all key/value pairs from the countries dictionary
在前面的代码中,countries 字典最初有三个键值对。然后我们将与键 IN 关联的值设置为 nil,这将从字典中移除键值对。我们使用 removeValueForKey() 方法来移除与 UK 键关联的键。在移除与 UK 键关联的值之前,removeValueForKey() 方法将值保存在 orig 变量中。最后,我们使用 removeAll() 方法来移除 countries 字典中剩余的所有键值对。
现在让我们看看集合类型。
集合
集合类型是一个类似于数组类型的泛型集合。虽然数组类型是一个有序集合,可能包含重复的元素,但集合类型是一个无序集合,其中每个元素必须是唯一的。
与字典中的键类似,存储在数组中的类型必须符合 Hashable 协议。这意味着该类型必须提供一种方法来计算自己的哈希值。Swift 的所有基本类型,如 String、Double、Int 和 Bool,都符合 Hashable 协议,并且默认情况下可以用于集合。
让我们看看如何使用集合类型。
初始化集合
我们有几种初始化集合的方法。就像数组和字典类型一样,Swift 需要知道将要存储的数据类型。这意味着我们必须告诉 Swift 要在集合中存储的数据类型,或者用一些数据初始化它,以便它可以推断数据类型。
就像数组和字典类型一样,我们使用 var 和 let 关键字来声明集合是否可变:
//Initializes an empty Set of the String type
var mySet = Set<String>()
//Initializes a mutable set of the String type with initial values
var mySet = Set(["one", "two", "three"])
//Creates aimmutable set of the String type.
let mySet = Set(["one", "two", "three"])
将元素插入到集合中
我们使用 insert 方法将一个元素插入到集合中。如果我们尝试插入一个已经存在于集合中的元素,该元素将被忽略,并且不会抛出错误。以下是一些如何将元素插入到集合中的示例:
var mySet = Set<String>()
mySet.insert("One")
mySet.insert("Two")
mySet.insert("Three")
集合中的元素数量
我们可以使用 count 属性来确定 Swift 集合中元素的数量。以下是如何使用 count 方法的示例:
var mySet = Set<String>()
mySet.insert("One")
mySet.insert("Two")
mySet.insert("Three")
print("\(mySet.count) items")
当执行此代码时,它将在控制台打印出消息 "Three items",因为集合包含三个元素。
检查集合中是否包含一个元素
我们可以通过使用 contains() 方法非常容易地检查一个集合是否包含一个元素,如下所示:
var mySet = Set<String>()
mySet.insert("One")
mySet.insert("Two")
mySet.insert("Three")
var contain = mySet.contains("Two")
在前面的示例中,contain 变量被设置为 True,因为集合确实包含字符串 "Two"。
遍历集合
我们可以使用 for 语句遍历 Set 中的项目。以下示例展示了我们如何遍历集合中的项目:
for item inmySet {
print(item)
}
上述示例将打印出集合中的每个项目到控制台。
从集合中移除项目
我们可以移除单个项目或集合中的所有项目。要移除单个项目,我们会使用 remove() 方法,要移除所有项目,则使用 removeAll() 方法。以下示例展示了如何从集合中移除项目:
//The remove method will return and remove an item from a set
var item = mySet.remove("Two")
//The removeAll method will remove all items from a set
mySet.removeAll()
集合操作
苹果提供了四种方法,我们可以使用这些方法从两个其他集合中构建一个集合。这些操作可以在集合中就地执行,在其中一个集合上执行,或者用于创建一个新集合。这些操作包括:
-
union和unionInPlace:这些创建一个包含两个集合所有唯一值的集合 -
subtract和subtractInPlace:这些创建一个包含第一个集合中不在第二个集合中的值的集合 -
intersect和intersectInPlace:这些创建一个包含两个集合共有值的集合 -
exclusiveOr和exclusiveOrInPlace:这些创建一个新集合,其中包含在任一集合中但不在两个集合中的值
让我们看看一些示例,并查看从每个操作中得到的每个结果。对于所有集合操作示例,我们将使用以下两个集合:
var mySet1 = Set(["One", "Two", "Three", "abc"])
var mySet2 = Set(["abc","def","ghi", "One"])
现在让我们看看我们的示例。我们将首先查看使用 union 方法。这个方法将取两个集合中的唯一值来创建另一个集合:
var newSetUnion = mySet1.union(mySet2)
newSetUnion 变量将包含以下值:"One", "Two", "Three", "abc", "def", "ghi"。现在让我们看看 subtract 方法。这个方法将创建一个集合,其中包含第一个集合中不在第二个集合中的值:
var newSetSubtract = mySet1.subtract(mySet2)
在这个例子中,newSetSubtract 变量将包含 "Two" 和 "Three" 这两个值,因为它们是唯一不在第二个集合中的两个值。
现在让我们看看 intersect 方法。intersect 方法将创建一个新集合,其中包含两个集合之间的公共值:
var newSetIntersect = mySet1.intersect(mySet2)
在这个例子中,newSetIntersect 变量将包含 "One" 和 "abc" 这两个值,因为它们是两个集合之间的公共值。
现在让我们看看 exclusiveOr 方法。这个方法将创建一个新集合,其中包含在任一集合中但不在两个集合中的值:
//newSetExclusiveOr = {"Two", "Three", "def", "ghi"}
var newSetExclusiveOr = mySet1.exclusiveOr(mySet2)
在这个例子中,newSetExclusiveOr 变量将包含 "Two", "Three", "def" 和 "ghi"。
这四种操作(union、subtract、intersect 和 exclusiveor 方法)增加了数组中不存在的一些附加功能。与数组相比,查找速度更快,当集合的顺序不重要且集合中的对象必须是唯一的时候,Set 可以是一个非常有用的替代品。
元组
元组将多个值组合成一个单一的复合值。与数组和字典不同,元组中的值不必是同一类型。以下示例展示了如何定义一个元组:
var team = ("Boston", "Red Sox", 97, 65, 59.9)
在前面的例子中,我们创建了一个未命名的元组,其中包含两个字符串、两个整数和一个双精度浮点数。我们可以将这个元组中的值分解到一组变量中,如下面的例子所示:
var team = ("Boston", "Red Sox", 97, 65, 59.9)
var (city, name, wins, loses, percent) = team
在前面的代码中,city 变量将包含 Boston,name 变量将包含 Red Sox,wins 变量将包含 97,loses 变量将包含 65,最后,percent 变量将包含 0.599。
我们也可以通过指定值的地址来从元组中检索值。以下示例展示了如何通过位置检索值:
var team = ("Boston", "Red Sox", 97, 65, 59.9)
var city = team.0
var name = team.1
var wins = team.2
var loses = team.3
var percent = team.4
为了避免这一分解步骤,我们可以创建一个命名元组。命名元组将一个名称(键)与元组的每个元素关联起来。以下示例展示了如何创建命名元组:
var team = (city:"Boston", name:"Red Sox", wins:97, loses:65, percent:59.9)
要访问命名元组中的值,我们使用点语法。在前面代码中,我们将像这样访问元组的 city 元素:team.city。在前面代码中,team.city 元素将包含 Boston,team.name 元素将包含 Red Sox,team.wins 元素将包含 97,team.loses 元素将包含 65,最后,team.percent 元素将包含 59.9。
元组非常有用,可以用于各种目的。我发现它们非常适合替换那些仅用于存储数据且不包含任何方法的类和结构体。我们将在第五章类和结构体中了解更多关于类的内容。
使用 Cocoa 数据类型
到目前为止,在本章中,我们已经探讨了几个原生 Swift 数据类型,如字符串、数组和字典类型。虽然使用这些类型是首选的,但作为 Objective-C 兼容性的一部分,Apple 已经提供了方便且有效的方法,让我们在 Swift 应用程序中处理 Cocoa 数据类型。
一些 Cocoa 和 Swift 数据类型可以互换使用,而另一些则可以在 Cocoa 和 Swift 数据类型之间自动转换。那些可以互换使用或转换的数据类型被称为桥接数据类型。
Swift 还提供了对 Foundation 数据类型的覆盖,这使得我们能够以更接近原生 Swift 类型的方式处理 Foundation 数据类型。如果我们需要使用这些 Foundation 数据类型,我们需要在 Swift 文件的顶部添加以下导入语句:
import Foundation
让我们看看如何处理一些常见的 Cocoa 数据类型。
NSNumber
Swift 会自动将某些原生数值类型,如 Int、UInt、Float、Bool 和 Double 转换为 NSNumber 对象。这允许我们将这些原生数值类型传递给期望 NSNumber 对象的参数。这种自动转换是单向的,因为 NSNumber 对象可以包含各种数值类型;因此,Swift 编译器将不知道要将 NSNumber 转换为哪种数值类型。以下示例展示了如何将原生 Swift Int 和 Double 转换为 NSNumber,以及如何将其转换回 Swift Int 和 Double。让我们看一下以下代码:
var inum = 7 //Creates an Int
var dnum = 10.6 //Creates a Double
var insnum: NSNumber = inum //Bridges the Int to a NSNumber
var dnsnum: NSNumber = dnum //Bridges the Double to a NSNumber
var newint = Int(insnum) //Creates an Int from a NSNumber
var newdouble = Double(dnsnum) //Creates a Double from a NSNumber
在前面的代码中,Swift 自动将 inum 和 dnum 转换为 NSNumber 对象,而不需要进行类型转换;然而,当我们尝试将 NSNumber 对象转换回 Swift 的 Int 或 Double 类型时,我们需要将 NSNumber 对象类型转换为告诉 Swift 我们正在转换成哪种类型的数字。
NSString
Swift 会自动将原生 String 类型桥接到 NSString 类型;然而,它不会自动将 NSString 对象桥接到原生 String 类型。这允许我们将原生字符串类型传递给期望 NSString 对象的参数。因此,当我们混合使用 Objective-C API 与我们的 Swift 项目集成时,它会在需要时自动将 String 类型转换为 NSString 对象。
这种自动桥接允许我们在 Swift 字符串上调用 NSString 方法。Swift 自动将字符串转换为 NSString 对象并调用该方法。以下示例展示了如何使用来自 NSString 类型的 cStringUsingEncoding() 方法将我们的字符串值转换为 C 字符串:
var str = "Hello World from Swift"
str.cStringUsingEncoding(NSUTF8StringEncoding)
要将 NSString 对象转换为字符串类型,我们将使用 as 关键字。由于 NSString 对象始终可以转换为字符串类型,我们不需要使用此类型转换运算符的可选版本 (as?)。以下示例展示了如何将 NSString 对象转换为字符串类型:
func testFunc(test: String) {
print(test)
}
var nsstr: NSString = "abc"
testFunc(nsstr as String)
在下一个示例中,我们将 NSString 对象转换为原生 Swift 字符串类型,然后调用 Swift 字符串类型的 toInt() 方法,将字符串转换为整数,如下所示:
var nsstr: NSString = "1234"
var num = Int(nsstr as String)//num contains the number 1234
在前面的代码中,num 变量将包含数字 1234,而不是字符串 1234。
NSArray
Swift 会自动在 NSArray 类和 Swift 原生数组类型之间进行桥接。由于 NSArray 对象的元素不需要是同一类型,当我们从 NSArray 对象桥接到 Swift 数组时,Swift 数组的元素被设置为 [AnyObject] 类型。[AnyObject] 类型是一个 Objective-C 或 Swift 类的实例,或者可以桥接到其中一个。
以下示例展示了我们如何在 Swift 中创建一个包含字符串和 Int 类型的 NSArray 对象,然后从该 NSArray 对象创建一个 Swift 数组:
var nsarr: NSArray = ["HI","There", 1,2]
var arr = nsarr as? [AnyObject]
在前面的代码中,nsarr: NSArray 包含四个元素——HI、There、1 和 2。在 nsarr: NSArray 中,有两个元素是字符串类型,两个是 Int 类型。当我们将 nsarr: NSArray 转换为 Swift 数组时,arr 数组变成了 AnyObject 类型的数组。
如果 NSArray 包含特定的对象类型,一旦它被桥接到 Swift 数组,我们就可以将 Swift 数组 [AnyObject] 降级为特定对象类型的数组。这个降级的唯一问题是如果数组中的任何元素不是指定的对象类型,降级就会失败,并且新的数组将被设置为 nil。以下示例展示了如何降级一个数组:
var nsarr: NSArray = ["HI","There"]
var arr = nsarr as [AnyObject]
var newarr = arr as? [String]
在前面的例子中,newarr 将是一个包含两个元素的字符串数组。现在,让我们将原始的 NSArray 改为包含两个整数以及两个字符串。新的代码现在看起来将类似于以下这样:
var nsarr: NSArray = ["HI","There", 1, 2]
var arr = nsarr as [AnyObject]
var newarr = arr as? [String]
由于原始 NSArray 定义了一个包含字符串和整数的数组,当我们尝试将 Swift 数组从 [AnyObject] 数组降级为字符串数组时,降级失败,并且 newarr 变量被设置为 nil。
当我们使用 as? 关键字将 NSArray 对象转换为数组类型时,建议我们使用可选绑定,因为可能会接收到 nil 值。以下示例说明了如何使用可选绑定进行此转换:
var nsarr: NSArray = ["HI","There", 1,2]
if let arr = nsarr as? [String] {
// arr is a native Swift array type.
}
现在,让我们看看 NSDictionary 对象以及我们如何在 Swift 代码中使用它。
NSDictionary
我们使用 as 和 as? 关键字在 NSDictionary 对象和 Swift 字典类型之间进行转换。以下示例展示了如何进行此转换:
var nsdic: NSDictionary = ["one":"HI", "two":"There"]
if let dic = nsdic as? [String: String] {
var newDic = dicasNSDictionary
}
在前面的例子中,我们创建了一个包含两个键值对的 NSDictionary 对象。这个 NSDictionary 对象中的所有键和值都是字符串类型。在第二行,nsdic2as? [String: String] 将 NSDictionary 对象转换为键和值都是字符串类型的字典类型。然后我们使用 as 关键字将字典类型转换回 NSDictionaryobject。
在前面的例子中,当我们从 NSDictionary 对象转换为字典类型时,我们使用了可选绑定,因为如果 NSDictionary 对象中的任何值不是字符串类型,转换就会失败。以下示例说明了这一点:
var nsdic2: NSDictionary = ["one":"HI", "two":2]
if let dic2 = nsdic2 as? [String:String] {
// Would not reach this because
// conversion failed
}
在这个例子中,转换失败是因为其中一个值是整型而不是字符串类型。当我们从 NSDictionary 对象转换为 Swift 字典时,我们使用 as? 关键字,因为转换可能会失败,但当我们从 Swift 字典转换为 NSDictionary 对象时,我们使用 as 关键字,因为转换总是成功的。
现在让我们看看如何使用 Swift 中的 Foundation 数据类型。
Foundation 数据类型
当使用 Foundation 数据类型时,Swift 提供了一个覆盖层,使得与之交互的感觉就像它们是原生的 Swift 类型。我们使用这个覆盖层来与 Foundation 类型交互,例如 CGSize 和 CGRect 用于 iOS 应用程序(NSSize 和 NSRect 用于 OS X 应用程序)。在开发 iOS 或 OS X 应用程序时,我们将定期与 Foundation 数据类型交互,因此看到这个覆盖层在行动中是很好的。
让我们看看如何初始化一些 Foundation 数据类型。以下示例定义了 NSRange、CGRect 和 NSSize:
var range = NSRange(location: 3, length: 5)
var rect = CGRect(x: 10, y: 10, width: 20, height: 20)
var size = NSSize(width: 20, height: 40)
这个覆盖层还允许我们以类似原生 Swift 类型的感觉访问属性和函数。以下示例展示了如何访问属性和函数:
var rect = CGRect(x: 10, y: 10, width: 20, height: 20)
rect.origin.x = 20
//Changes the X value from 10 to 20
var rectMaxY = rect.maxY
//rectMaxY contains 30 (value of y + value of height)
var validRect = rect.isEmpty
//validRect contains false because rect is valid.
在前面的代码中,我们初始化了一个 CGRect 类型。然后我们将 x 属性从 10 改为 20,检索 maxY 属性的值,并检查 isEmpty 属性以确定我们是否有一个有效的 CGRect 类型。
我们只是刚刚触及了 Swift 和 Objective-C 之间互操作性的表面。我们将在本书后面的 第十三章 中深入讨论这种互操作性。
摘要
在本章中,我们介绍了 Swift 集合、元组、Foundation 和 Cocoa 数据类型。对 Swift 的原生集合类型有良好的理解对于用 Swift 架构和开发应用程序至关重要,因为除了非常基础的应用程序之外,所有应用程序都使用集合在内存中存储数据。
在撰写这本书的时候,Swift 仅用于在苹果(iOS 或 OS X)环境中开发应用程序,因此理解 Swift 如何与 Cocoa 和 Foundation 类型交互是至关重要的。虽然我们在本章中简要地介绍了这个主题,但我们将在本书后面的 第十三章,使用混合匹配 中更深入地探讨这种交互。
第四章。控制流和函数
当我在我的 Vic-20 上学习 BASIC 编程时,每个月我都会阅读几本早期的计算机杂志,如《Byte 杂志》。我记得我阅读的一个特别评论;它是一篇关于一款名为《Zork》的游戏的评论。虽然《Zork》不是为我的 Vic-20 提供的游戏,但游戏的概念深深吸引了我,因为我真的很喜欢科幻和奇幻。我记得在想,写一个那样的游戏会多么酷,所以我决定找出如何做到这一点。当时我必须掌握的最大概念之一是根据用户的操作控制应用程序的流程。
在本章中,我们将涵盖以下主题:
-
条件语句是什么以及如何使用它们
-
循环是什么以及如何使用它们
-
控制转移语句是什么以及如何使用它们
-
如何在 Swift 中创建和使用函数
我们到目前为止学到了什么
到目前为止,我们一直在为使用 Swift 编写应用程序打下基础。虽然我们可以用我们学到的东西编写一个非常基础的程序,但仅使用我们前三章中涵盖的内容来编写一个有用的应用程序将会非常困难。
从本章开始,我们将开始从 Swift 语言的根基中走出来,并开始学习使用 Swift 进行应用开发的构建块。在本章中,我们将介绍控制流和函数。要成为 Swift 编程语言的大师,完全理解和掌握本章以及第五章中讨论的概念是非常重要的。
在我们介绍控制流和函数之前,让我们看看在 Swift 中如何使用花括号和括号。
括号
在 Swift 中,与其它 C-like 语言不同,条件语句和循环需要使用花括号。在其它 C-like 语言中,如果条件语句或循环只有一个要执行的语句,那么围绕该行的花括号是可选的。这导致了大量的错误和 bug,例如苹果的goto fail bug;因此,当苹果设计 Swift 时,他们决定使用花括号,即使只有一行代码要执行。让我们看看一些说明这一点的代码。以下第一个例子在 Swift 中是无效的,因为它缺少花括号;然而,它将在大多数其它语言中是有效的:
if (x > y)
x=0
在 Swift 中,你需要使用花括号,如下面的例子所示:
if (x > y) {
x=0
}
括号
与其它 C-like 语言不同,Swift 中条件表达式的括号是可选的。在上面的例子中,我们围绕条件表达式放置了括号,但它们不是必需的。以下例子在 Swift 中是有效的,但在大多数 C-like 语言中则不是:
if x > y {
x=0
}
控制流
控制流,也称为流程控制,指的是在应用程序中语句、指令或函数执行的顺序。Swift 支持所有熟悉的 C 类语言中的控制流语句。这包括循环(包括 for 和 while)、条件语句(包括 if 和 switch)和控制语句的转移(包括 break 和 continue)。除了标准的 C 控制流语句之外,Swift 还添加了额外的语句,例如 for-in 循环,并增强了一些现有的语句,例如 switch 语句。
让我们从查看 Swift 中的条件语句开始。
条件语句
条件语句将检查条件,并且只有当条件为真时才会执行代码块。Swift 提供了 if 和 if-else 条件语句。让我们看看如何使用这些条件语句在指定的条件为真时执行代码块。
if 语句
if 语句将检查条件语句,如果条件为真,它将执行代码块。if 语句采用以下格式:
if condition {
block of code
}
现在,让我们看看如何使用 if 语句:
let teamOneScore = 7
let teamTwoScore = 6
if teamOneScore > teamTwoScore {
print("Team One Won")
}
在前面的示例中,我们首先设置了 teamOneScore 和 teamTwoScore 常量。然后我们使用 if 语句来检查 teamOneScore 的值是否大于 teamTwoScore 的值。如果值更大,我们将 Team One Won 打印到控制台。如果我们运行此代码,我们确实会看到 Team One Won 被打印到控制台,但如果 teamTwoScore 的值大于 teamOneScore 的值,则不会打印任何内容到控制台。这并不是编写应用程序的最佳方式,因为我们希望用户知道哪个队伍实际上获胜了。if-else 语句可以帮助我们解决这个问题。
使用 if-else 语句进行条件代码执行
if-else 语句将检查条件语句,如果条件为真,它将执行一个代码块。如果条件语句不为真,它将执行另一个代码块。if-else 语句遵循以下格式:
if condition {
block of code if true
} else {
block of code if not true
}
让我们修改前面的示例,使用 if-else 语句来告诉用户哪个队伍获胜:
var teamOneScore = 7
var teamTwoScore = 6
if teamOneScore > teamTwoScore {
print("Team One Won")
} else {
print("Team Two Won")
}
这个新版本将在 teamOneScore 的值大于 teamTwoScore 的值时打印出 Team One Won;否则,它将打印出消息,Team Two Won。你认为如果 teamOneScore 的值等于 teamTwoScore 的值,代码会做什么?在现实世界中,我们将会有平局,但在前面的代码中,我们将打印出 Team Two Won;这对第一队来说是不公平的。在这种情况下,我们可以使用多个 else if 语句和一个普通的 else 语句,如下面的示例所示:
var teamOneScore = 7
var teamTwoScore = 6
if teamOneScore > teamTwoScore {
print("Team One Won")
} else if teamTwoScore > teamOneScore {
print("Team Two Won")
} else {
print("We have a tie")
}
在前面的代码中,如果 teamOneScore 的值大于 teamTwoScore 的值,我们将打印 Team One Won 到控制台。然后我们还有一个 if 语句来检查 teamTwoScore 的值是否大于 teamOneScore 的值,但这个 if 语句跟随一个 else 语句,这意味着只有当前面的条件语句为假时,才会检查这个 if 语句。最后,如果两个 if 语句都为假,那么我们假设值是相等的,并将 We have a tie 打印到控制台。
条件语句会检查条件一次,如果条件满足,它将执行代码块。如果我们想连续执行代码块直到满足条件怎么办?为此,我们将使用 Swift 中的循环语句之一。让我们看看 Swift 中的循环语句。
For 循环
for 循环的变体可能是最广泛使用的循环语句。Swift 提供了基于 C 的标准 for 循环以及额外的 for-in 循环。基于 C 的标准 for 循环会执行代码块直到满足条件,通常是通过增加或减少计数器来实现的。for-in 语句将为范围、集合或序列中的每个项目执行代码块。当我们需要遍历集合或有一个固定次数要执行代码块时,我们通常使用 for 循环的变体之一。
使用 for 循环变体
让我们从查看基于 C 的标准 for 循环及其使用方法开始。for 语句的格式看起来类似于这个:
for initialization; condition; update-rule {
block of code
}
如前所述的格式所示,for 循环有三个部分:
-
初始化:这是初始化所需变量的地方;如果需要,可以包含多个初始化,用逗号分隔 -
条件:这是需要检查的条件;当条件为假时,循环将退出 -
更新规则:这是每个循环结束时需要更新的内容
理解各个部分被调用的顺序是很重要的。当代码执行遇到一个 for 循环时,会调用 for 循环的初始化部分来初始化变量。接下来,执行条件部分以验证是否应该执行代码块,如果是的话,它将执行代码块。最后,调用更新规则来在循环回跳并重新开始之前执行任何更新。
以下示例展示了如何使用 for 循环遍历一系列数字:
for var index = 1; index <= 4; index++ {
print(index)
}
在前面的例子中,index 变量被初始化为数字 1。在每次循环的开始,我们检查 index 变量是否等于或小于数字 4。如果 index 变量等于或小于数字 4,则执行代码的内层块,并将 index 变量的值打印到控制台。最后,我们在循环回跳并重新开始之前增加 index 变量。一旦 index 变量大于 4,for 循环就会退出。如果我们运行前面的例子,数字 1 到 4 将确实被打印到控制台。
for 循环最常见的一个用途是遍历一个集合并对该集合中的每个项目执行一段代码。让我们看看如何遍历一个数组,然后通过一个例子来看看如何遍历一个字典:
var countries = ["USA","UK", "IN"]
for var index = 0; index < countries.count; index++ {
print(countries[index])
}
在前面的例子中,我们首先使用三个国家的缩写初始化 countries 数组。在 for 循环中,我们将 index 变量初始化为 0(数组的第一个索引),并在 for 循环的条件语句中检查 index 变量是否小于 countries 数组中的元素数量。每次循环时,我们从 countries 数组中检索并打印由 index 变量指定的索引处的值。
新程序员在使用 for 循环遍历数组时犯的最大错误之一是使用小于或等于 (<=) 操作符而不是小于 (<) 操作符。使用小于或等于 (<=) 操作符会导致循环迭代次数过多,并在代码运行时生成 Index out of Bounds 异常。在前面的例子中,小于或等于 (<=) 操作符将生成从 0 到 3(包括)的计数,因为数组中有三个元素;然而,数组中的元素索引从 0 到 2(0、1 和 2)。因此,当我们尝试获取索引 3 的值时,将抛出 Index out of Bounds 异常。建议使用 for-in 循环遍历数组而不是标准 for 循环。我们将在本章稍后讨论 for-in 循环。
让我们看看如何使用基于 C 的标准 for 循环遍历字典:
var dic = ["USA": "United States", "UK": "United Kingdom", "IN":"India"]
var keys = Array(dic.keys)
for var index = 0; index < keys.count; index++ {
print(dic[keys[index]])
}
在前面的例子中,我们首先创建了一个包含国家名称作为值、缩写作为键的字典对象。然后我们使用字典的 keys 属性来获取键的数组。在 for 循环中,我们将 index 变量初始化为 0,验证 index 变量是否小于国家数组中的元素数量,并在每次循环结束时增加 index 变量。每次循环时,我们将打印国家的名称到控制台。
现在,让我们看看如何使用 for-in 语句以及它如何帮助我们防止在使用标准 for 语句时出现的常见错误。
使用 for-in 循环变体
在标准的for循环中,我们提供一个索引,然后循环直到满足条件。虽然这种方法非常好,但当我们想要遍历一系列数字时,它可能会导致错误,正如之前提到的,如果我们的条件语句不正确。for-in循环旨在防止这些类型的异常。
for-in循环遍历一个项集合或数字范围,并为集合或范围中的每个项执行一段代码。for-in语句的格式看起来类似于以下:
for variable in Collection/Range {
block of code
}
如前述代码所示,for-in循环有两个部分:
-
变量: 这个变量会在每次for-in循环执行时改变,并保存集合或范围内的当前项 -
集合/范围: 这是遍历的集合或范围
让我们看看如何使用for-in循环遍历一系列数字:
for index in 1...5 {
print(index)
}
在前面的例子中,我们遍历了从1到5的数字范围,并将每个数字打印到控制台。这个特定的for-in语句使用了闭包范围运算符(…)为for-in循环提供一个遍历的范围。Swift 还提供了一个名为半开范围运算符的第二个范围操作(..<)。半开范围运算符遍历数字范围,但不包括最后一个数字。让我们看看如何使用半开范围运算符:
for index in 1..<5 {
print(index)
}
在闭包范围运算符示例(…)中,我们将看到数字1到5被打印到控制台。在半开范围运算符示例中,最后一个数字(5)将被排除;因此,我们将看到数字1到4被打印到控制台。
现在,让我们看看如何使用for-in循环遍历数组:
var countries = ["USA","UK", "IN"]
for item in countries {
print(item)
}
在前面的例子中,我们遍历了countries数组,并将countries数组中的每个元素打印到控制台。正如我们所见,使用for-in循环遍历数组比使用标准的基于 C 的for循环更安全、更简洁,也更简单。使用for-in循环可以防止我们犯一些常见的错误,例如在条件语句中使用<=(小于等于)运算符而不是<(小于)运算符。
让我们看看如何使用for-in循环遍历字典:
var dic = ["USA": "United States", "UK": "United Kingdom", "IN":"India"]
for (abbr, name) in dic {
print("\(abbr) -- \(name)")
}
在前面的例子中,我们使用了for-in循环来遍历字典中的每个键值对。在这个例子中,字典中的每个项都作为(键,值)元组返回。我们可以在for-in循环体中将(键,值)元组成员分解为命名常量。需要注意的是,由于字典不保证存储项的顺序,遍历的顺序可能与插入的顺序不同。
现在,让我们看看另一种类型的循环,即while循环。
while循环
while循环会执行一个代码块,直到满足某个条件。Swift 提供了两种while循环的形式;这些是while循环和repeat-while循环。在 Swift 2.0 中,Apple 用repeat-while循环替换了do-while循环。repeat-while循环的功能与do-while循环完全相同。现在,Apple 使用do语句来进行错误处理。
当要执行的迭代次数未知且通常依赖于某些业务逻辑时,我们使用while循环。当你想运行零次或多次循环时,使用while循环;而当你想运行一次或多次循环时,使用repeat-while循环。
使用while循环
while循环首先评估一个条件语句,然后如果条件语句为真,则重复执行一个代码块。while语句的格式如下:
while condition {
block of code
}
让我们看看如何使用while循环。在下面的例子中,如果生成的随机数小于4,while循环将继续循环。在这个例子中,我们使用arc4random()函数在0和4之间生成一个随机数:
var ran = 0
while ran < 4 {
ran = Int(arc4random() % 5)
}
在前面的例子中,我们首先将ran变量初始化为0。然后while循环检查ran变量,如果其值小于4,就会生成一个新的随机数,介于0和4之间。只要生成的随机数小于4,while循环将继续循环。一旦生成的随机数等于或大于4,while循环将退出。
在前面的例子中,while循环在生成新的随机数之前会检查条件语句。如果我们不想在生成随机数之前检查条件语句呢?我们可以在第一次初始化ran变量时生成一个随机数,但这意味着我们需要复制生成随机数的代码,而复制代码永远不是一种理想的做法。在这种情况下,使用repeat-while循环会更合适。
使用repeat-while循环
while循环和repeat-while循环的区别在于,while循环在第一次执行代码块之前会检查条件语句;因此,所有在条件语句中的变量都需要在执行while循环之前初始化。repeat-while循环会在第一次检查条件语句之前运行循环块;这意味着我们可以在条件代码块中初始化变量。当条件语句依赖于循环块中的代码时,repeat-while循环的使用更为合适。repeat-while循环的格式如下:
repeat {
block of code
} while condition
让我们通过创建一个repeat-while循环来查看这个具体的例子,在这个循环中,我们在循环块内初始化我们要检查的变量,在条件while语句中:
var ran: Int
repeat {
ran = Int(arc4random() % 5)
} while ran < 4
在前面的例子中,我们将ran变量定义为Int类型,但我们直到进入循环块并生成随机数之前都没有初始化它。如果我们尝试使用while循环(未初始化ran变量),我们会收到一个变量在使用前未初始化异常。
switch语句
switch语句取一个值,然后将其与几个可能的匹配项进行比较,并根据第一次成功的匹配执行相应的代码块。当可能有多个匹配项时,switch语句是if-else语句的替代方案。switch语句的格式如下:
switch value {
case match1 :
block of code
case match2 :
block of code
…… as many cases as needed
default :
block of code
}
与大多数其他语言中的switch语句不同,在 Swift 中,它不会自动跳转到下一个case语句;因此,我们不需要使用break语句来防止跳转。这是 Swift 内置的另一个安全特性,因为初学者在switch语句中最常见的编程错误之一就是忘记在case语句的末尾添加break语句。让我们看看如何使用switch语句:
var speed = 300000000
switch speed {
case 300000000:
print("Speed of light")
case 340:
print("Speed of sound")
default:
print("Unknown speed")
}
在前面的例子中,switch语句将speed变量的值与两个case语句进行比较,如果speed的值与任一case匹配,它将打印出速度是多少。如果switch语句找不到匹配项,它将打印出Unknown speed消息。
每个switch语句都必须匹配所有可能的值。这意味着除非我们是在匹配枚举,否则每个switch语句都必须有一个default情况。让我们看看没有default情况的例子:
var num = 5
switch num {
case 1 :
print("number is one")
case 2 :
print("Number is two")
case 3 :
print("Number is three")
}
如果我们将前面的代码放入游乐场并尝试编译,我们会收到一个switch 必须完整,考虑添加默认情况错误。这是一个编译时错误;因此,我们只有在尝试编译代码时才会收到通知。
在单个case中可以包含多个项。要在单个case中设置多个项,我们需要用逗号分隔这些项。让我们看看我们如何使用switch语句来判断一个字符是元音还是辅音:
var char : Character = "e"
switch char {
case "a", "e", "i", "o", "u":
print("letter is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("letter is a consonant")
default:
print("unknown letter")
}
我们可以在前面的例子中看到,每个case都有多个项。这些项用逗号分隔,switch语句将尝试将char变量与case语句中列出的每个项匹配。
我们也可以检查switch语句的值,看看它是否包含在某个范围内。为此,我们会在case语句中使用范围运算符,如下面的示例所示:
var grade = 93
switch grade {
case 90...100:
print("Grade is an A")
case 80...89:
print("Grade is a B")
case 70...79:
print("Grade is an C")
case 60...69:
print("Grade is a D")
case 0...59:
print("Grade is a F")
default:
print("Unknown Grade")
}
在前面的例子中,switch语句将grade变量与每个case语句中的grade范围进行比较,并打印出相应的等级。
在 Swift 中,任何 case 语句都可以包含一个可选的 guard 条件,它可以提供额外的条件来验证。guard 条件是用 where 关键字定义的。假设,在我们的上一个例子中,我们有一些学生在课堂上得到了特殊帮助,我们想要为他们定义一个 D 等级,范围在 55 到 69 之间。以下示例展示了如何做到这一点:
var studentId = 4
var grade = 57
switch grade {
case 90...100:
print("Grade is an A")
case 80...89:
print("Grade is a B")
case 70...79:
print("Grade is an C")
case 55...69 where studentId == 4:
print("Grade is a D for student 4")
case 60...69:
print("Grade is a D")
case 0...59:
print("Grade is a F")
default:
print("Unknown Grade")
}
在处理 guard 表达式时,需要注意的一点是,Swift 将尝试从第一个 case 语句开始匹配值,并按顺序检查每个 case 语句。这意味着如果我们把带有 guard 表达式的 case 语句放在 Grade F case 语句之后,那么带有 guard 表达式的 case 语句将永远不会被触及。以下示例说明了这一点:
var studentId = 4
var grade = 57
switch grade {
case 90...100:
print("Grade is an A")
case 80...89:
print("Grade is a B")
case 70...79:
print("Grade is an C")
case 60...69:
print("Grade is a D")
case 0...59:
print("Grade is a F")
//The following case statement would never be reached because the
//grades would always match one of the previous two
case 55...69 where studentId == 4:
print("Grade is a D for student 4")
default:
print("Unknown Grade")
}
注意
一个好的经验法则是,如果你正在使用 guard 表达式,总是把带有 guard 条件的 case 语句放在任何没有 guard 表达式的类似 case 语句之前。
Switch 语句对于评估枚举也非常有用。由于枚举具有有限数量的值,如果我们为枚举中的所有值提供了 case 语句,我们就不需要提供默认情况。以下示例展示了我们如何使用 switch 语句来评估枚举:
Product {
case Book(String, Double, Int)
case Puzzle(String, Double)
}
var order = Product.Book("Mastering Swift 2", 49.99, 2015)
switch order {
case .Book(let name, let price, let year):
print("You ordered the book \(name) for \(price)")
case .Puzzle(let name, let price):
print("You ordered the Puzzle \(name) for \(price)")
}
在这个例子中,我们首先定义了一个名为 Product 的枚举,它有两个值,每个值都关联着相应的值。然后我们创建了一个 order 变量,其类型为产品类型,并使用 switch 语句来评估它。请注意,我们在 switch 语句的末尾没有放置默认情况。如果我们稍后向产品枚举中添加额外的值,我们需要在 switch 语句的末尾放置一个默认情况,或者添加额外的 case 语句来处理额外的值。
使用 case 和 where 语句与条件语句
正如我们在上一节中看到的,switch 语句中的 case 和 where 语句可以非常强大。从 Swift 2 开始,我们能够使用这些语句与 if、for 和 while 等其他条件语句一起使用。在我们的条件语句中使用 case 和 where 语句可以使我们的代码更加紧凑且易于阅读。让我们看看一些示例,从使用 where 语句在 for-in 循环中过滤结果开始。
使用 where 语句进行过滤
在本节中,我们将看到如何使用 where 语句来过滤 for-in 循环的结果。为了举例,我们将使用一个整数数组,并只打印出偶数;然而,在我们查看如何使用 where 语句过滤结果之前,让我们看看没有使用 where 语句会如何做:
for number in 1…30 {
if number % 2 == 0 {
print(number)
}
}
在这个例子中,我们使用一个 for-in 循环来遍历数字 1 到 30。在 for-in 循环内部,我们使用一个 if 条件语句来过滤出奇数。在这个简单的例子中,代码读起来相当容易,但让我们看看我们如何使用 where 语句来减少代码行数并使其更容易阅读:
for number in 1...30 where number % 2 == 0 {
print(number)
}
我们仍然有与上一个例子相同的 for-in 循环;然而,现在我们将 where 语句放在最后,在这个特定的例子中,我们只遍历偶数。使用 where 语句缩短了我们的例子两行,并且也使其更容易阅读,因为过滤语句与 for-in 循环在同一行,而不是嵌入在循环本身中。
现在,让我们看看如何使用 for-case 语句进行过滤。
使用 for-case 语句进行过滤
在下一个例子中,我们将使用 for-case 语句来过滤一个元组数组,并仅打印出符合我们标准的结果。for-case 例子与我们之前看到的 where 语句非常相似,它旨在消除循环中过滤结果时需要 if 语句的需求。在这个例子中,我们将使用 for-case 语句来过滤一系列世界系列冠军,并打印出特定球队赢得世界系列赛的年份:
var worldSeriesWinners = [
("Red Sox", 2004),
("White Sox", 2005),
("Cardinals", 2006),
("Red Sox", 2007),
("Phillies", 2008),
("Yankees", 2009),
("Giants", 2010),
("Cardinals", 2011),
("Giants", 2012),
("Red Sox", 2013),
("Giants", 2014)
]
for case let ("Red Sox", year) in worldSeriesWinners {
print(year)
}
在这个例子中,我们创建了一个名为 worldSeriesWinners 的元组数组,其中数组中的每个元组都包含球队的名称和他们赢得世界系列赛的年份。然后我们使用 for-case 语句来过滤这个数组,并仅打印出红袜队赢得世界系列赛的年份。过滤是在 case 语句中完成的,其中 ("Red Sox", year) 表示我们想要所有第一个元素为字符串 "Red Sox" 且第二个元素的值赋给 year 常量的结果。然后 for 循环遍历 case 语句的结果,并打印出 year 常量的值。
for-case 语句也使得在可选数组中过滤出 nil 值变得非常容易。让我们看看一个这样的例子:
let myNumbers: [Int?] = [1, 2, nil, 4, 5, nil, 6]
for case let .Some(num) in myNumbers {
print(num)
}
在这个例子中,我们创建了一个名为 myNumbers 的可选数组,它可能包含一个整数值,也可能包含 nil。正如我们将在第十章中看到的那样,使用可选类型,可选在内部被定义为枚举,如下面的代码所示:
enum Optional<T> {
case None,
case Some(T)
}
如果一个可选被设置为 nil,它将有一个值为 None,但如果它不是 nil,那么它将有一个值为 Some,并关联实际值的类型。在我们的例子中,当我们过滤 .Some(num) 时,我们正在寻找任何具有 .Some (非 nil 值) 的可选。作为 .Some() 的简写,我们可以使用 ?(问号)符号,正如我们将在下面的例子中看到的那样。
我们还可以将 for-case 与 where 语句结合使用以进行额外的过滤,如下例所示:
let myNumbers: [Int?] = [1, 2, nil, 4, 5, nil, 6]
for case let num? in myNumbers where num > 3 {
print(num)
}
这个例子与上一个例子相同,只是我们添加了 where 语句的额外过滤。在上一个例子中,我们遍历了所有非 nil 值,但在本例中,我们遍历了大于 3 的非 nil 值。让我们看看我们如何在不使用 case 或 where 语句的情况下进行相同的过滤:
for num in myNumbers {
if let num = num {
if num > 3 {
print(num)
}
}
}
如我们所见,使用 for-case 和 where 语句可以大大减少所需的行数。它还使我们的代码更容易阅读,因为所有过滤语句都在同一行上。
让我们再看一个过滤示例。这次,我们将查看 if-case 语句。
使用 if-case 语句
使用 if-case 语句与使用 switch 语句非常相似。大多数情况下,switch 语句是首选,但在某些情况下,if-case 语句更好。其中一种情况是我们只寻找一两个可能的匹配项,并且我们不希望处理所有可能的匹配项。让我们看看这个例子:
enum Identifier {
case Name(String)
case Number(Int)
case NoIdentifier
}
var playerIdentifier = Identifier.Number(42)
if case let .Number(num) = playerIdentifier {
print("Player's number is \(num)")
}
在这个例子中,我们创建了一个名为 Identifier 的枚举,其中包含三个可能的值:Name、Number 和 NoIdentifier。我们创建了一个名为 playerIdentifier 的 Identifier 枚举实例,其值为 Number,关联值为 42。然后我们使用 if-case 语句检查 playerIdentifier 是否具有 Number 值,如果是,则向控制台打印一条消息。
就像 for-case 语句一样,我们能够使用 where 语句进行额外的过滤。以下示例使用了我们在上一个例子中使用的相同的 Identifier 枚举:
var playerIdentifier = Identifier.Number(42)
if case let .Number(num) = playerIdentifier where num == 2 {
print("Player is either Xander Bogarts or Derek Jeter")
}
在这个例子中,我们仍然使用 if-case 语句来检查 playerIdentifier 是否具有 Number 值,但我们添加了 where 语句来检查关联值是否等于 42,如果是,我们将玩家标识为 Xander Bogarts 或 Derek Jeter。
正如我们在示例中看到的那样,使用 case 和 where 语句与我们的条件语句结合可以减少进行某些类型过滤所需的行数。它还可以使我们的代码更容易阅读。现在让我们看看控制转移语句。
控制转移语句
控制转移语句用于将控制权转移到代码的另一个部分。Swift 提供了五种控制转移语句;这些是 continue、break、fallthrough、guard、throws 和 return。我们将在本章后面的 函数 部分中查看 return 语句,并在 第七章 中讨论 throws 语句,使用可用性和错误处理编写更安全的代码。
continue 语句
continue 语句告诉循环停止执行代码块并转到循环的下一个迭代。以下示例展示了如何使用 continue 语句仅打印出范围内的奇数:
for i in 1...10 {
if i % 2 == 0 {
continue
}
print("\(i) is odd")
}
在前面的示例中,我们遍历 1 到 10 的范围。对于 for-in 循环的每次迭代,我们使用取余 (%) 运算符来判断数字是奇数还是偶数。如果数字是偶数,continue 语句告诉循环立即转到下一个迭代。如果数字是奇数,我们打印出该数字是奇数,然后继续前进。前面代码的输出如下:
1 is odd
3 is odd
5 is odd
7 is odd
9 is odd
现在,让我们看看 break 语句。
break 语句
break 语句立即结束控制流中的代码块执行。以下示例展示了当我们遇到第一个偶数时如何从 for 循环中退出:
for i in 1...10 {
if i % 2 == 0 {
break
}
print("\(i) is odd")
}
在前面的示例中,我们遍历 1 到 10 的范围。对于 for 循环的每次迭代,我们使用取余 (%) 运算符来判断数字是奇数还是偶数。如果数字是偶数,我们使用 break 语句立即退出循环。如果数字是奇数,我们打印出该数字是奇数,然后进入循环的下一个迭代。前面的代码有以下输出:
1 is odd
跌落语句
在 Swift 中,switch 语句不像其他语言那样会跌落;然而,我们可以使用 fallthrough 语句来强制它们跌落。fallthrough 语句可能非常危险,因为一旦找到匹配项,下一个情况默认为真,并且执行该代码块。以下示例说明了这一点:
var name = "Jon"
var sport = "Baseball"
switch sport {
case "Baseball":
print("\(name) plays Baseball")
fallthrough
case "Basketball":
print("\(name) plays Basketball")
fallthrough
default:
print("Unknown sport")
}
在前面的示例中,由于第一个情况 Baseball 与代码匹配,并且剩余的代码块也执行,输出看起来类似于以下内容:
Jon plays Baseball
Jon plays Basketball
Unknown sport
guard 语句
在 Swift 和大多数现代语言中,我们的条件语句往往侧重于测试一个条件是否为真。例如,以下代码检查变量 x 是否大于 10,如果是,则执行某些函数;否则,处理错误条件:
var x = 9
if x > 10 {
// Functional code here
} else {
// Do error condition
}
这种类型的代码导致我们的功能代码嵌入到我们的检查中,并且错误条件被放在函数的末尾,但如果我们不希望这样呢?有时,在函数的开始处理错误条件可能更好。我知道,在我们的简单示例中,我们可以轻松检查 x 是否小于或等于 10,如果是,则执行错误条件,但并非所有条件语句都那么容易重写,尤其是像可选绑定这样的项目。
在 Swift 2 中,Apple 引入了新的 guard 语句。guard 语句侧重于在条件为假时执行一个函数;这允许我们在函数的早期捕获错误并执行错误条件。我们可以使用 guard 语句重写之前的示例,如下所示:
var x = 9
guard x > 10 else {
// Do error condition
return
}
// Functional code here
在这个新示例中,我们检查变量 x 是否大于 10,如果不是,我们执行错误条件。如果变量 x 大于 10,我们的代码将继续执行。我们注意到在错误条件代码中嵌入了一个 return 语句。guard 语句中的代码必须包含一个控制转移语句;这正是防止其他代码执行的原因。如果我们忘记了控制转移语句,Swift 将在编译时显示错误。
让我们看看 guard 语句的一些更多示例。以下示例展示了我们如何使用 guard 语句来验证一个可选值是否包含有效值:
func guardFunction(str: String?) {
guard let goodStr = str else {
print("Input was nil")
return
}
print("Input was \(goodStr)")
}
在这个示例中,我们创建了一个名为 guardFunction() 的函数,它接受一个包含字符串或 nil 值的可选值。然后我们使用 guard 语句和可选绑定来验证字符串可选值不包含 nil。如果它包含 nil,则执行 guard 语句中的代码,并使用 return 语句退出函数。使用 guard 语句和可选绑定时真正令人愉快的事情是,新变量在整个函数的范围内有效,而不仅仅是可选绑定语句的范围内。
现在我们已经看到了 Swift 中控制流语句的工作方式,让我们来介绍 Swift 中的函数和类。
函数
在 Swift 中,函数是一个执行特定任务的独立代码块。函数通常用于将我们的代码逻辑上分解成可重用的命名块。我们使用函数的名称来调用函数。
当我们定义一个函数时,我们还可以选择性地定义一个或多个参数(也称为参数)。参数是通过调用它的代码传递到函数中的命名值。这些参数通常在函数内部用于执行函数的任务。我们还可以为参数定义默认值,以简化函数的调用方式。
每个 Swift 函数都有一个与其关联的类型。这个类型被称为返回类型,它定义了从函数返回到调用它的代码的数据类型。如果一个函数没有返回值,则返回类型为 Void。
让我们看看如何在 Swift 中定义函数。
使用单个参数函数
定义 Swift 中函数的语法非常灵活。这种灵活性使得我们能够轻松地定义简单的 C 风格函数或更复杂的 Objective-C 风格函数,包括局部和外部参数名称。让我们看看如何定义函数的一些示例。以下示例接受一个参数,并且不向调用它的代码返回任何值(返回类型—void):
func sayHello(name: String) -> Void {
let retString = "Hello " + name
print( retString)
}
在前面的例子中,我们定义了一个名为 sayHello 的函数,它接受一个名为 name 的变量。在函数内部,我们打印出一个 Hello 问候语给这个人的名字。一旦函数内部的代码执行完毕,函数就会退出,控制权返回给调用它的代码。如果我们不想打印问候语,而是想将问候语返回给调用它的代码,我们可以添加一个返回类型,如下所示:
func sayHello2(name: String) ->String {
let retString = "Hello " + name
return retString
}
-> 字符串定义了与函数相关联的返回类型是一个字符串。这意味着该函数必须将一个 string 变量返回给调用它的代码。在函数内部,我们构建一个包含问候信息的 string 常量,然后使用 return 关键字返回这个 string 常量。
调用 Swift 函数与在其他语言(如 C 或 Java)中调用函数或方法非常相似。以下示例展示了如何在函数内部调用将问候消息打印到屏幕上的 sayHello() 函数:
sayHello("Jon")
现在,让我们看看如何调用返回值的 sayHello2() 函数,该函数将值返回给调用它的代码:
var message = sayHello2("Jon")
print(message)
在前面的例子中,我们调用了 sayHello2() 函数,并将返回的值存储在 message 变量中。如果一个函数定义了返回类型,例如 sayHello2() 函数那样,它必须返回一个与该类型相匹配的值给调用它的代码。因此,函数内的每一个可能的条件路径都必须以返回指定类型的值结束。这并不意味着调用函数的代码必须检索返回的值。以下是一个例子,其中两行都是有效的:
sayHello2("Jon")
var message = sayHello2("Jon")
如果你没有指定一个变量来存储返回值,该值将被丢弃。
使用多参数函数
我们在函数中不仅限于只有一个参数,我们还可以定义多个参数。要创建一个多参数函数,我们在括号中列出参数,并用逗号分隔参数定义。让我们看看如何在函数中定义多个参数:
func sayHello(name: String, greeting: String) {
print("\(greeting) \(name)")
}
在前面的例子中,该函数接受两个参数:name 和 greeting。然后我们使用这两个参数在控制台上打印一个 greeting。
调用一个多参数函数与调用一个单参数函数略有不同。在调用多参数函数时,我们用逗号分隔参数,并且除了第一个参数之外,我们还需要包括所有其他参数的名称。以下示例展示了如何调用一个多参数函数:
sayHello("Jon", greeting:"Bonjour")
如果我们为参数定义了默认值,我们不需要为函数的每个参数提供一个参数。让我们看看如何为我们的参数配置默认值。
定义参数的默认值
我们可以在函数定义中通过使用等于运算符(=)为参数定义默认值,当我们声明变量时。以下示例展示了如何声明一个具有默认参数值的函数:
func sayHello(name: String, greeting: String = "Bonjour") {
print("\(greeting) \(name)")
}
在函数声明中,我们定义了一个没有默认值(name: String)的参数和一个具有默认值(greeting: String = "Bonjour")的参数。当一个参数有默认值声明时,我们可以选择在调用函数时为该参数设置值或不设置值。以下示例展示了如何调用 sayHello() 函数而不设置 greeting 参数,以及如何设置 greeting 参数来调用它:
sayHello("Jon")
sayHello("Jon", greeting: "Hello")
在 sayHello("Jon") 行中,sayHello() 函数将输出消息 Bonjour Jon,因为它使用了 greeting 参数的默认值。在 sayHello("Jon", greeting: "Hello") 行中,sayHello() 函数将输出消息 Hello Jon,因为我们覆盖了 greeting 参数的默认值。
我们可以通过使用参数名称来声明多个具有默认值的参数,并且只覆盖我们想要的那些。以下示例展示了我们如何通过在调用时覆盖其中一个默认值来实现这一点:
func sayHello4(name: String, name2: String = "Kim", greeting: String = "Bonjour") {
println("\(greeting) \(name) and \(name2)")
}
sayHello("Jon", greeting: "Hello")
在上述示例中,我们声明了一个没有默认值(name: String)的参数和两个具有默认值(name2: String = "Kim", greeting: String = "Bonjour")的参数。然后我们调用函数,让 name2 参数保持其默认值,但覆盖了 greeting 参数的默认值。
上述示例将输出消息,Hello Jon and Kim。
从函数中返回多个值
有几种方法可以从 Swift 函数中返回多个值。最常见的方法是将值放入集合类型(数组或字典)中,然后返回该集合。以下示例展示了如何从 Swift 函数中返回一个集合类型:
func getNames() -> [String] {
var retArray = ["Jon", "Kim", "Kailey", "Kara"]
return retArray
}
var names = getNames()
在上述示例中,我们声明了没有参数且返回类型为 [String] 的 getNames() 函数。[String] 返回类型指定返回类型为字符串类型的数组。
返回集合类型的一个缺点是集合中的值必须是同一类型,或者我们必须将我们的集合类型声明为 AnyObject 类型。在上述示例中,我们的数组只能返回字符串类型。如果我们需要与字符串一起返回数字,我们可以返回一个 AnyObjects 类型的数组,然后使用类型转换来指定对象类型。然而,这并不是我们应用程序的一个很好的设计,因为它很容易出错。返回不同类型值的一个更好的方式是使用元组类型。
当我们从函数返回一个元组时,建议我们使用命名元组,这样我们可以使用点语法来访问返回的值。以下示例展示了如何从函数中返回一个命名元组并访问返回的命名元组的值:
func getTeam() -> (team:String, wins:Int, percent:Double) {
let retTuple = ("Red Sox", 99, 0.611)
return retTuple
}
var t = getTeam()
print("\(t.team) had \(t.wins) wins")
在前面的示例中,我们定义了getTeam()函数,该函数返回一个包含三个值(String、Int和Double)的命名元组。在函数内部,我们创建了将要返回的元组。注意,只要元组中的值类型与函数定义中的值类型匹配,我们就不需要将将要返回的元组定义为命名元组。然后我们可以像调用其他函数一样调用该函数,并使用点语法来访问返回的元组的值。在前面示例中,代码会打印出以下行:
Red Sox had 99 wins
返回可选值
在前面的章节中,我们从函数中返回了非nil值;然而,这并不总是我们所需要的。如果我们需要从函数中返回一个nil值,会发生什么?以下代码会抛出expression does not conform to type 'NilLiteralConvertible'异常:
func getName() ->String {
return nil
}
这段代码抛出异常的原因是我们将返回类型定义为string值;然而,我们尝试返回nil。如果有必要返回nil,我们需要将返回类型定义为可选类型,以便调用它的代码知道该值可能是nil。要定义返回类型为可选类型,我们使用问号(?)的方式,就像我们定义一个变量为可选类型时一样。以下示例展示了如何定义可选返回类型:
func getName() ->String? {
return nil
}
前面的代码不会抛出异常。
我们也可以将元组设置为可选类型,或者将元组中的任何值设置为可选类型。以下示例展示了我们如何将元组作为可选类型返回:
func getTeam2(id: Int) -> (team:String, wins:Int, percent:Double)? {
if id == 1 {
return ("Red Sox", 99, 0.611)
}
return nil
}
在下面的示例中,我们可以返回一个在函数定义中定义的元组,或者返回一个nil;这两种选项都是有效的。如果我们需要元组中的某个值是nil,我们需要在元组中添加一个可选类型。以下示例展示了如何在元组中返回一个nil:
func getTeam() -> (team:String, wins:Int, percent:Double?) {
let retTuple: (String, Int, Double?) = ("Red Sox", 99, nil)
return retTuple
}
在前面的示例中,我们可以将percent值设置为Double值或nil。
添加外部参数名称
在本节前面的示例中,参数的定义方式类似于我们在 C 代码中定义参数的方式,即我们定义参数名称和值类型。当我们调用函数时,我们也可以像调用 C 代码中的函数一样调用它,其中我们使用函数名称并在括号内指定传递给函数的值。在 Swift 中,我们不仅限于这种语法;我们还可以使用外部参数名称。
当我们调用一个函数以指示每个参数的目的时,会使用外部参数名称。如果我们想在函数中使用外部参数名称,我们需要为每个参数定义一个外部参数名称,除了其局部参数名称之外。外部参数名称在函数定义中添加在局部参数名称之前。外部和局部参数名称之间用空格分隔。
让我们看看如何使用外部参数名称。但在这样做之前,让我们回顾一下我们之前是如何定义函数的。在接下来的两个示例中,我们将定义一个没有外部参数名称的函数,然后我们将使用外部参数名称重新定义该函数:
func winPercentage(team: String, wins: Int, loses: Int) -> Double {
return Double(wins) / Double(wins + loses)
}
在前面的示例中,我们定义了接受三个参数的winPercentage()函数。这些参数是team、wins和loses。team参数是String类型,而wins和loses参数是Int类型。以下代码行展示了如何调用winPercentage()函数:
var per = winPercentage("Red Sox", wins: 99, loses: 63)
现在,让我们使用外部参数名称定义相同的函数:
func winPercentage(BaseballTeam team: String, withWins wins: Int, andLoses losses: Int) -> Double {
return Double(wins) / Double(wins + losses)
}
在前面的示例中,我们使用外部参数名称重新定义了winPercentage函数。在这个重新定义中,我们有相同的三个参数:team、wins和losses。区别在于我们定义参数的方式。当使用外部参数时,我们使用外部参数名称和局部参数名称(两者之间用空格分隔)来定义每个参数。在前面的示例中,第一个参数有一个外部参数名称BaseballTeam,内部参数名称team,以及类型String。
当我们使用外部参数名称调用一个函数时,我们需要在函数调用中包含外部参数名称。以下代码展示了如何调用前面示例中的函数:
var per = winPercentage(BaseballTeam:"Red Sox", withWins:99, andLoses:63)
虽然使用外部参数名称需要更多的输入,但它确实使代码更容易阅读。在前面的示例中,很容易看出函数正在寻找一支棒球队的名称,第二个参数是获胜次数,最后一个参数是失败次数。
使用可变参数
可变参数是指可以接受零个或多个指定类型的值的参数。在函数定义内部,我们通过在参数类型名称后附加三个点(...)来定义可变参数。可变参数的值以指定类型的数组形式提供给函数。以下示例展示了我们如何在一个函数中使用可变参数:
func sayHello(greeting: String, names: String...) {
for name in names {
print("\(greeting) \(name)")
}
}
在前面的示例中,sayHello()函数接受两个参数。第一个参数是字符串类型,即要使用的问候语。第二个参数是字符串类型的可变参数,即要向其发送问候语的名字。在函数内部,可变参数是一个包含指定类型的数组;因此,在我们的示例中,names参数是一个包含String值的数组。在这个例子中,我们使用一个for-in循环来访问names参数中的值。
以下行代码展示了如何使用可变参数调用sayHello()函数:
sayHello("Hello", names: "Jon", "Kim")
上一行代码将打印两个问候语:Hello Jon和Hello Kim。
参数作为变量
默认情况下,参数是常量,这意味着它们在函数内部不能被更改。让我们看看以下示例:
func sayHello(greeting: String, name: String, count: Int) {
while count > 0 {
print("\(greeting) \(name)")
count--
}
}
如果我们尝试运行此示例,我们将得到一个异常,因为我们尝试使用递减运算符(--)更改count参数的值。如果我们需要在函数内部更改参数的值,我们需要通过在函数定义中使用var关键字来指定该参数是一个变量。
以下示例展示了如何将count参数声明为一个变量(而不是一个常量),这样我们就可以在函数内部更改其值:
func sayHello(greeting: String, name: String, var count: Int) {
while (count > 0) {
println("\(greeting) \(name)")
count--
}
}
你可以看到在前面的示例中,我们在count参数名称之前添加了var关键字。这指定了参数是一个变量而不是常量;因此,我们可以在函数中更改count参数的值。
使用inout参数
如我们刚才所描述的,变量参数只能更改函数内部的参数值;因此,任何更改在函数结束后都会丢失。如果我们希望参数的更改在函数结束后仍然持续,我们需要将参数定义为inout参数。对inout参数所做的任何更改都会传递回函数调用中使用的变量。
使用inout参数时有两个需要注意的事项:这些参数不能有默认值,也不能是可变参数。
让我们看看如何使用inout参数来交换两个变量的值:
func swap(inout first: String, inout second: String) {
let tmp = first
first = second
second = tmp
}
此函数将接受两个参数,并交换函数调用中使用的变量的值。当我们调用函数时,我们在变量名称前放置一个井号(&),表示函数可以修改其值。以下示例展示了如何调用反转函数:
var one = "One"
var two = "Two"
swap(&one,&two)
print("one: \(one) two: \(two)")
在前面的示例中,我们将变量one设置为值One,变量two设置为值Two。然后我们使用one和two变量调用反转函数。一旦交换函数返回,名为one的变量将包含值Two,而名为two的变量将包含值One。
函数嵌套
我们迄今为止所展示的所有函数都是全局函数的例子。全局函数是在它们所在的类或文件中的全局作用域内定义的。Swift 还允许我们在一个函数内部嵌套另一个函数。嵌套函数只能在封装函数内部调用;然而,封装函数可以返回一个嵌套函数,这允许它在封装函数的作用域之外使用。我们将在本书的第十二章中介绍如何返回一个函数,使用闭包。
让我们看看如何通过创建一个简单的排序函数来嵌套函数,该函数将接受一个整数数组并对其进行排序:
func sort(inout numbers: [Int]) {
//This is the nested function
func reverse(inout first: Int, inout second: Int) {
let tmp = first
first = second
second = tmp
}
//Nested function ends.
var count = numbers.count
while count > 0 {
for var i = 1; i < count; i++ {
if numbers[i] < numbers[i-1] {
reverse(&numbers[i], second: &numbers[i-1])
}
}
count--
}
}
在前面的代码中,我们首先创建了一个名为sort的全局函数,它接受一个inout参数,即Ints数组。在sort函数内部,我们首先定义了一个名为reverse的嵌套函数。函数需要在调用之前在代码中定义,因此将所有嵌套函数放在全局函数的开始部分是一个好习惯,这样我们就可以在调用它们之前知道它们已经被定义了。reverse函数简单地交换传入的两个值。
在sort函数的主体中,我们实现了简单排序的逻辑。在这个逻辑中,我们比较数组中的两个数字,如果需要交换这两个数字,我们就调用嵌套的reverse函数来交换这两个数字。这个例子展示了我们如何有效地使用嵌套函数来组织我们的代码,使其易于维护和阅读。让我们看看如何调用全局的排序函数:
var nums: [Int] = [6,2,5,3,1]
sort(&nums)
for value in nums {
print("--\(value)")
}
上述代码创建了一个包含五个整数的数组,然后将该数组传递给sort函数。当sort函数返回nums数组时,它包含了一个排序后的数组。
注意
正确使用嵌套函数时,它们可以非常有用。然而,过度使用它们是非常容易的。在创建嵌套函数之前,你可能想问自己为什么想要使用嵌套函数,以及通过使用嵌套函数你解决了什么问题。
将所有这些放在一起
为了巩固我们在本章学到的内容,让我们再看一个例子。对于这个例子,我们将创建一个函数来测试一个字符串值是否包含有效的 IPv4 地址。IPv4 地址是分配给使用互联网协议进行通信的计算机的地址。IP 地址由四个数值组成,范围从 0-255,由点(句号)分隔。一个有效的 IP 地址示例是 10.0.1.250:
func isValidIP(ipAddr: String?) -> Bool {
guard let ipAddr = ipAddr else {
return false
}
let octets = ipAddr.characters.split { $0 == "."}.map{String($0)}
guard octets.count == 4 else {
return false
}
func validOctet(octet: String) -> Bool {
guard let num = Int(String(octet))
where num >= 0 && num < 256 else {
return false
}
return true
}
for octet in octets {
guard validOctet(octet) else {
return false
}
}
return true
}
由于isValidIp()函数的参数是可选类型,我们首先需要验证ipAddr参数是否不为 nil。为此,我们使用了一个带有可选绑定的guard语句,如果可选绑定失败,则返回一个布尔值false,因为 nil 不是一个有效的 IP 地址。
如果 ipAddr 参数包含一个非空值,我们将字符串分割成字符串数组,以点为分隔符。由于 IP 地址应该包含由点分隔的四个数字,我们使用 guard 语句来检查数组是否包含四个元素。如果不包含,我们返回 false,因为我们知道 ipAddr 参数没有包含有效的 IP 地址。
接下来,我们创建了一个名为 validOctet() 的嵌套函数,它有一个名为 octet 的 String 参数。这个嵌套函数将验证 octet 参数是否包含介于 0 和 255 之间的数值,如果是,它将返回一个 Boolean true 值,否则,它将返回一个 false Boolean 值。
最后,我们遍历通过在点处分割原始 ipAddr 参数创建的数组中的值,并将这些值传递给 validOctet() 嵌套函数。如果所有四个值都通过 validOctet() 函数的验证,我们就有一个有效的 IP 地址,并返回一个 Boolean true 值;然而,如果任何一个值未能通过 validOctet() 函数的验证,我们返回一个 Boolean false 值。
摘要
在本章中,我们介绍了 Swift 中的控制流和函数。在继续学习之前,理解本章中的概念是至关重要的。我们编写的每一个应用程序,除了简单的 Hello World 应用程序之外,都将非常依赖控制流语句和函数。
控制流语句用于在应用程序中做出决策,而函数将用于将我们的代码分组到可重用和组织化的部分。
第五章。类和结构体
我最初学习的编程语言是 BASIC。这是一个开始编程的好语言,但当我用 Commodore Vic-20 交换成 PCjr(是的,我确实有 PCjr,并且真的很喜欢它)时,我意识到还有其他更高级的语言,于是花了很多时间学习 Pascal 和 C。直到我开始上大学,我才听到“面向对象语言”这个术语。当时,面向对象语言还非常新,没有真正的课程,但我能够尝试使用 C++ 进行一些实验。毕业后,我放弃了面向对象编程,直到几年后,当我再次开始尝试使用 C++ 时,我才真正发现了面向对象编程的强大和灵活性。
在本章中,我们将涵盖以下主题:
-
创建和使用类和结构体
-
为类和结构体添加属性和属性观察器
-
为类和结构体添加方法
-
为类和结构体添加初始化器
-
使用访问控制
-
创建类层次结构
-
扩展类
-
理解内存管理和 ARC
什么是类和结构体?
在 Swift 中,类和结构体非常相似。如果我们真的想精通 Swift,了解使类和结构体如此相似以及它们之间区别的原因非常重要,因为它们是应用程序的构建块。Apple 将类和结构体描述为:
"类和结构体是通用、灵活的构造,成为你程序代码的构建块。你通过使用已经熟悉的常量、变量和函数的语法来定义属性和方法,为你的类和结构体添加功能。"
让我们先快速看一下类和结构体之间的一些相似之处。
类和结构体之间的相似性
在 Swift 中,类和结构体比其他语言,如 Objective-C,更为相似。以下是一些类和结构体共有的特性列表:
-
属性:这些用于在类和结构体中存储信息
-
方法:这些为我们的类和结构体提供功能
-
初始化器:这些用于初始化我们的类和结构体实例
-
下标:这些通过下标语法提供对值的访问
-
扩展:这些有助于扩展类和结构体
现在,让我们快速看一下类和结构体之间的一些区别。
类和结构体之间的区别
虽然类和结构体非常相似,但也有几个非常重要的区别。以下是在 Swift 中类和结构体之间的一些区别列表:
-
类型:结构体是一个值类型,而类是一个引用类型
-
继承:结构体不能从其他类型继承,而类可以
-
初始化器:在类存在时,结构体不能有自定义的析构器
-
多重引用:我们可以对一个类的实例有多个引用;然而,对于结构体来说,则不行
在本章中,我们将强调类和结构体之间的区别,以帮助我们了解何时使用哪一个。在我们真正深入类和结构体之前,让我们看看值类型(结构体)和引用类型(类)之间的区别。为了理解何时使用类和结构体以及如何正确使用它们,了解值类型和引用类型之间的区别是非常重要的。
值类型与引用类型
枚举和元组等结构体是值类型。这意味着当我们在我们应用程序中传递结构体的实例时,我们传递的是结构体的副本,而不是原始结构体。类是引用类型,这意味着当我们在我们应用程序中传递类的实例时,我们传递的是原始实例的引用。理解值类型和引用类型之间的区别非常重要。在这里,我们将提供一个非常高级的概述,并在本章末尾的 内存管理 部分提供更多详细信息。
当我们在应用程序中传递结构体时,我们传递的是结构体的副本,而不是原始结构体。由于函数获得的是结构体的副本,它可以按需更改它,而不会影响结构体的原始实例。
当我们在应用程序中传递类的实例时,我们传递的是该类原始实例的引用。由于我们将类的实例传递给函数,函数获得的是原始实例的引用;因此,在函数退出后,函数内所做的任何更改都将保持不变。
为了说明值类型和引用类型之间的区别,让我们看看一个现实世界的对象——一本书。如果我们有一个朋友想阅读 Mastering Swift,我们可以给他们买自己的副本,或者分享我们的副本。
如果我们为我们朋友买了书的副本,那么他们在书中做的任何笔记都会保留在他们自己的副本中,而不会反映在我们的副本中。这就是结构体和变量使用按值传递的方式。在函数中对结构体或变量所做的任何更改都不会反映在结构体或变量的原始实例中。
如果我们分享我们的书副本,那么他们在书中做的任何笔记在他们归还给我们时都会留在书中。这就是按引用传递的工作方式。对类的实例所做的任何更改在函数退出后都将保持不变。
要了解更多关于值类型与引用类型的信息,请参阅本章末尾的 内存管理 部分。
创建类或结构体
我们使用相同的语法来定义类和结构体。唯一的区别是我们使用 class 关键字定义类,使用 struct 关键字定义结构体。让我们看看创建类和结构体所使用的语法:
class MyClass {
// MyClass definition
}
struct MyStruct {
// MyStruct definition
}
在前面的代码中,我们定义了一个名为 MyClass 的新类和一个名为 MyStruct 的新结构体。这实际上创建了两个新的 Swift 类型,分别名为 MyClass 和 MyStruct。当我们命名一个新类型时,我们希望使用 Swift 定义的命名约定,即名称为驼峰式,首字母大写。在类或结构体内部定义的任何方法或属性也应使用驼峰式命名,首字母小写。
空类和结构体并不那么有用,因此让我们看看我们如何向我们的类和结构体添加属性。
属性
属性将值与类或结构体关联。属性有两种类型,如下所示:
-
存储属性:它们将变量或常量值作为类或结构体实例的一部分存储。存储属性还可以有属性观察器,可以监视属性的变化,并在属性值发生变化时执行自定义操作。
-
计算属性:它们本身不存储值,但检索并可能设置其他属性。计算属性的值也可以在请求时计算。
存储属性
存储属性是作为类或结构体实例的一部分存储的变量或常量。我们可以为存储属性提供默认值。这些使用 var 关键字定义。让我们看看如何在类和结构体中使用存储属性。在以下代码中,我们将创建一个名为 MyStruct 的结构体和一个名为 MyClass 的类。结构和类都包含两个存储属性,c 和 v。存储属性 c 是一个常量,因为它使用 let 关键字定义,而 v 是一个变量,因为它使用 var 关键字定义。让我们看一下以下代码:
struct MyStruct {
let c = 5
var v = ""
}
class MyClass {
let c = 5
var v = ""
}
如示例所示,定义存储属性的语法对类和结构体都是相同的。让我们看看如何创建结构和类的实例。以下代码创建了一个名为 myStruct 的 MyStruct 结构体实例和一个名为 myClass 的 MyClass 类实例:
var myStruct = MyStruct()
var myClass = MyClass()
结构体和类之间的一个区别是,默认情况下,结构体会创建一个初始化器,允许我们在创建结构体实例时填充存储属性。因此,我们也可以这样创建 MyStruct 的实例:
var myStruct = MyStruct(v: "Hello")
在前面的示例中,初始化器用于设置变量v,而c常量将包含在struct本身中设置的数字 5。例如,如果我们没有为常量提供一个初始值,如以下示例所示,则默认初始化器也会用来设置常量:
struct MyStruct {
let c: Int
var v = ""
}
以下示例显示了新struct的初始化器将如何工作:
var myStruct = MyStruct(c: 10, v: "Hello")
这允许我们在运行时初始化类或结构体时设置值,而不是在我们的代码中硬编码常量的值。
初始化器中参数出现的顺序是我们定义它们的顺序。在之前的示例中,我们首先定义了 c 常量;因此,它是初始化器中的第一个参数。我们定义了 v 参数第二个;因此它是初始化器中的第二个参数。
要设置或读取存储属性,我们使用标准的点语法。让我们看看在 Swift 中如何设置和读取存储属性:
var x = myClass.c
myClass.v = "Howdy"
在我们继续到计算属性之前,让我们创建一个表示员工的结构体和类。我们将在这个章节中使用和扩展这些内容,以展示类和结构体是如何相似以及它们有何不同:
struct EmployeeStruct {
var firstName = ""
var lastName = ""
var salaryYear = 0.0
}
public class EmployeeClass {
var firstName = ""
var lastName = ""
var salaryYear = 0.0
}
员工结构体命名为EmployeeStruct,员工类命名为EmployeeClass。类和结构体都有三个存储属性:firstName、lastName和salaryYear。
在我们的结构体或类中,我们现在可以通过使用属性名或self关键字来访问这些属性。结构体或类的每个实例都有一个名为self的属性。这个属性指向实例本身;因此,我们可以使用它来访问实例内的属性。以下示例显示了如何在结构体或类的实例中使用self关键字来访问属性:
self.firstName = "Jon"
self.lastName = "Hoffman"
计算属性
计算属性是没有用于存储与属性相关值的后端变量的属性。计算属性的值通常在代码请求时计算。你可以将计算属性视为一个伪装成属性的函数。让我们看看我们如何定义一个只读计算属性:
var salaryWeek: Double {
get{
return self.salaryYear/52
}
}
要创建一个只读计算属性,我们首先使用var关键字定义它,就像它是一个普通的变量,然后是变量名,冒号和变量类型。接下来的是不同的;我们在声明末尾添加一个花括号,然后定义一个getter方法,当请求计算属性的值时会被调用。在示例中,getter方法将salaryYear属性的当前值除以52以获取员工的周薪。
我们可以通过移除get关键字来简化只读计算属性的定义。我们可以将salaryWeek函数重写如下:
var salaryWeek: Double {
return self.salaryYear/52
}
计算属性不仅限于只读,我们也可以写入它们。要使 salaryWeek 属性可写,我们需要添加一个 setter 方法。以下示例显示了如何添加一个 setter 方法,该方法将根据传递给 salaryWeek 属性的值设置 salaryYear 属性:
var salaryWeek: Double {
get {
return self.salaryYear/52
}
set (newSalaryWeek){
self.salaryYear = newSalaryWeek*52
}
}
我们可以通过不定义新值的名称来简化设置器的定义。在这种情况下,值将被分配给默认变量名,newValue。salaryWeek 计算属性可以重写如下:
var salaryWeek: Double {
get{
return self.salaryYear/52
}
set{
self.salaryYear = newValue*52
}
}
如前例所示,salaryWeek 计算属性可以添加到 EmployeeClass 类或 EmployeeStruct 结构体中,无需任何修改。让我们通过将 salaryWeek 属性添加到我们的 EmployeeClass 类中来看如何实现这一点:
public class EmployeeClass {
var firstName = ""
var lastName = ""
var salaryYear = 0.0
var salaryWeek: Double {
get{
return self.salaryYear/52
}
set (newSalaryWeek){
self.salaryYear = newSalaryWeek*52
}
}
}
现在,让我们看看如何将计算属性 salaryWeek 添加到 EmployeeStruct 结构体中:
struct EmployeeStruct {
var firstName = ""
var lastName = ""
var salaryYear = 0.0
var salaryWeek: Double {
get{
return self.salaryYear/52
}
set (newSalaryWeek){
self.salaryYear = newSalaryWeek*52
}
}
}
如我们所见,类和结构体的定义到目前为止是相同的,只是使用初始的类或 struct 关键字来定义它们是结构体还是类。
我们对计算属性的读写方式与对存储属性的读写方式相同。类或结构体外部代码不应知道该属性是计算属性。让我们通过创建 EmployeeStruct 结构体的一个实例来观察这个动作:
var f = EmployeeStruct(firstName: "Jon", lastName: "Hoffman", salaryYear: 39000)
print(f.salaryWeek) //prints 750.00 to the console
f.salaryWeek = 1000
print(f.salaryWeek) //prints 1000.00 to the console
print(f.salaryYear) //prints 52000.00 to the console
前面的例子首先创建了一个 EmployStruct 结构体的实例,并将 salaryYear 的值设置为 39,000。接下来,我们将 salaryWeek 属性的值打印到 console。当前这个值是 750.00。然后我们将 salaryWeek 属性设置为 1000.00,并将 salaryWeek 和 salaryYear 属性的值都打印到 console。现在 salaryWeek 和 salaryYear 属性的值分别是 1000.00 和 52000。正如我们所看到的,在这个例子中,设置 salaryWeek 或 salaryYear 属性都会改变两个属性返回的值。
计算属性对于提供相同数据的不同视图非常有用。例如,如果我们有一个表示某物长度的值,我们可以将长度存储为厘米,然后使用计算属性来计算米、毫米和千米的值。
现在,让我们看看属性观察者。
属性观察者
每当属性的值被设置时,都会调用属性观察者。我们可以将属性观察者添加到任何非懒加载的存储属性。我们还可以通过在子类中重写属性来将属性观察者添加到任何继承的存储或计算属性。我们将在本章稍后的 重写属性 部分讨论这个问题。
在 Swift 中,我们可以设置两种属性观察者——willSet 和 didSet。willSet 观察者在属性被设置之前被调用,而 didSet 观察者在属性被设置之后被调用。
关于属性观察者需要注意的一点是,当在初始化期间设置值时,它们不会被调用。让我们看看如何向我们的EmployeeClass类和EmployeeStruct结构的工资属性添加属性观察者:
var salaryYear: Double = 0.0 {
willSet(newSalary) {
print("About to set salaryYear to \(newSalary)")
}
didSet {
if salaryWeek > oldValue {
print("\(firstName) got a raise")
}
else {
print("\(firstName) did not get a raise")
}
}
}
当我们向存储属性添加属性观察者时,我们需要在属性的定义中包含要存储的值的类型。在先前的例子中,我们不需要将salaryYear属性定义为Double类型;然而,当我们添加属性观察者时,定义是必需的。
在属性定义之后,我们定义一个willSet观察者,它简单地打印出salaryYear属性将被设置的新值。我们还定义了一个didSet观察者,它将检查新值是否大于旧值;如果是,它将打印出员工得到了加薪,否则,它将打印出员工没有得到加薪。
就像计算属性中的 getter 一样,我们不需要为willSet观察者定义新值的名称。如果我们不定义名称,新值将被放入一个名为newValue的常量中。以下示例显示了如何不定义新值的名称来重写之前的willSet观察者:
willSet {
print("About to set salaryYear to \(newValue)")
}
正如我们所见,属性主要用于存储与类或结构相关的信息,而方法主要用于向类或结构添加业务逻辑。让我们看看如何向类或结构添加方法。
方法
方法是与类或结构相关联的函数。方法,就像函数一样,将封装与类或结构相关联的特定任务或功能的相关代码。让我们看看如何在类和结构中定义方法。以下代码将通过使用firstName和lastName属性来返回员工的完整姓名:
func getFullName() -> String {
return firstName + " " + lastName
}
我们定义这个方法就像定义任何函数一样。方法简单地说就是一个与特定类或结构相关联的函数,我们之前章节中学到的关于函数的所有内容都适用于方法。getFullName()函数可以直接添加到EmployeeClass类或EmployeeStruct结构中,无需任何修改。
要访问方法,我们使用与访问属性相同的点语法。以下代码显示了如何访问类和结构中的getFullName()方法:
var e = EmployeeClass()
var f = EmployeeStruct(firstName: "Jon", lastName: "Hoffman", salaryYear: 50000)
e.firstName = "Jon"
e.lastName = "Hoffman"
e.salaryYear = 50000.00
print(e.getFullName()) //Jon Hoffman is printed to the console
print(f.getFullName()) //Jon Hoffman is printed to the console
在前面的例子中,我们初始化了EmployeeClass类和EmployeeStruct结构的实例。我们用相同的信息填充结构和类,然后使用getFullName()方法将员工的完整姓名打印到控制台。在两种情况下,都会打印出Jon Hoffman到控制台。
在定义类和结构体的方法方面存在差异,我们需要在方法中更新属性值。让我们看看我们如何在 EmployeeClass 类中定义一个给员工加薪的方法:
func giveRaise(amount: Double) {
self.salaryYear += amount
}
如果我们将前面的代码添加到我们的 EmployeeClass 中,它将按预期工作,并且当我们调用该方法并传递一个金额时,员工会得到加薪。然而,如果我们尝试将此方法按原样添加到 EmployeeStruct 结构体中,我们会收到 Cannot invoke '+=' with an argument list of type '(Double, Double)' 错误。默认情况下,我们不允许在结构体的方法中更新属性值。如果我们想修改属性,我们可以通过在方法声明的 func 关键字之前添加 mutating 关键字来选择该方法进入修改行为。因此,以下代码将是定义 EmployeeStruct 结构体的 giveRaise() 方法的正确方式:
mutating func giveRase(amount: Double) {
self.salaryYear += amount
}
在前面的示例中,我们使用了 self 属性。类型的每个实例都有一个名为 self 的属性,它就是该实例本身。我们使用 self 属性来在实例本身中引用类型的当前实例,因此当我们写 self.salaryYear 时,我们请求当前实例的 salaryYear 属性的值。
可以使用 self 属性来区分具有相同名称的局部变量和实例变量。让我们看看一个示例,以说明这一点:
func compareFirstName(firstName: String) -> Bool {
return self.firstName == firstName
}
在前面的示例中,方法接受一个名为 firstName 的参数。还有一个具有此名称的属性。我们使用 self 属性来指定我们想要具有该名称的实例属性,而不是具有此名称的局部变量。
除了需要 mutating 关键字来定义更改结构体属性值的函数外,方法可以像定义和使用函数一样定义和使用。因此,我们之前章节中学到的关于函数的所有内容都可以应用到方法上。
有时候,我们希望在类或结构体首次初始化时初始化属性或执行一些业务逻辑。为此,我们将使用初始化器。
自定义初始化器
当我们初始化特定类型(类或结构体)的新实例时,会调用初始化器。初始化是准备实例以供使用的进程。初始化过程可能包括设置存储属性的初始值、验证资源,例如网络服务、文件等是否可用,或正确设置用户界面。初始化器通常用于确保类或结构体的实例在使用前得到适当的初始化。
初始化器是用于创建类型新实例的特殊方法。我们定义初始化器的方式与定义其他方法相同,但我们必须使用 init 关键字作为初始化器的名称,以告诉编译器这个方法是初始化器。在其最简单的形式中,初始化器不接受任何参数。让我们看看编写简单初始化器所使用的语法:
init() {
//Perform initialization here
}
这种格式适用于类和结构体。默认情况下,所有类和结构体都有一个空的默认初始化器,我们可以选择覆盖它。我们在使用 EmployeeClass 类和 EmployeeStruct 结构体时看到了这些默认初始化器。结构体还有一个额外的默认初始化器,我们在 EmployeeStruct 结构体中看到了,它接受每个存储属性的值并将它们初始化为这些值。让我们看看我们如何向我们的 EmployeeClass 类和 EmployeeStruct 结构体添加自定义初始化器。在下面的代码中,我们创建了三个自定义初始化器,它们将适用于 EmployeeClass 类和 EmployeeStruct 结构体:
init() {
self.firstName = ""
self.lastName = ""
self.salaryYear = 0.0
}
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
self.salaryYear = 0.0
}
init(firstName: String, lastName: String, salaryYear: Double) {
self.firstName = firstName
self.lastName = lastName
self.salaryYear = salaryYear
}
第一个初始化器 init(),当使用时,将所有存储属性设置为它们的默认值。第二个初始化器 init(firstName: String, lastName: String),当使用时,将 firstName 和 lastName 属性填充为参数的值。第三个初始化器 init(firstName: String, lastName: String, salaryYear: Double),将所有属性填充为参数的值。
在上一个示例中,我们可以看到在 Swift 中,与 Objective-C 不同,初始化器没有返回值。这意味着我们不需要为初始化器定义返回类型,或者初始化器内部不需要有返回语句。让我们看看我们如何使用这些初始化器:
var g = EmployeeClass()
var h = EmployeeStruct(firstName: "Me", lastName: "Moe")
var i = EmployeeClass(firstName: "Me", lastName: "Moe", salaryYear: 45000)
变量 g 使用 init() 初始化器创建 EmployeeClass 类的一个实例;因此,这个 EmployeeClass 实例的所有属性都包含它们的默认值。
变量 h 使用 init(firstName: String, lastName: String) 初始化器创建 EmployeeStruct 结构体的一个实例;因此,结构体的 firstName 属性被设置为 Me,lastName 属性被设置为 Moe,这两个参数被传递到初始化器中。salaryYear 属性仍然设置为默认值 0.0。
EmployeeClass 使用 init(firstName: String, lastName: String, salaryYear: Double) 初始化器创建 EmployeeClass 类的一个实例;因此,firstName 属性被设置为 Me,lastName 属性被设置为 Moe,salaryYear 被设置为 45000。
由于所有初始化器都与init关键字相关联,因此参数和参数类型被用来识别要使用哪个初始化器。因此,Swift 为所有这些参数提供了自动的外部名称。在先前的例子中,我们可以看到当我们使用具有参数的初始化器时,我们会包含参数名称。让我们来看看初始化器中内部和外部参数名称。
内部和外部参数名称
就像函数一样,与初始化器关联的参数可以有独立的内部和外部名称。与函数不同,如果我们没有为我们的参数提供外部参数名称,Swift 会自动为我们生成它们。在先前的例子中,我们没有在初始化器的定义中包含外部参数名称,所以 Swift 使用内部参数名称作为外部参数名称。
如果我们想要提供自己的参数名称,我们会在内部函数名称之前放置外部函数名称,这与我们对任何正常函数的做法完全一样。让我们看看我们如何通过重新定义EmployeeClass类中的一个初始化器来定义我们自己的外部参数名称:
init(employeeWithFirstName firstName: String, lastName lastName: String, andSalary salaryYear: Double) {
self.firstName = firstName
self.lastName = lastName
self.salaryYear = salaryYear
}
在先前的例子中,我们创建了init(employeeWithFirstName firstName: String, lastName lastName: String, andSalary salaryYear: Double)初始化器。这个初始化器将创建EmployeeClass类的一个实例,并用参数的值填充实例属性。在这个例子中,每个参数都有外部和内部属性名称。让我们看看我们如何使用这个初始化器(带有外部属性名称)来创建EmployeeClass类的一个实例:
var i = EmployeeClass(employeeWithFirstName: "Me", lastName: "Moe", andSalary: 45000)
注意我们现在正在使用我们在初始化器中定义的外部参数名称。使用外部参数名称可以帮助使我们的代码更易于阅读,并区分不同的初始化器。
那么,如果我们的初始化器失败会发生什么?例如,如果我们的类依赖于一个特定的资源,比如网络服务或当前不可用的特定文件,会怎样?这就是 failable 初始化器发挥作用的地方。
Failable 初始化器
一个 failable 初始化器是一个可能无法初始化类或结构体所需资源的初始化器,从而使实例不可用。当使用 failable 初始化器时,初始化器的结果是可选类型,包含该类型的有效实例或 nil。
可以通过在init关键字后添加一个问号(?)来使初始化器成为 failable。让我们看看我们如何创建一个不允许新员工以每年低于$20,000 的薪水进行初始化的 failable 初始化器:
init?(firstName: String, lastName: String, salaryYear: Double) {
self.firstName = firstName
self.lastName = lastName
self.salaryYear = salaryYear
if self.salaryYear < 20000 {
return nil
}
}
在前面的示例中,我们没有在初始化器中包含 return 语句,因为 Swift 不需要返回初始化的实例;然而,在可失败初始化器中,如果初始化失败,我们将返回 nil。如果初始化器成功初始化了实例,我们不需要返回任何内容。因此,在我们的示例中,如果传入的年薪低于 $20,000 一年,我们返回 nil,表示初始化失败,否则,将不返回任何内容。让我们看看如何使用可失败初始化器来创建类或结构的实例:
if let f = EmployeeClass(firstName: "Jon", lastName: "Hoffman",
salaryYear: 29000) {
print(f.getFullName())
} else {
print("Failed to initialize")
}
在前面的示例中,我们使用年薪超过 $20,000 初始化 EmployeeClass 类的实例;因此,实例被正确初始化,并在控制台打印出 Jon Hoffman 的全名。现在让我们尝试使用年薪低于 $20,000 初始化 EmployeeClass 类的实例,以查看它如何失败:
if let f = EmployeeClass(firstName: "Jon", lastName: "Hoffman", salaryYear: 19000) {
print(f.getFullName())
print(f.compareFirstName("Jon"))
} else {
print("Failed to initialize")
}
在示例中,我们尝试为我们的员工初始化的年薪低于 $20,000;因此,初始化失败,并在控制台打印出Failed to initialize消息。
有时候,我们希望限制对代码中某些部分的访问。这使我们能够隐藏实现细节,仅暴露我们希望暴露的接口。此功能通过命名访问控制来处理。
访问控制允许我们限制对代码中某些部分的访问和可见性。这使我们能够隐藏实现细节,仅暴露外部代码希望访问的接口。我们可以为类和结构体分配特定的访问级别。我们还可以为属于我们的类和结构体的属性、方法和初始化器分配特定的访问级别。
在 Swift 中,有三个访问级别:
-
Public:这是最可见的访问控制级别。它允许我们在任何我们想要导入模块的地方使用属性、方法、类等。基本上,任何具有公共访问控制级别的属性、方法、类等都可以使用。此级别主要用于框架,以暴露框架的公共 API。
-
Internal:这是默认的访问级别。此访问级别允许我们在定义源以及源所在的模块(应用程序或框架)中使用属性、方法、类等。如果在此级别中使用框架,它允许框架的其他部分使用属性、方法、类等,但框架外部的代码将无法访问它。
-
Private:这是最不可见的访问控制级别。它只允许我们在定义它的源文件中使用属性、方法、类等。
如果我们正在编写将在单个应用程序内自包含的代码,并且不需要将其提供给应用程序外部,那么我们可以很大程度上忽略访问控制。内部默认的访问级别已经符合这一要求。然而,我们可能仍然想隐藏实现的一部分,这可以通过将访问级别设置为私有来实现,但这应该是一个例外而不是规则。
当我们开发框架时,访问控制变得非常有用。我们需要将面向公众的接口标记为公共,以便其他模块,如导入框架的应用程序,可以使用它们。然后,我们将使用内部和私有访问控制级别来标记我们希望在框架和源文件内部使用的接口。
要定义访问级别,我们在实体的定义之前放置级别的名称。以下代码显示了如何向几个实体添加访问级别的示例:
private struct EmployeeStruct {}
public class EmployeeClass {}
internal class EmployeeClass2 {}
public var firstName = "Jon"
internal var lastName = "Hoffman"
private var salaryYear = 0.0
public func getFullName() -> String {}
private func giveRaise(amount: Double) {}
在访问控制方面存在一些限制,但这些限制是为了确保 Swift 中的访问级别遵循一个简单的指导原则——没有实体可以被定义为低于(更限制性)访问级别的另一个实体的术语。这意味着当我们依赖一个具有较低(更限制性)访问级别的实体时,我们不能将较高的(较少限制性)访问级别分配给该实体。
如以下示例所示:
-
如果其中一个参数或返回类型具有私有访问级别,我们不能将方法标记为公共,因为外部代码无法访问私有类型。
-
如果类或结构体的访问级别为私有,我们不能将方法或属性的访问级别设置为公共,因为外部代码无法在类为私有时访问构造函数。
继承
继承的概念是面向对象开发的基本概念。继承允许一个类被定义为具有一组特定的特征,然后其他类可以从该类派生。派生类继承了它所继承的类的所有特征(除非派生类覆盖了这些特征),然后通常还会添加它自己的额外特征。
通过继承,我们可以创建所谓的类层次结构。在类层次结构中,层次结构顶部的类被称为基类,派生类被称为子类。我们不仅限于仅从基类创建子类;我们还可以从其他子类创建子类。子类所继承的类被称为父类或超类。在 Swift 中,一个类只能有一个父类,这被称为单一继承。
注意
继承是区分类和结构体的基本差异之一。类可以从父类或超类派生,但结构体不能。
子类可以调用和访问其超类的属性、方法和下标。它们还可以覆盖其超类的属性、方法和下标。子类可以向从超类继承的属性添加属性观察者,以便在属性值发生变化时收到通知。让我们看看一个示例,说明 Swift 中继承是如何工作的。
我们将首先定义一个名为 Plant 的基类。Plant 类将有两个属性,height 和 age。它还将有一个方法,growHeight()。height 属性将代表植物的高度,age 属性将代表植物的年龄,growHeight() 方法将用于增加植物的高度。以下是定义 Plant 类的方法:
class Plant {
var height = 0.0
var age = 0
func growHeight(inches: Double) {
self.height += inches;
}
}
现在我们有了 Plant 基类,让我们看看我们如何定义它的子类。我们将把这个子类命名为 Tree。Tree 类将继承 Plant 类的 age 和 height 属性,并添加一个额外的属性名为 limbs。它还将继承 Plant 类的 growHeight() 方法,并添加两个额外的方法:limbGrow(),用于生长新的枝条,以及 limbFall(),用于树枝从树上掉落。让我们看看以下代码:
class Tree: Plant {
private var limbs = 0
func limbGrow() {
self.limbs++
}
func limbFall() {
self.limbs--
}
}
我们通过在类定义的末尾添加一个冒号和超类的名称来表示一个类有一个超类。在 Tree 示例中,我们表明 Tree 类有一个名为 Plant 的超类。
现在,让我们看看我们如何使用从 Plant 类继承来的 age 和 height 属性的 Tree 类:
var tree = Tree()
tree.age = 5
tree.height = 4
tree.limbGrow()
tree.limbGrow()
以下面的示例开始,我们创建了一个 Tree 类的实例。然后我们将 age 和 height 属性分别设置为 5 和 4,并通过调用 limbGrow() 方法两次来给树添加两个枝条。
我们现在有一个名为 Plant 的基类,它有一个名为 Tree 的子类。这意味着 Tree 的超类(或父类)是 Plant 类。这也意味着 Plant 的一个子类(或子类)被命名为 Tree。然而,世界上有各种各样的树木。让我们从 Tree 类创建两个子类。这两个子类将是 PineTree 类和 OakTree 类:
class PineTree: Tree {
var needles = 0
}
class OakTree: Tree {
var leaves = 0
}
现在的类层次结构看起来是这样的:

在 Swift 中,一个类可以有多个子类;然而,一个类只能有一个超类。有时子类需要为其从超类继承来的方法或属性提供自己的实现。这被称为覆盖。
覆盖方法和属性
要重写一个方法、属性或索引,我们需要在定义前加上override关键字。这告诉编译器我们打算重写超类中的某个东西,并且我们没有错误地创建了重复的定义。override关键字会提示 Swift 编译器验证超类(或其父类之一)中是否存在可以重写的匹配声明。如果它在一个超类中找不到匹配的声明,将会抛出错误。
重写方法
让我们看看如何重写一个方法。我们将首先向Plant类添加一个getDetails()方法,然后将在子类中重写它。以下代码显示了添加了getDetails()方法后的新Plant类的外观:
class Plant {
var height = 0.0
var age = 0
func growHeight(inches: Double) {
self.height += inches;
}
func getDetails() -> String {
return "Plant Details"
}
}
现在,让我们看看我们如何在Tree类中重写getDetails()方法:
class Tree: Plant {
private var limbs = 0
func limbGrow() {
self.limbs++
}
func limbFall() {
self.limbs--
}
override func getDetails() -> String {
return "Tree Details"
}
}
这里需要注意的是,我们在Plant类中不使用override关键字,因为它是最先实现此方法的类;然而,我们在Tree类中包含它,因为我们正在重写从Plant类继承的getDetails()方法。现在,让我们看看如果我们从一个Plant和Tree类的实例中调用getDetails()方法会发生什么:
var plant = Plant()
var tree = Tree()
print("Plant: \(plant.getDetails())")
print("Tree: \(tree.getDetails())")
之前的例子会在控制台打印以下两行:
Plant: Plant Details
Tree: Tree Details
如我们所见,Tree子类中的getDetails()方法重写了其父类Plant中的getDetails()方法。
在Tree类内部,我们仍然可以通过使用super前缀来调用其超类(在这种情况下是Plant类)的getDetails()方法(或任何重写的方法、属性或索引)。让我们看看如何从一个Tree类的实例中调用Plant类的getDetails()方法。我们将首先将Plant类中的getDetails()方法替换为以下方法,该方法将生成包含height和age属性值的字符串。让我们看一下以下代码:
func getDetails() -> String {
return "Height: \(height) age: \(age)"
}
在前面的代码中,我们将getDetails()方法更改为返回一个包含植物height和age的字符串。现在让我们用以下方法替换Tree类的getDetails()方法:
override func getDetails() -> String {
var details = super.getDetails()
return "\(details) limbs: \(limbs)"
}
在前面的例子中,我们首先调用超类(在这种情况下是Plant类)的getDetails()方法,以获取包含树木的height和age的字符串。然后我们构建一个新的字符串对象,包含从超类中调用getDetails()方法的结果,将其添加到其中,然后返回它。让我们看看如果我们调用Tree类的getDetails()方法会发生什么:
var tree = Tree()
tree.age = 5
tree.height = 4
tree.limbGrow()
tree.limbGrow()
print(tree.getDetails())
如果我们运行前面的代码,控制台将打印以下行:
Height: 4.0 age: 5 limbs: 2
如我们所见,返回的字符串包含了来自Plant类的height和age信息以及来自Tree类的树枝信息。
我们还可以链式调用重写的方法。让我们看看如果我们向OakTree类添加以下方法会发生什么:
override func getDetails() -> String {
let details = super.getDetails()
return "\(details) Leaves: \(leaves)"
}
当我们调用 OakTree 类实例的 getDetails() 方法时,它会调用其超类(Tree 类)的 getDetails() 方法。Tree 类的 getDetails() 方法也会调用其超类(Plant 类)的 getDetails() 方法。Tree 类的 getDetails() 方法最终将创建一个包含来自 Plant 类的 height 和 age、来自 Tree 类的 limbs 以及来自 OakTree 类的 leaves 的字符串对象。让我们看看这个例子:
var tree = OakTree()
tree.age = 5
tree.height = 4
tree.leaves = 50
tree.limbGrow()
tree.limbGrow()
print(tree.getDetails())
如果我们运行前面的代码,我们会在控制台看到以下行打印出来:
Height: 4.0 age: 5 limbs: 2 Leaves: 50
重写属性
我们可以提供自定义的 getter 和 setter 来重写任何继承的属性。当我们重写一个属性时,我们必须提供我们正在重写的属性名称和类型,以便编译器可以验证类层次结构中的某个类是否有一个匹配的重写属性。虽然重写属性不如重写方法常见,但当我们需要时了解如何做是好的。
让我们看看我们如何通过向我们的 Plant 类添加以下方法来重写属性:
var description: String {
get {
return "Base class is Plant."
}
}
description 属性是一个基本的只读属性。该属性返回字符串 Base class is Plant.。现在让我们通过向 Tree 类添加以下属性来重写这个属性:
override var description: String {
return "\(super.description) I am a Tree class."
}
当我们重写一个属性时,我们使用与重写方法时相同的 override 关键字。override 关键字告诉编译器我们想要重写一个属性,因此编译器可以验证类层次结构中的另一个类是否包含一个匹配的重写属性。然后我们像实现任何其他属性一样实现该属性。调用树的 description 属性将返回字符串 Base class is Plant. I am a Tree class.。
有时候,我们可能想要防止子类重写属性和方法。也有时候,我们可能想要防止整个类被继承。让我们看看我们如何做到这一点。
防止重写
为了防止重写或子类化,我们使用 final 关键字。要使用 final 关键字,我们将其添加到项的定义之前。例如,有 final func、final var 和 final class。
任何尝试重写标记为 final 的项都会抛出编译时错误。
协议
有时候,我们可能想要描述一个类的实现(方法、属性和其他要求),而不实际提供实现。为此,我们会使用协议。
协议定义了一个类或结构的方法、属性和其他要求的蓝图。然后,类或结构可以提供一个符合这些要求的实现。提供实现的类或结构被称为符合该协议。
协议语法
定义协议的语法与我们定义类或结构体非常相似。以下示例显示了定义协议时使用的语法:
protocol MyProtocol {
//protocol definition here
}
我们通过在类或结构体名称之后放置协议名称,并用冒号分隔来声明一个类或结构体符合特定的协议。以下是一个示例,说明我们如何声明一个类符合 MyProtocol 协议:
class myClass: MyProtocol {
//class implementation here
}
一个类或结构体可以符合多个协议。我们会通过逗号分隔来列出类或结构体符合的协议。以下示例显示了如何声明我们的类符合多个协议:
class MyClass: MyProtocol, AnotherProtocol, ThirdProtocol {
// class implementation here
}
当我们需要一个类继承自一个超类并实现一个协议时,我们会首先列出超类,然后是协议。以下示例说明了这一点:
Class MyClass: MySuperClass, MyProtocol, MyProtocol2 {
// Class implementation here
}
属性要求
协议可以要求符合该协议的类或结构提供具有指定名称和类型的某些属性。协议没有说明属性应该是存储属性还是计算属性,因为实现细节留给了符合协议的类或结构。
在协议中定义属性时,我们必须使用 get 和 set 关键字来指定属性是只读的还是可读写的。让我们通过创建一个 FullName 协议来看看如何在协议中定义属性:
protocol FullName {
var firstName: String {get set}
var lastName: String {get set}
}
FullName 协议定义了两个属性,任何符合该协议的类或结构都必须实现这些属性。这些属性是 firstName 和 lastName 属性。在 FullName 协议中,这两个属性都是可读写属性。如果我们想指定属性是只读的,我们可以只使用 get 关键字来定义它,如下所示:
var readOnly: String {get}
让我们看看我们如何创建一个符合此协议的 Scientist 类:
class Scientist: FullName {
var firstName = ""
var lastName = ""
}
如果我们忘记包含 firstName 或 lastName 属性中的任何一个,我们会收到 Scientist does not conform to protocol 'FullName' 错误信息。我们还需要确保属性的类型相同。例如,如果我们将 Scientist 类中的 lastName 定义更改为 var lastName = 42,我们也会收到 Scientist does not conform to protocol 'FullName' 错误信息,因为协议指定我们必须有一个字符串类型的 lastName 属性。
方法要求
协议可以要求符合该协议的类或结构提供某些方法。我们可以在协议中定义一个方法,就像在普通类或结构体中定义一样,只是不需要花括号或方法体。让我们向我们的 FullName 协议和 Scientist 类添加一个 getFullName() 方法。
以下示例显示了添加了 getFullName() 方法的 FullName 协议的外观:
protocol FullName {
var firstName: String {get set}
var lastName: String {get set}
func getFullName() -> String
}
现在,我们需要在我们的 Scientist 类中添加一个 getFullName() 方法,以便它能够正确符合 FullName 协议:
class Scientist: FullName {
var firstName = ""
var lastName = ""
var field = ""
func getFullName() -> String {
return "\(firstName) \(lastName) studies \(field)"
}
}
结构体可以像类一样符合 Swift 协议。以下示例展示了我们如何创建一个符合 FullName 协议的 FootballPlayer 结构体:
struct FootballPlayer: FullName {
var firstName = ""
var lastName = ""
var number = 0
func getFullName() -> String {
return "\(firstName) \(lastName) has the number \(number)"
}
}
当一个类或结构体符合 Swift 协议时,我们可以确信它已经实现了必需的属性和方法。这在我们想要确保在多个类中实现某些属性或方法时非常有用,如我们前面的例子所示。
协议在我们要将代码与特定的类或结构体解耦时也非常有用。以下代码展示了我们如何使用 FullName 协议、Scientist 类和已经构建的 FootballPlayer 结构体来解耦我们的代码:
var scientist = Scientist()
scientist.firstName = "Kara"
scientist.lastName = "Hoffman"
scientist.field = "Physics"
var player = FootballPlayer();
player.firstName = "Dan"
player.lastName = "Marino"
player.number = 13
var person: FullName
person = scientist
print(person.getFullName())
person = player
print(player.getFullName())
在前面的代码中,我们首先创建了一个 Scientist 类的实例和一个 FootballPlayer 结构体的实例。然后我们创建了一个 person 变量,其类型为 FullName(协议),并将其设置为刚刚创建的 scientist 实例。然后我们调用 getFullName() 方法来获取我们的描述。这将打印出 Kara Hoffman 研究物理学 的消息到控制台。
接着我们将 person 变量设置为 player 实例,并再次调用 getFullName() 方法。这将打印出 Dan Marino 穿着号码 13 的消息到控制台。
如我们所见,person 变量并不关心实际的实现类或结构体是什么。由于我们将 person 变量定义为 FullName 类型,因此我们可以将 person 变量设置为任何符合 FullName 协议的类或结构体的实例。
可选要求
有时候我们希望协议定义可选要求,也就是说,不是必须实现的方法或属性。要使用可选要求,我们需要首先使用 @objc 属性标记协议。要将属性或方法标记为可选,我们使用 optional 关键字。
注意
在使用 @objc 属性时,有一点非常重要需要注意,那就是只有类可以采用标记了 @objc 属性的协议;结构体不能采用这些协议。
让我们看看如何使用 optional 关键字来定义可选属性和方法:
@objc protocol Phone {
var phoneNumber: String {get set}
optional var emailAddress: String {get set}
func dialNumber()
optional func getEmail()
}
在我们刚刚创建的 Phone 协议中,我们定义了一个必需的属性名为 phoneNumber,以及一个可选属性名为 emailAddress。此外,在 Phone 协议中,我们还定义了一个必需的函数名为 dialNumber() 和一个可选函数名为 getEmail()。这意味着采用 Phone 协议的类必须提供一个 phoneNumber 属性和一个 dialNumber() 方法。采用 Phone 协议的类也可以选择性地提供一个 emailAddress 属性和一个 getEmail() 方法,但这不是必需的。
Swift 2 为 Swift 添加了协议扩展功能。这是 Swift 语言中一个非常激动人心且重要的特性。要了解有关协议扩展的内容,请参阅第六章,使用协议和协议扩展。
有时候我们需要向现有的类或结构体添加额外的功能。为此,我们使用扩展。
扩展
使用扩展,我们可以添加新的属性、方法、构造函数和下标,或者使现有的类或结构体符合一个协议。需要注意的是,扩展不能覆盖现有的功能。
要定义一个扩展,我们使用 extension 关键字,后跟我们要扩展的类型。以下示例展示了我们如何创建一个扩展来扩展字符串类:
extension String {
//add new functionality here
}
让我们通过向 Swift 的标准字符串类添加一个 reverse() 方法和 firstLetter 属性来了解扩展是如何工作的:
extension String {
var firstLetter: Character {
get {
return self.characters.first
}
}
func reverse() -> String {
var reverse = ""
for letter in self.characters {
reverse = "\(letter)" + reverse
}
return reverse
}
}
当我们扩展一个现有的类或结构体时,我们定义属性、方法、构造函数、下标和协议的方式与我们在标准类或结构体中通常定义它们的方式完全相同。在字符串扩展示例中,我们看到我们定义 reverse() 方法和 firstLetter 属性的方式与我们在正常类中定义它们的方式完全相同。
扩展对于向外部框架中的类和结构体添加额外功能非常有用,甚至对于苹果的框架也是如此,如示例所示。我们更倾向于使用扩展来向外部框架中的类添加额外功能,而不是子类化,因为这允许我们在整个代码中使用框架提供的类。
内存管理
如我在本章开头提到的,结构体是值类型,而类是引用类型。这意味着当我们在我们应用程序中传递结构体的实例时,例如方法的一个参数,我们会在内存中创建结构体的一个新实例。这个结构体的新实例仅在结构体创建的作用域内有效。一旦结构体超出作用域,结构体的这个新实例就会被销毁,并且内存会被释放。这使得结构体的内存管理变得相当简单,并且相对无痛。
与此相反,类是引用类型。这意味着当类首次创建实例时,我们只为其分配一次内存。当我们想要在我们的应用程序中将类的实例传递给函数作为参数,或者将其分配给变量时,我们实际上传递的是实例在内存中存储位置的引用。由于类的实例可能在多个作用域中被引用(与结构体不同),它不能被自动销毁,如果它在另一个作用域中被引用,当它超出作用域时,内存不会被释放。因此,Swift 需要某种形式的内存管理来跟踪和释放当类不再需要时实例使用的内存。Swift 使用自动引用计数(ARC)来跟踪和管理内存使用。
在 ARC 中,大部分情况下,Swift 的内存管理很简单。ARC 会自动跟踪类的实例引用,当一个实例不再需要时(没有引用指向它),ARC 会自动销毁该实例并释放内存。有一些情况下,ARC 需要额外的信息来正确管理内存。在我们查看 ARC 需要帮助的实例之前,让我们看看内存管理和 ARC 是如何工作的。
引用类型与值类型
让我们通过一个示例来了解如何将引用类型(类的实例)和值类型(结构体或变量的实例)传递给函数。我们将首先定义一个新的类MyClass和一个新的结构体MyStruct。MyClass类和MyStruct结构体各自包含一个名为name的属性:
class MyClass {
var name = ""
}
struct MyStruct {
var name = ""
}
现在,我们将创建一个函数,该函数将接受一个MyClass类的实例和一个MyStruct结构体的实例作为参数。在函数内部,我们将改变MyClass类和MyStruct结构体的name属性的值。然后,通过在函数退出后检查该属性的值,我们将能够看到类和结构体的实例是如何传递给函数的。以下是showPass()函数的代码:
func showPass(myc: MyClass, var mys: MyStruct) {
print("Received Class: \(myc.name) Struct: \(mys.name)")
myc.name = "Set in function - class"
mys.name = "Set in function - struct"
print("Set Class: \(myc.name) Struct: \(mys.name)")
}
在showPass()函数中,我们将MyClass和MyStruct实例的命名属性值打印到控制台。然后我们改变命名属性的值,并将这些值再次打印到控制台。这将显示函数开始时的属性值以及属性更改后的值(在函数退出之前)。
现在,为了了解引用类型和值类型是如何传递给函数的,我们将创建MyClass类和MyStruct结构体的实例,设置name属性的值,并将这些实例传递给showPass()函数。然后,该函数将更改name属性的值,并返回控制权给调用它的代码。最后,我们将在showPass()函数退出后检查name属性的值,以查看它们是否保持了原始值或函数中设置的值。以下是执行此操作的代码:
var mci = MyClass()
mci.name = "set in main - class"
var msi = MyStruct()
msi.name = "set in main - struct"
print("Main Class: \(mci.name) Struct: \(msi.name)")
showPass(mci, msi)
print("Main Class: \(mci.name) Struct: \(msi.name)")
如果我们运行此代码,我们将看到以下输出:
Received Class: set in main - class Struct: set in main - struct
Set Class: Set in function - class Struct: Set in function - struct
Main Class: Set in function - class Struct: set in main – struct
如我们从输出中可以看到,showPass()函数接收了类和结构体的实例,其name属性分别设置为在 main 中设置 - class和在 main 中设置 - struct。接下来,在函数退出之前,我们看到类的name属性被设置为在函数中设置 - class,而结构的name属性被设置为在函数中设置 - struct。最后,当函数退出,我们回到代码的主要部分时,我们看到类的name属性被设置为在函数中设置 - class,这是在showPass()函数中设置的。然而,结构的name属性具有在调用函数之前设置的原始值,即在 main 中设置 - struct。
这个例子说明,当我们向函数传递引用类型(类的实例)时,我们传递的是原始类的引用,这意味着我们在函数退出时保留所做的任何更改。当我们向函数传递值类型(结构体或变量的实例)时,我们传递的是值(实例的副本),这意味着我们对局部副本所做的任何更改都会丢失,一旦函数退出。
自动引用计数(ARC)的工作原理
每次我们创建一个新的类实例时,ARC 都会分配存储该类所需的内存。这确保了有足够的内存来存储与该类实例相关的信息,并且锁定内存,以防止任何东西覆盖它。当类的实例不再需要时,ARC 将释放为该类分配的内存,以便它可以用于其他目的。这确保了我们不会占用不再需要的内存。
如果 ARC 释放了我们仍然需要的类的实例的内存,将无法从内存中检索类信息。如果我们尝试在内存释放后访问类的实例,应用程序可能会崩溃。为了确保不会释放我们仍然需要的类的实例的内存,ARC 会计算实例被引用的次数(有多少活跃的属性、变量或常量指向类的实例)。一旦类的实例的引用计数为零(没有东西引用该实例),内存就会被释放。
所有的前一个示例在 Playground 中都能正常运行,下面的示例将无法运行。当我们运行 Playground 中的示例代码时,ARC 不会释放我们创建的对象;这是设计上的考虑,以便我们可以看到应用程序的运行情况以及每个步骤中对象的状态。因此,我们需要将这些样本作为 iOS 或 OS X 项目来运行。让我们看看 ARC 是如何工作的一个例子。
我们首先使用以下代码创建一个MyClass类:
class MyClass {
var name = ""
init(name: String) {
self.name = name
print("Initializing class with name \(self.name)")
}
deinit {
print("Releasing class with name \(self.name)")
}
}
这个类与我们之前的MyClass类非常相似,除了我们添加了一个析构器,它在类的实例被销毁并从内存中移除之前被调用。这个析构器会在控制台打印一条消息,让我们知道类的实例即将被移除。
现在,让我们看看展示 ARC 如何创建和销毁类实例的代码:
var class1ref1: MyClass? = MyClass(name: "One")
var class2ref1: MyClass? = MyClass(name: "Two")
var class2ref2: MyClass? = class2ref1
print("Setting class1ref1 to nil")
class1ref1 = nil
print("Setting class2ref1 to nil")
class2ref1 = nil
print("Setting class2ref2 to nil")
class2ref2 = nil
在示例中,我们首先创建了两个MyClass类的实例,分别命名为class1ref1(代表类 1 引用 1)和class2ref1(代表类 2 引用 1)。然后,我们为class2ref1创建了一个名为class2ref2的第二个引用。现在,为了了解 ARC 的工作原理,我们需要开始将引用设置为 nil。我们首先将class1ref1设置为nil。由于只有一个引用指向class1ref1,因此将调用析构器。一旦析构器完成其任务,在我们的例子中,它会在控制台打印一条消息,让我们知道类的实例已被销毁,内存已被释放。
然后,我们将class2ref1设置为 nil,但还有一个指向这个类的第二个引用(class2ref2),这阻止了 ARC 销毁实例,因此不会调用析构器。最后,我们将class2ref2设置为 nil,这允许 ARC 销毁这个MyClass类的实例。
如果我们运行这段代码,我们将看到以下输出,它说明了 ARC 的工作原理:
Initializing class with name One
Initializing class with name Two
Setting class1ref1 to nil
Releasing class with name One
Setting class2ref1 to nil
Setting class2ref2 to nil
Releasing class with name Two
从示例中可以看出,ARC 处理内存管理得非常好。然而,编写防止 ARC 正常工作的代码是可能的。
强引用循环
强引用循环是指两个类的实例相互持有强引用,阻止 ARC 释放任一实例。通过示例来理解强引用循环要容易得多,所以让我们创建一个。再次强调,我们无法在这个例子中使用 Playground,因此我们需要创建一个 Xcode 项目。在项目中,我们首先创建两个名为MyClass1和MyClass2的类,代码如下:
class MyClass1 {
var name = ""
var class2: MyClass2?
init(name: String) {
self.name = name
print("Initializing class with name \(self.name)")
}
deinit {
print("Releaseing class with name \(self.name)")
}
}
class MyClass2 {
var name = ""
var class1: MyClass1?
init(name: String) {
self.name = name
print("Initializing class2 with name \(self.name)")
}
deinit {
print("Releaseing class2 with name \(self.name)")
}
}
如我们从代码中可以看到,MyClass1 包含了 MyClass2 的一个实例;因此,直到 MyClass1 被销毁,MyClass2 的实例都不能被释放。我们也可以从代码中看到,MyClass2 包含了 MyClass1 的一个实例;因此,直到 MyClass2 被销毁,MyClass1 的实例都不能被释放。这创建了一个依赖循环,其中两个实例都无法被销毁,直到另一个被销毁。让我们通过运行以下代码来查看这是如何工作的:
var class1: MyClass1? = MyClass1(name: "Class1")
var class2: MyClass2? = MyClass2(name: "Class2")
//class1 and class2 each have a reference count of 1
.
class1?.class2 = class2
//Class2 now has a reference count of 2
class2?.class1 = class1
//class1 now has a reference count of 2
.
print("Setting classes to nil")
class2 = nil
//class2 now has a reference count of 1, not destroyed
class1 = nil
//class1 now has a reference count of 1, not destroyed
如我们从示例中的注释中可以看到,每个实例的引用计数器从未达到零;因此,自动引用计数(ARC)无法销毁实例,从而创建了内存泄漏。内存泄漏是指应用程序继续使用内存但未正确释放它。这可能导致应用程序最终崩溃。
要解决强引用循环,我们需要防止其中一个类对另一个类的实例保持强引用,从而允许 ARC 销毁它们。Swift 通过让我们定义属性为弱引用或无主引用提供了两种方法来做这件事。
弱引用和无主引用之间的区别是,弱引用所引用的实例可以是 nil,而无主引用所引用的实例不能是 nil。这意味着当我们使用弱引用时,属性必须是可选属性,因为它可以是 nil。让我们看看如何使用无主引用和弱引用来解决强引用循环。让我们首先看看无主引用。
我们首先创建另外两个类,MyClass3 和 MyClass4:
class MyClass3 {
var name = ""
unowned let class4: MyClass4
init(name: String, class4: MyClass4) {
self.name = name
self.class4 = class4
print("Initializing class3 with name \(self.name)")
}
deinit {
print("Releasing class3 with name \(self.name)")
}
}
class MyClass4{
var name = ""
var class3: MyClass3?
init(name: String) {
self.name = name
print("Initializing class4 with name \(self.name)")
}
deinit {
print("Releasing class4 with name \(self.name)")
}
}
MyClass4 类在先前的示例中看起来与 MyClass1 和 MyClass2 类非常相似。这里的不同之处在于 MyClass3 类。在 MyClass3 类中,我们将 class4 属性设置为 unowned,这意味着它不能是 nil,并且它不会保持对它所引用的 MyClass4 实例的强引用。由于 class4 属性不能是 nil,我们还需要在类初始化时设置它。
现在让我们看看如何使用以下代码初始化和销毁这些类的实例:
var class4 = MyClass4(name: "Class4")
var class3: MyClass3? = MyClass3(name: "class3", class4: class4)
class4.class3 = class3
print("Classes going out of scope")
在前面的代码中,我们创建了一个 MyClass4 类的实例,然后使用该实例创建了一个 MyClass3 类的实例。然后我们将 MyClass4 实例的 class3 属性设置为刚刚创建的 MyClass3 实例。这再次在两个类之间创建了一个依赖循环,但这次 MyClass3 实例并没有对 MyClass4 实例保持强引用,允许 ARC 在不再需要时释放这两个实例。
如果我们运行此代码,我们将看到以下输出,显示 MyClass3 和 MyClass4 的实例都被释放,内存也被释放了:
Initializing class4 with name Class4
Initializing class3 with name class3
Classes going out of scope.
Releasing class4 with name Class4
Releasing class3 with name class3
现在让我们看看如何使用弱引用来防止强引用循环。我们首先创建两个新的类:
class MyClass5 {
var name = ""
var class6: MyClass6?
init(name: String) {
self.name = name
print("Initializing class5 with name \(self.name)")
}
deinit {
print("Releasing class5 with name \(self.name)")
}
}
class MyClass6 {
var name = ""
weak var class5: MyClass5?
init(name: String) {
self.name = name
print("Initializing class6 with name \(self.name)")
}
deinit {
print("Releasing class6 with name \(self.name)")
}
}
MyClass5和MyClass6类与我们之前创建的MyClass1和MyClass2类非常相似,用以展示强引用循环的工作原理。最大的不同之处在于我们在MyClass6类中定义了class5属性为一个弱引用。
现在,让我们看看如何使用以下代码初始化和销毁这些类的实例:
var class5: MyClass5? = MyClass5(name: "Class5")
var class6: MyClass6? = MyClass6(name: "Class6")
class5?.class6 = class6
class6?.class5 = class5
print("Classes going out of scope ")
在前面的代码中,我们创建了MyClass5和MyClass6类的实例,并将这些类的属性设置为指向另一个类的实例。再次,这创建了一个依赖循环,但由于我们将MyClass6类的class5属性设置为弱引用,它不会创建强引用,从而允许两个实例都被释放。
如果我们运行代码,我们将看到以下输出,显示MyClass5和MyClass6实例已被释放,内存已被释放:
Initializing class5 with name Class5
Initializing class6 with name Class6
Classes going out of scope.
Releasing class5 with name Class5
Releasing class6 with name Class6
建议我们避免创建循环依赖,正如本节所示,但有时我们确实需要它们。对于那些时候,请记住,自动引用计数(ARC)确实需要一些帮助来释放它们。
摘要
随着本章的结束,我们完成了对 Swift 编程语言的介绍。到目前为止,我们对 Swift 语言已经有了足够的了解,可以开始编写自己的应用程序;然而,还有很多东西需要学习。
在接下来的章节中,我们将更深入地探讨一些我们已经讨论过的概念,例如可选值和下标。我们还将展示如何使用 Swift 执行常见任务,例如解析常见文件格式和处理并发。最后,我们还将有一些章节帮助我们编写更好的代码,例如一个 Swift 风格指南的示例,以及一个关于设计模式的章节。
第六章:使用协议和协议扩展
当我在观看 2015 年 WWDC 关于协议扩展和面向协议编程的演讲时,我必须承认我非常怀疑。我从事面向对象编程已经很长时间了,以至于我不确定这种新的编程范式是否能够解决苹果所声称的所有问题。既然我不是那种让怀疑阻碍尝试新事物的人,我就设置了一个新的项目,这个项目与我现在正在工作的项目相似,但我使用了苹果面向协议编程的建议来编写代码,并在代码中广泛使用了协议扩展。我可以诚实地说我被这个新项目与原始项目相比的整洁程度所震惊。我相信协议扩展将是那些定义性特征之一,将一种编程语言与其它编程语言区分开来。我也相信许多主要语言很快就会拥有类似的功能。
在本章中,你将学习:
-
协议作为类型的使用
-
如何在 Swift 中使用协议实现多态
-
如何使用协议扩展
-
为什么我们想要使用协议扩展
协议扩展是苹果新面向协议编程范式的骨架,并且可以说是 Swift 编程语言最重要的补充之一。有了协议扩展,我们能够为符合协议的任何类型提供方法和属性实现。为了真正理解协议和协议扩展有多有用,让我们更好地理解协议。
注意
虽然class、struct和enum都可以在 Swift 中符合协议,但在这个章节中,我们将专注于class和struct。当我们需要表示有限数量的情况时,我们会使用enum,虽然有一些有效的用例,其中枚举会符合协议,但在我个人的经验中,这种情况非常罕见。只需记住,在任何我们提到class或struct的地方,我们也可以使用enum。
让我们从了解它们在 Swift 中是如何作为完整的类型开始探索协议。
协议作为类型
尽管在协议中没有实现任何功能,但在 Swift 编程语言中,它们仍然被视为一个完整的类型,可以像任何其他类型一样使用。这意味着我们可以将协议用作函数中的参数类型或返回类型。我们还可以将它们用作变量、常量和集合的类型。让我们看看一些例子。对于这些例子,我们将使用PersonProtocol协议:
protocol PersonProtocol {
var firstName: String {get set}
var lastName: String {get set}
var birthDate: NSDate {get set}
var profession: String {get}
init (firstName: String, lastName: String, birthDate: NSDate)
}
在这个第一个例子中,我们将看到我们如何将协议用作函数、方法或初始化器中的参数类型或返回类型:
func updatePerson(person: PersonProtocol) -> PersonProtocol {
// Code to update person goes here
return person
}
在这个例子中,updatePerson()函数接受一个PersonProtocol协议类型的参数,然后返回一个PersonProtocol协议类型的值。现在让我们看看我们如何可以将协议用作常量、变量或属性的类型:
var myPerson: PersonProtocol
在这个例子中,我们创建了一个名为myPerson的PersonProtocol协议类型的变量。我们还可以将协议用作集合(如数组、字典或集合)中的项目类型来存储:
var people: [PersonProtocol] = []
在这个最后的例子中,我们创建了一个PersonProtocol协议类型的数组。从这三个例子中我们可以看到,尽管PersonProtocol协议没有实现任何功能,我们仍然可以在需要指定类型时使用协议。然而,我们无法创建协议的实例。这是因为协议中没有实现任何功能。例如,如果我们尝试创建PersonProtocol协议的实例,我们会收到error: 协议类型 'PersonProtocol' 不能实例化错误,如下面的例子所示:
var test = PersonProtocol(firstName: "Jon", lastName: "Hoffman", birthDate: bDateProgrammer)
我们可以在需要协议类型的地方使用任何符合我们协议的类或结构体的实例。例如,如果我们定义一个变量为PersonProtocol协议类型,那么我们可以用任何符合PersonProtocol协议的类或结构体来填充这个变量。为了这个例子,让我们假设我们有两个类型名为SwiftProgrammer和FootballPlayer,它们符合PersonProtocol协议:
var myPerson: PersonProtocol
myPerson = SwiftProgrammer(firstName: "Jon", lastName: "Hoffman", birthDate: bDateProgrammer)
print("\(myPerson.firstName) \(myPerson.lastName)")
myPerson = FootballPlayer(firstName: "Dan", lastName: "Marino", birthDate: bDatePlayer)
print("\(myPerson.firstName) \(myPerson.lastName)")
在这个例子中,我们首先创建了一个PersonProtocol协议类型的myPerson变量。然后我们将变量设置为SwiftProgrammer类型的实例并打印出名字和姓氏。接下来,我们将myPerson变量设置为FootballPlayer类型的实例并再次打印出名字和姓氏。需要注意的是,Swift 并不关心实例是类还是结构体。重要的是类型符合PersonProtocol协议类型。因此,如果我们的SwiftProgrammer类型是结构体,而FootballPlayer类型是类,我们之前的例子将完全有效。
正如我们之前看到的,我们可以将我们的PersonProtocol协议用作数组的类型。这意味着我们可以用符合PersonProtocol协议的任何类型的实例来填充数组。再次强调,只要类型符合PersonProtocol协议,类型是类还是结构体并不重要。以下是一个例子:
var programmer = SwiftProgrammer(firstName: "Jon", lastName: "Hoffman", birthDate: bDateProgrammer)
var player = FootballPlayer(firstName: "Dan", lastName: "Marino", birthDate: bDatePlayer)
var people: [PersonProtocol] = []
people.append(programmer)
people.append(player)
在这个例子中,我们创建了一个SwiftProgrammer类型的实例和一个FootballPlayer类型的实例。然后我们将这两个实例添加到people数组中。
使用协议的多态性
我们在之前的例子中看到的是一种多态形式。多态这个词来源于希腊语根词Poly,意为许多,morphe,意为形式。在编程语言中,多态是单一接口对多种类型(许多形式)。在之前的例子中,单一接口是PersonProtocol协议,多种类型是符合该协议的任何类型。
多态性使我们能够以统一的方式与多个类型交互。为了说明这一点,我们可以扩展我们之前的示例,其中我们创建了一个PersonProtocol类型的数组并遍历该数组。然后我们可以使用在PersonProtocol协议中定义的属性和方法来访问数组中的每个项目,而不管实际的类型是什么。让我们看看这个示例:
for person in people {
print("\(person.firstName) \(person.lastName): \(person.profession)")
}
如果我们运行此示例,输出将类似于以下内容:
Jon Hoffman: Swift Programmer
Dan Marino: Football Player
在本章中,我们提到过几次,当我们定义变量、常量、集合类型等类型为协议类型时,我们就可以使用符合该协议的任何类型的实例。这是一个非常重要的概念,也是协议和协议扩展之所以强大的原因。
当我们使用协议来访问实例时,如前一个示例所示,我们仅限于使用协议中定义的属性和方法。如果我们想使用特定于单个类型的属性或方法,我们需要将该实例转换为该类型。
使用协议进行类型转换
类型转换是一种检查实例类型的方法,并且/或者将实例视为指定的类型。在 Swift 中,我们使用is关键字来检查实例是否是特定类型,并使用as关键字将实例视为特定类型。
首先,让我们看看如何使用is关键字来检查实例类型。以下示例展示了如何进行此操作:
for person in people {
if person is SwiftProgrammer {
print("\(person.firstName) is a Swift Programmer")
}
}
在这个示例中,我们使用if条件语句来检查people数组中的每个元素是否是SwiftProgrammer类型的实例,如果是,我们将打印出该人是 Swift 程序员到控制台。虽然这是一种检查我们是否具有特定类或结构体实例的好方法,但如果我们要检查多个类型,则效率不高。如果我们想要检查多个类型,使用switch语句会更高效,如下一个示例所示:
for person in people {
switch (person) {
case is SwiftProgrammer:
print("\(person.firstName) is a Swift Programmer")
case is FootballPlayer:
print("\(person.firstName) is a Football Player")
default:
print("\(person.firstName) is an unknown type")
}
}
在前一个示例中,我们展示了如何使用switch语句来检查数组中每个元素的实例类型。为此检查,我们在每个case语句中使用is关键字尝试匹配实例类型。
在第四章中,控制流和函数,我们看到了如何使用where语句来过滤条件语句。我们还可以使用where语句与is关键字一起过滤数组,如下面的示例所示:
for person in people where person is SwiftProgrammer {
print("\(person.firstName) is a Swift Programmer")
}
现在让我们看看我们如何将一个类或结构体的实例转换为特定类型。为此,我们可以使用 as 关键字。由于转换可能会失败,如果实例不是指定的类型,as 关键字有两种形式:as? 和 as!。使用 as? 形式,如果转换失败,它返回 nil,而使用 as! 形式,如果转换失败,我们会得到一个运行时错误;因此,除非我们绝对确定实例类型,或者在我们进行转换之前对实例类型进行了检查,否则建议使用 as? 形式。
让我们看看我们如何使用 as? 关键字将一个类或结构体的实例转换为指定的类型:
for person in people {
if let p = person as? SwiftProgrammer {
print("\(person.firstName) is a Swift Programmer")
}
}
由于 as? 关键字返回一个可选值,我们可以使用可选绑定来执行转换,如下面的例子所示。如果我们确定实例类型,我们可以使用 as! 关键字。以下示例展示了如何使用 as! 关键字在过滤数组结果时仅返回 SwiftProgrammer 类型的实例:
for person in people where person is SwiftProgrammer {
let p = person as! SwiftProgrammer
}
现在我们已经涵盖了协议的基础知识,即多态工作和类型转换,让我们深入了解 Swift 协议扩展中最激动人心的一个新特性。
协议扩展
协议扩展允许我们扩展一个协议,为符合的类型提供方法和属性实现。它们还允许我们为所有符合的类型提供通用实现,从而消除了在每个单独的类型中提供实现或创建类层次结构的需要。虽然协议扩展可能看起来并不太吸引人,但一旦你看到它们真正的强大之处,它们将改变你思考和使用代码的方式。
让我们从使用一个非常简单的例子来看如何使用协议扩展开始。我们将从定义一个名为 DogProtocol 的协议开始,如下所示:
protocol DogProtocol {
var name: String {get set}
var color: String {get set}
}
使用这个协议,我们是在说任何符合 DogProtocol 协议的类型,都必须有两个 String 类型的属性,即 name 和 color。现在让我们定义符合此协议的三个类型。我们将这些类型命名为 JackRussel、WhiteLab 和 Mutt,如下所示:
struct JackRussel: DogProtocol {
var name: String
var color: String
}
class WhiteLab: DogProtocol {
var name: String
var color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
}
struct Mutt: DogProtocol {
var name: String
var color: String
}
我们故意将 JackRussel 和 Mutt 类型创建为结构体,将 WhiteLab 类型创建为类,以展示两种类型设置方式的不同,并说明当涉及到协议和协议扩展时,它们是如何以相同的方式被处理的。在这个例子中,我们可以看到的最大区别是结构体类型提供了一个默认的初始化器,但在类中,我们必须提供初始化器来填充属性。
现在假设我们想要为符合 DogProtocol 协议的每个类型提供一个名为 speak 的方法。在协议扩展之前,我们会从向协议添加方法定义开始,如下面的代码所示:
protocol DogProtocol {
var name: String {get set}
var color: String {get set}
func speak() -> String
}
一旦在协议中定义了该方法,我们随后就需要为符合该协议的每个类型提供一个该方法的实现。根据符合此协议的类型数量,实现这一过程可能需要一些时间。以下代码示例展示了我们可能如何实现此方法:
struct JackRussel: DogProtocol {
var name: String
var color: String
func speak() -> String {
return "Woof Woof"
}
}
class WhiteLab: DogProtocol {
var name: String
var color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
func speak() -> String {
return "Woof Woof"
}
}
struct Mutt: DogProtocol {
var name: String
var color: String
func speak() -> String {
return "Woof Woof"
}
}
虽然此方法可行,但效率不高,因为每次我们更新协议时,都需要更新所有符合该协议的类型,并且我们可能会在代码中重复很多内容,如本例所示。另一个担忧是,如果我们需要更改 speak()方法的默认行为,我们必须进入每个实现并更改 speak()方法。这就是协议扩展发挥作用的地方。
使用协议扩展,我们可以将 speak()方法定义从协议本身中提取出来,并在协议扩展中定义它,带有默认行为。以下代码展示了我们将如何定义协议和协议扩展:
protocol DogProtocol {
var name: String {get set}
var color: String {get set}
}
extension DogProtocol {
func speak() -> String {
return "Woof Woof"
}
}
我们首先使用原始的两个属性定义DogProtocol。然后创建一个扩展DogProtocol的协议扩展,其中包含 speak()方法的默认实现。使用此代码,我们无需在每个符合DogProtocol的类型的实现中提供 speak()方法的实现,因为它们会自动作为协议的一部分接收实现。让我们通过将符合DogProtocol的三个类型恢复到它们的原始实现来查看这是如何工作的,它们应该会从协议扩展中接收 speak()方法:
struct JackRussel: DogProtocol {
var name: String
var color: String
}
class WhiteLab: DogProtocol {
var name: String
var color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
}
struct Mutt: DogProtocol {
var name: String
var color: String
}
我们现在可以使用以下代码所示的方式使用每个类型:
let dash = JackRussel(name: "Dash", color: "Brown and White")
let lily = WhiteLab(name: "Lily", color: "White")
let buddy = Mutt(name: "Buddy", color: "Brown")
let dSpeak = dash.speak() // returns "woof woof"
let lSpeak = lily.speak() // returns "woof woof"
let bSpeak = buddy.speak() // returns "woof woof"
如此例所示,通过将 speak()方法添加到 DogProtocol协议扩展中,我们自动将该方法添加到所有符合 DogProtocol的类型中。 DogProtocol协议扩展中的 speak()方法可以被视为 speak()方法的默认实现,因为我们能够在类型实现中覆盖它。例如,我们可以在以下代码中覆盖 Mutt结构体中的 speak()方法:
struct Mutt: DogProtocol {
var name: String
var color: String
func speak() -> String {
return "I am hungry"
}
}
当我们为Mutt类型的实例调用 speak()方法时,它将返回字符串"I am hungry"。
现在我们已经看到了如何使用协议和协议扩展,让我们看看一个更实际的例子。在多个平台(iOS、Android 和 Windows)的众多应用程序中,我需要验证用户输入。这种验证可以用正则表达式非常容易地完成;然而,我们不希望各种正则表达式散布在我们的代码中。通过创建包含验证代码的不同类或结构体来解决这个问题非常容易;然而,我们必须组织这些类,使它们易于使用和维护。在 Swift 的协议扩展之前,我会使用协议来定义验证要求,然后为每个需要的验证创建一个符合协议的结构体。让我们看看这个预协议扩展方法。
注意
正则表达式是一系列字符,用于定义特定的模式。这个模式可以用来搜索字符串,以查看字符串是否匹配该模式或包含该模式的匹配项。大多数主要的编程语言都包含正则表达式解析器,如果你不熟悉正则表达式,了解它们可能是有益的。
以下代码显示了定义任何我们想要用于文本验证的类型的要求的TextValidationProtocol协议:
protocol TextValidationProtocol {
var regExMatchingString: String {get}
var regExFindMatchString: String {get}
var validationMessage: String {get}
func validateString(str: String) -> Bool
func getMatchingString(str: String) -> String?
}
在此协议中,我们定义了三个属性和两个方法,任何符合TextValidationProtocol的类型都必须实现。这三个属性是:
-
regExMatchingString:这是一个正则表达式字符串,用于验证输入字符串是否只包含有效字符。 -
regExFindMatchString:这是一个正则表达式字符串,用于从输入字符串中检索只包含有效字符的新字符串。这个正则表达式通常在我们需要实时验证输入时使用,因为用户输入信息时,它会找到输入字符串的最长匹配前缀。 -
validationMessage:这是如果输入字符串包含非有效字符时显示的错误信息。
此协议的两种方法如下:
-
validateString:如果输入字符串只包含有效字符,此方法将返回true。在这个方法中,将使用regExMatchingString属性来执行匹配。 -
getMatchingString:此方法将返回一个只包含有效字符的新字符串。这个方法通常在我们需要实时验证用户输入时使用,因为它会找到输入字符串的最长匹配前缀。我们将使用regExFindMatchString属性来检索新字符串。
现在我们来看看如何创建一个符合此协议的结构体。以下结构体会被用来验证输入字符串是否只包含字母字符:
struct AlphaValidation1: TextValidationProtocol {
static let sharedInstance = AlphaValidation1()
private init(){}
let regExFindMatchString = "^[a-zA-Z]{0,10}"
let validationMessage = "Can only contain Alpha characters"
var regExMatchingString: String { get {
return regExFindMatchString + "$"
}
}
func validateString(str: String) -> Bool {
if let _ = str.rangeOfString(regExMatchingString, options: .RegularExpressionSearch) {
return true
} else {
return false
}
}
func getMatchingString(str: String) -> String? {
if let newMatch = str.rangeOfString(regExFindMatchString, options: .RegularExpressionSearch) {
return str.substringWithRange(newMatch)
} else {
return nil
}
}
}
在此实现中,regExFindMatchString和validationMessage属性是存储属性,而regExMatchingString属性是一个计算属性。我们还在结构体内部实现了validateString()和getMatchingString()方法。
通常情况下,我们会有几个不同的类型符合TextValidationProtocol,其中每个类型都会验证不同类型的输入。正如我们从AlphaValidation1结构体中可以看到的,每个验证类型都涉及一些代码。大量的代码也会在每个类型中重复。两个方法(validateString()和getMatchingString())和regExMatchingString属性的代码会在每个验证类中重复。这并不理想,但如果我们想避免创建一个包含重复代码的超级类类层次结构(我个人更喜欢使用值类型而不是类),我们就别无选择。现在让我们看看我们如何使用协议扩展来实现这一点。
使用协议扩展时,我们需要稍微改变一下对代码的思考方式。最大的不同是,我们既不需要也不希望定义协议中的所有内容。使用标准协议或当我们使用类层次结构时,所有你想要通过通用超类或协议访问的方法和属性都必须在超类或协议中定义。使用协议扩展时,如果我们打算在协议扩展中定义它,我们更倾向于不在协议中定义属性或方法。因此,当我们用协议扩展重写我们的文本验证类型时,TextValidationProtocol将大大简化,看起来类似于这样:
protocol TextValidationProtocol {
var regExFindMatchString: String {get}
var validationMessage: String {get}
}
在原始的TextValidationProtocol中,我们定义了三个属性和两个方法。正如我们在这个新协议中可以看到的,我们只定义了两个属性。现在我们已经定义了TextValidationProtocol,让我们为它创建一个协议扩展:
extension TextValidationProtocol {
var regExMatchingString: String { get {
return regExFindMatchString + "$"
}
}
func validateString(str: String) -> Bool {
if let _ = str.rangeOfString(regExMatchingString, options: .RegularExpressionSearch) {
return true
} else {
return false
}
}
func getMatchingString(str: String) -> String? {
if let newMatch = str.rangeOfString(regExFindMatchString, options: .RegularExpressionSearch) {
return str.substringWithRange(newMatch)
} else {
return nil
}
}
}
在TextValidationProtocol协议扩展中,我们定义了原始TextValidationProtocol中定义的两个方法和第三个属性,但在新协议中没有定义。现在我们已经创建了我们的协议和协议扩展,我们能够定义我们的文本验证类型。在下面的代码中,我们定义了三个结构体,我们将使用这些结构体在用户输入文本时验证文本:
struct AlphaValidation: TextValidationProtocol {
static let sharedInstance = AlphaValidation()
private init(){}
let regExFindMatchString = "^[a-zA-Z]{0,10}"
let validationMessage = "Can only contain Alpha characters"
}
struct AlphaNumericValidation: TextValidationProtocol {
static let sharedInstance = AlphaNumericValidation()
private init(){}
let regExFindMatchString = "^[a-zA-Z0-9]{0,15}"
let validationMessage = "Can only contain Alpha Numeric characters"
}
struct DisplayNameValidation: TextValidationProtocol {
static let sharedInstance = DisplayNameValidation()
private init(){}
let regExFindMatchString = "^[\\s?[a-zA-Z0-9\\-_\\s]]{0,15}"
let validationMessage = "Display Name can contain only contain Alphanumeric Characters"
}
在每个文本验证结构体中,我们创建一个静态常量和私有初始化器,以便我们可以将结构体用作单例。有关单例模式的更多信息,请参阅第十七章中的单例设计模式部分,在 Swift 中使用设计模式。
在我们定义了单例模式之后,在每种类型中我们只需设置 regExFindMatchString 和 validationMessage 属性的值。现在,我们实际上并没有复制代码,因为即使我们可以,我们也不希望在协议扩展中定义单例代码,因为我们不希望将该模式强加给所有符合的类型。
要使用文本验证类,我们希望创建一个字典对象,将 UITextField 对象映射到要使用的验证类,如下所示:
var validators = [UITextField: TextValidationProtocol]()
然后,我们可以像下面这样填充 validators 字典:
validators[alphaTextField] = AlphaValidation.sharedInstance
validators[alphaNumericTextField] = AlphaNumericValidation.sharedInstance
validators[displayNameTextField] = DisplayNameValidation.sharedInstance
现在,我们可以将文本字段的 EditingChanged 事件设置为名为 keyPressed() 的单个方法。要为每个字段设置编辑更改事件,我们将在视图控制器的 viewDidLoad() 方法中添加以下代码:
alphaTextField.addTarget(self, action:Selector("keyPressed:"), forControlEvents: UIControlEvents.EditingChanged)
alphaNumericTextField.addTarget(self, action: Selector("keyPressed:"), forControlEvents: UIControlEvents.EditingChanged)
displayNameTextField.addTarget(self, action: Selector("keyPressed:"), forControlEvents: UIControlEvents.EditingChanged)
现在让我们创建 keyPressed() 方法,当用户在字段中输入字符时,每个文本字段都会调用此方法:
@IBAction func keyPressed(textField: UITextField) {
if let validator = validators[textField] where !validator.validateString(textField.text!) {
textField.text = validator.getMatchingString(textField.text!)
messageLabel?.text = validator.validationMessage
}
}
在这个方法中,我们使用 if let validator = validators[textField] 语句来检索特定文本字段的验证器,然后我们使用 where !validator.validateString(textField.text!) 语句来验证用户输入的字符串。如果字符串验证失败,我们使用 getMatchingString() 方法通过从输入字符串的第一个无效字符开始删除所有字符,并显示来自文本验证类的错误消息来更新文本字段中的文本。如果字符串通过验证,文本字段中的文本保持不变。
在本书的下载代码中,你可以找到一个示例项目,演示如何使用文本验证类型。
摘要
在本章中,我们看到了 Swift 如何将协议视为完整的类型。我们还看到了如何在 Swift 中使用协议实现多态。我们通过深入了解协议扩展来结束本章,并看到了我们如何在 Swift 中使用它们。
协议和协议扩展是苹果新协议导向编程范式的骨架。这种新的编程模型有可能改变我们编写和思考代码的方式。虽然我们在这章没有专门介绍协议导向编程,但理解本章的内容为我们提供了学习这种新编程模型所需的关于协议和协议扩展的坚实基础。
第七章:使用可用性和错误处理编写更安全的代码
当我最初开始用 Objective-C 编写 iOS 和 OS X 应用程序时,最明显的 缺陷 是在处理 Cocoa 和 Cocoa Touch 框架时缺乏异常处理。大多数现代编程语言,如 Java 和 C#,都使用 try-catch 块或类似机制来处理异常。虽然 Objective-C 确实有 try-catch 块,但它并没有在 Cocoa 框架内部使用,并且它从未感觉像是语言的一部分。我确实有丰富的 C 语言经验,所以我能够理解 Cocoa 和 Cocoa Touch 框架如何接收和响应错误,并且坦白说,我实际上更喜欢这种方法,尽管我已经习惯了使用 Java 和 C# 进行异常处理。当 Swift 首次推出时,我希望能看到 Apple 将真正的错误处理集成到语言中,这样我们就有选择使用它的选项;然而,它并没有包含在 Swift 的初始版本中。现在随着 Swift 2 的推出,Apple 已经将错误处理添加到了 Swift 中。虽然这种错误处理看起来可能类似于 Java 和 C# 中的异常处理,但有一些非常显著的不同之处。
本章我们将涵盖以下主题:
-
如何在 Swift 中使用
do-catch块 -
如何表示错误
-
如何使用可用性属性
Swift 2.0 之前的错误处理
错误处理是我们应用程序中响应和恢复错误条件的过程。在 Swift 2.0 之前,错误报告遵循与 Objective-C 相同的模式;然而,随着 Swift 的推出,我们确实有使用可选返回值的额外好处,其中返回 nil 会指示函数中存在错误。
在最简单的错误处理形式中,函数的返回值将指示其是否成功。这个返回值可以是简单的布尔值 true/false,也可以是更复杂的枚举,其值表示如果函数失败,实际上发生了什么。如果我们需要报告关于发生的错误的附加信息,我们可以添加一个 NSError 输出参数,其类型为 NSErrorPointer,但这并不是最容易的方法,并且这些错误往往被开发者忽略。以下示例说明了在 Swift 2.0 之前通常是如何处理错误的:
var str = "Hello World"
var error: NSError
var results = str.writeToFile(path, atomically: true, encoding: NSUTF8StringEncoding, error: &error)
if results {
// successful code here
} else {
println("Error writing filer: \(error)")
}
虽然以这种方式处理错误效果良好,并且可以根据大多数需求进行修改,但它绝对不是完美的解决方案。这个解决方案有几个问题,其中最大的问题是开发者很容易忽略返回的值以及错误本身。虽然大多数经验丰富的开发者都会非常小心地检查所有错误,但有时,对于新手开发者来说,很难理解应该检查什么以及何时检查,尤其是如果函数不包含 NSError 参数的话。
除了使用 NSError,我们还可以使用 NSException 类来抛出和捕获异常;然而,实际上很少开发者使用这种方法。即使在 Cocoa 和 Cocoa Touch 框架中,这种异常处理方法也很少被使用。
虽然使用 NSError 类和返回值来处理错误是可行的,但包括我在内很多人对 Apple 在 Swift 最初发布时没有包含额外的错误处理功能感到失望。然而,现在随着 Swift 2.0 的发布,我们确实有了本地的错误处理功能。
Swift 2 中的错误处理
类似于 Java 和 C# 这样的语言通常将错误处理过程称为 异常处理;在 Swift 文档中,Apple 将这个过程称为 错误处理。虽然从外观上看,Java 和 C# 的异常处理可能与 Swift 的错误处理非常相似,但熟悉其他语言异常处理的开发者会在本章中注意到一些显著的不同之处。
表示错误
在我们真正理解 Swift 中的错误处理机制之前,我们首先需要了解我们如何表示一个错误。在 Swift 中,错误是通过符合 ErrorType 协议的类型值来表示的。Swift 的枚举非常适合于建模错误条件,因为通常我们只需要表示有限数量的错误条件。
让我们看看我们如何使用枚举来表示一个错误。为此,我们将定义一个虚构的错误名为 MyError,包含三个错误条件:Minor、Bad 和 Terrible:
enum MyError: ErrorType {
case Minor
case Bad
case Terrible
}
在这个例子中,我们定义 MyError 枚举符合 ErrorType 协议。然后我们定义三个错误条件:Minor、Bad 和 Terrible。我们还可以使用关联值来表示错误条件。假设我们想要给其中一个错误条件添加一个描述;我们可以这样做:
enum MyError: ErrorType {
case Minor
case Bad
case Terrible (description: String)
}
对于熟悉 Java 和 C# 中的异常处理的开发者来说,他们可能会发现 Swift 中的错误表示要干净得多,也更容易。我们拥有的另一个优点是定义多个错误条件并将它们分组在一起非常容易,因此所有相关的错误条件都属于同一类型。
现在,让我们看看我们如何在 Swift 中建模错误。为了这个例子,让我们看看我们如何为一个棒球队分配球员号码。在棒球队中,每个被召回的新球员都会被分配一个唯一的号码,这个号码也必须在一定的号码范围内。在这种情况下,我们会遇到三个错误条件:号码太大、号码太小或号码不唯一。以下示例展示了我们如何表示这些错误条件:
enum PlayerNumberError: ErrorType {
case NumberTooHigh(description: String)
case NumberTooLow(description: String)
case NumberAlreadyAssigned
}
使用 PlayerNumberError 类型,我们定义了三个非常具体的错误条件,这些条件可以确切地告诉我们出了什么问题。这些错误条件也分组在同一个类型中,因为它们都与分配玩家号码相关。
这种定义错误的方法允许我们定义非常具体的错误,让我们的代码在发生错误条件时确切地知道出了什么问题,正如我们在示例中看到的那样,它还允许我们将错误分组,因此所有相关的错误都可以在同一个类型中定义。
现在我们知道了如何表示错误,让我们看看我们如何抛出错误。
抛出错误
当函数中发生错误时,调用函数的代码必须知道这一点;这被称为抛出错误。当函数抛出错误时,它假设调用函数的代码,或者链中的某些代码,将捕获并适当地从错误中恢复。
要从函数中抛出错误,我们使用throws关键字。这个关键字让调用它的代码知道函数可能会抛出错误。与其它语言的异常处理不同,我们不需要列出可能抛出的具体错误类型。
注意
由于我们不在函数定义中列出可能抛出的具体错误类型,因此在文档和注释中列出这些错误类型是一种良好的实践,这样其他使用我们函数的开发者就会知道应该捕获哪些错误类型。
让我们看看我们如何抛出错误,但首先,让我们向之前定义的PlayerNumberError类型中添加一个第四个错误。如果尝试通过球员的号码检索球员但未分配该号码,则抛出此错误条件。新的PlayerNumberError类型现在看起来类似于以下内容:
enum PlayerNumberError: ErrorType {
case NumberTooHigh(description: String)
case NumberTooLow(description: String)
case NumberAlreadyAssigned
case NumberDoesNotExist
}
为了演示如何抛出错误,我们将首先创建一个BaseballTeam结构体,它将包含一个给定队伍的球员列表。这些球员将存储在一个名为players的字典对象中,并使用球员的号码作为键。用于表示单个球员的BaseballPlayer类型,将是一个元组类型的typealias,其定义如下:
typealias BaseballPlayer = (firstName: String, lastName: String, number: Int)
在这个BaseballTeam结构体中,我们将有两个方法。第一个方法将被命名为addPlayer()。这个方法将有一个BaseballPlayer类型的参数,并尝试将球员添加到队伍中。这个方法可能会抛出三种错误条件之一:NumberTooHigh、NumberTooLow或NumberAlreadyExists。以下是这个方法的实现方式:
mutating func addPlayer(player: BaseballPlayer) throws {
guard player.number < maxNumber else {
throw PlayerNumberError.NumberTooHigh(description: "Max number is \(maxNumber)")
}
guard player.number > minNumber else {
throw PlayerNumberError.NumberTooLow(description: "Min number is \(minNumber)")
}
guard players[player.number] == nil else {
throw PlayerNumberError.NumberAlreadyAssigned
}
players[player.number] = player
}
在方法定义中,我们看到添加了throws关键字。throws关键字让调用此方法的任何代码都知道它可能会抛出错误,并且必须处理这些错误。然后我们使用了三个guard语句。这些guard语句用于验证数字不是太大、不是太小,并且在players字典中是唯一的。如果任何条件不满足,我们将使用throw关键字抛出适当的错误。
如果我们通过了所有三个检查,球员将被添加到players字典中。
我们将要添加到 BaseballTeam 结构体的第二个方法是 getPlayerByNumber() 方法。这个方法将尝试检索被分配了给定编号的棒球运动员。如果没有球员被分配了这个编号,这个方法将抛出 PlayerNumberError.NumberDoesNotExist 错误。getPlayerByNumber() 方法将看起来像这样:
func getPlayerByNumber(number: Int) throws -> BaseballPlayer {
if let player = players[number] {
return player
} else {
throw PlayerNumberError.NumberDoesNotExist
}
}
在这个方法定义中,我们看到它可以抛出错误,因为我们使用了定义中的 throws 关键字。throws 关键字必须放在方法定义中的 return 类型之前。
在方法中,我们尝试检索传递给方法编号的棒球运动员。如果我们能够检索到球员,我们就返回它;否则,我们抛出 PlayerNumberError.NumberDoesNotExist 错误。注意,如果我们从一个具有 return 类型的方法中抛出错误,我们不需要返回一个值。
现在我们来看看我们如何用 Swift 捕获一个错误。
捕获错误
当从函数中抛出错误时,我们需要在调用函数的代码中捕获它;这是通过使用 do-catch 块来完成的。do-catch 块采用以下语法:
do {
try [Some function that throws]
[Any additional code]
} catch [pattern] {
[Code if function threw error]
}
如果抛出错误,它将传播出去,直到被 catch 子句处理。catch 子句由 catch 关键字组成,后跟一个用于匹配错误的模式。如果错误与模式匹配,则执行 catch 块内的代码。
让我们来看看我们如何通过调用 BaseballTeam 结构体的 getPlayerByNumber() 和 addPlayer() 方法来使用 do-catch 块。首先让我们看看 getPlayerByNumber() 方法,因为它只抛出一个错误条件:
do {
let player = try myTeam.getPlayerByNumber(34)
print("Player is \(player.firstName) \(player.lastName)")
} catch PlayerNumberError.NumberDoesNotExist {
print("No player has that number")
}
在这个例子中,do-catch 块调用了 BaseballTeam 结构体的 getPlayerByNumber() 方法。如果队中没有球员被分配了这个号码,该方法将抛出 PlayerNumberError.NumberDoesNotExist 错误条件;因此,我们在 catch 语句中尝试匹配这个错误。
任何时候在 do-catch 块中抛出错误,块内的其余代码将被跳过,并且执行与错误匹配的 catch 块内的代码。因此,在我们的例子中,如果 getPlayerByNumber() 方法抛出 PlayerNumberError.NumberDoesNotExist 错误,那么 print() 函数永远不会被执行。
我们不需要在 catch 语句后包含一个模式。如果 catch 语句后没有包含模式或者我们放置一个下划线,catch 语句将匹配所有错误条件。例如,以下两个 catch 语句中的任何一个都将捕获所有错误:
do {
// our statements
} catch {
// our error conditions
}
do {
// our statements
} catch _ {
// our error conditions
}
如果我们想要捕获错误,我们可以使用 let 关键字,如下面的示例所示:
do {
// our statements
} catch let error {
print("Error: \(error)")
}
现在我们来看看我们如何使用 catch 语句,类似于 switch 语句,来捕获不同的错误条件。为此,我们将调用我们的 BaseballTeam 结构体的 addPlayer() 方法:
do {
try myTeam.addPlayer(("David", "Ortiz", 34))
} catch PlayerNumberError.NumberTooHigh(let description) {
print("Error: \(description)")
} catch PlayerNumberError.NumberTooLow(let description) {
print("Error: \(description)")
} catch PlayerNumberError.NumberAlreadyAssigned {
print("Error: Number already assigned")
}
在这个例子中,我们有三个catch语句。每个catch语句都有一个不同的模式来匹配;因此,它们将分别匹配不同的错误条件。如果我们回想一下,PlayerNumberError.NumberToHigh和PlayerNumberError.NumberToLow错误条件都有关联的值。要检索关联的值,我们可以在括号内使用let语句,就像示例中那样。
总是好的做法是将你的最后一个catch语句留空,这样它就能捕获之前catch语句中未匹配到的任何错误。因此,之前的例子应该这样重写:
do {
try myTeam.addPlayer(("David", "Ortiz", 34))
} catch PlayerNumberError.NumberTooHigh(let description) {
print("Error: \(description)")
} catch PlayerNumberError.NumberTooLow(let description) {
print("Error: \(description)")
} catch PlayerNumberError.NumberAlreadyAssigned {
print("Error: Number already assigned")
} catch {
print("Error: Unknown Error")
}
我们也可以让错误传播出去而不是立即捕获它们。要做到这一点,我们只需要在函数定义中添加throws关键字。例如,在以下示例中,我们不是捕获错误,而是让它传播到调用函数的代码中,而不是在函数内部处理错误:
func myFunc() throws {
try myTeam.addPlayer(("David", "Ortiz", 34))
}
如果我们确定不会抛出错误,我们可以使用强制-尝试表达式来调用函数,该表达式写作try!。强制-尝试表达式禁用了错误传播,并将函数调用包裹在一个运行时断言中,断言该调用不会抛出错误。如果抛出错误,我们将得到运行时错误,所以使用这个表达式时要非常小心。
当我在使用 Java 和 C#等语言处理异常时,我看到很多空的catch块。这就是我们需要捕获异常的地方,因为可能会抛出异常;然而,我们并不想对它做任何事情。在 Swift 中,代码看起来可能像这样:
do {
let player = try myTeam.getPlayerByNumber(34)
print("Player is \(player.firstName) \(player.lastName)")
} catch {}
看到这样的代码是我对异常处理不喜欢的事情之一。嗯,Swift 开发者对此有一个答案:try?关键字。try?关键字尝试执行可能会抛出错误的操作。如果操作成功,结果以可选的形式返回;然而,如果操作失败并抛出错误,操作返回 nil,并且错误被丢弃。
由于try?关键字的结果以可选的形式返回,我们通常希望使用可选绑定来使用这个关键字。我们可以将之前的例子重写如下:
if let player = try? myTeam.getPlayerByNumber(34) {
print("Player is \(player.firstName) \(player.lastName)")
}
如我们所见,try?关键字使我们的代码更加简洁和易于阅读。
如果我们需要执行一些清理操作,无论是否有错误,我们都可以使用defer语句。我们使用defer语句在代码执行离开当前作用域之前执行一段代码。以下示例显示了如何使用defer语句:
func deferFunction() {
print("Function started")
var str: String?
defer {
print("In defer block")
if let s = str {
print("str is \(s)")
}
}
str = "Jon"
print("Function finished")
}
如果我们调用此函数,控制台打印的第一行是—函数开始。代码的执行将跳过 defer 块,接下来控制台会打印 Function finished。最后,在离开函数的作用域之前,会执行 defer 块中的代码,我们会看到消息,在 defer 块中。以下是从该函数输出的内容:
Function started
Function finished
In defer block
str is Jon
defer 块将在执行离开当前作用域之前始终被调用,即使抛出错误。当我们需要在离开函数之前执行一些清理函数时,defer 块非常有用。
当我们想要确保执行所有必要的清理操作,即使抛出错误时,defer 语句非常有用。例如,如果我们成功打开一个文件进行写入,我们总是想确保关闭该文件,即使写入操作期间出现错误。然后我们可以将文件关闭功能放在 defer 块中,以确保文件在离开当前作用域之前始终关闭。
现在让我们看看如何使用 Swift 中的新可用性属性。
可用性属性
使用最新的 SDK 可以让我们访问为我们正在开发的平台提供的所有最新功能;然而,有时我们还想针对旧平台。Swift 允许我们使用可用性属性来安全地包装代码,以确保只有在正确的操作系统版本可用时才运行。可用性属性首次在 Swift 2 中引入。
可用性块基本上允许我们说,“如果我们正在运行指定的操作系统版本或更高版本,则运行此代码。否则,运行其他代码。”我们可以使用 availability 属性的两种方式。第一种方式允许我们执行特定的代码块,并且可以与 if 或 guard 语句一起使用。第二种方式允许我们将方法或类型标记为仅在特定平台上可用。
availability 属性接受最多五个以逗号分隔的参数,允许我们定义执行我们的代码所需的操作系统或应用程序扩展的最小版本。这些参数是:
-
iOS: 这是与我们的代码兼容的最小 iOS 版本 -
OSX: 这是与我们的代码兼容的最小 OS X 版本 -
watchOS: 这是与我们的代码兼容的最小 watchOS 版本 -
iOSApplicationExtension: 这是与我们的代码兼容的最小 iOS 应用程序扩展 -
OSXApplicationExtension: 这是与我们的代码兼容的最小 OS X 应用程序扩展
在参数之后,我们指定所需的最低版本。我们只需要包含与我们的代码兼容的参数。例如,如果我们正在编写 iOS 应用程序,我们只需要在available属性中包含iOS参数。我们用*(星号)结束参数列表。让我们看看我们如何仅在我们满足最低要求时执行特定的代码块:
if #available(iOS 9.0, OSX 10.10, watchOS 2, *) {
// Available for iOS 9, OSX 10.10, watchOS 2 or above
print("Minimum requirements met")
} else {
// Block on anything below the above minimum requirements
print("Minimum requirements not met")
}
在这个示例中,if #available(iOS 9.0, OSX 10.10, watchOS 2, *)这一行代码阻止了当应用程序在不符合指定最低操作系统版本的系统上运行时执行代码块。在这个示例中,我们还使用了else语句,如果操作系统未满足最低要求,将执行一个单独的代码块。
我们还可以限制对函数或类型的访问。在前面的代码中,available属性以#(井号)字符为前缀。要限制对函数或类型的访问,我们用@(在号)字符作为available属性的前缀。以下示例展示了我们如何限制对类型和函数的访问:
@available(iOS 9.0, *)
func testAvailability() {
// Function only available for iOS 9 or above
}
@available(iOS 9.0, *)
struct TestStruct {
// Type only available for iOS 9 or above
}
在上一个示例中,我们指定了只有在代码在具有 iOS 9 或更高版本的设备上运行时,才能访问testAvailability()函数和testStruct()类型。为了使用@available属性来阻止对函数或类型的访问,我们必须使用#available属性将调用该函数或类型的代码包裹起来。以下示例展示了我们如何调用testAvailability()函数:
if #available(iOS 9.0, *) {
testAvailability()
} else {
// Fallback on earlier versions
}
在这个示例中,只有当应用程序在具有 iOS 9 或更高版本的设备上运行时,才会调用testAvailability()函数。
摘要
在本章中,我们探讨了 Swift 2 中添加的新错误处理功能和availability属性。这两个功能可以帮助我们编写更安全的代码,并使我们的应用程序更加稳定。
Swift 2 的错误处理功能显著改变了 Swift 程序员处理错误的方式。虽然我们不需要在我们的自定义类型中使用这个新功能,但它确实为我们提供了一种统一的方式来处理和响应错误。苹果公司也开始在 Cocoa 和 Cocoa Touch 框架中使用这种错误处理。
新的availability属性允许我们开发能够利用目标操作系统的最新功能的应用程序,同时仍然允许我们的应用程序在旧版本上运行。
在下一章中,我们将探讨如何创建和解析 XML 和 JSON 文档。
第八章. 处理 XML 和 JSON 数据
多年来,我一直将可扩展标记语言(XML)作为首选的数据交换格式,用于系统之间的数据交换。它的简单性、可读性和易用性使其成为易选。在我看来,XML 的唯一真正缺点是 XML 文档的大小很大。移动设备,如 iOS 设备,在未连接到 Wi-Fi 网络时,依赖于通过移动网络交换数据。这些移动网络通常比标准的 Wi-Fi 或有线网络慢。大多数移动设备也有数据计划,限制了用户在一个月内可以使用的数据量。那时,我开始真正考虑使用JavaScript 对象表示法(JSON)在系统之间交换数据。现在,我几乎完全使用 JSON 来交换数据,尤其是在移动设备上。尽管对于移动开发来说,JSON 似乎正在成为首选格式,但 XML 仍然非常广泛地被使用,因为它通常比 JSON 更容易阅读和使用。对于开发者来说,了解这两种格式都是很有帮助的。
在本章中,我们将介绍:
-
解析 XML 文档
-
构建 XML 文档
-
解析 JSON 文档
-
构建 JSON 文档
XML 和 JSON
并非很久以前,大多数基于消费者的应用程序都是自包含的,不需要与外部服务交换数据。然而,在智能手机和数据驱动应用程序的时代,现在开发不需要与外部服务交换数据的应用程序已经很少见了。这使得应用开发者了解如何以标准格式交换数据变得至关重要。
现在,API 设计者倾向于选择两种格式之一来交换数据——XML 或 JSON。多年来,已经推广了许多其他的数据交换格式,但 XML 和 JSON 无疑是当前的领导者。主要原因在于 XML 和 JSON 的开放性和互操作性是其他数据交换格式无法比拟的。很难找到一个不提供 XML 和/或 JSON 以交换数据的公共 Web API。
苹果公司提供了简单高效的 API 来处理 XML 和 JSON 数据。虽然有一些第三方库和框架在苹果 API 之上提供了一定的优势和劣势,但在本章中,我们将坚持使用苹果的 API。让我们看看我们将如何使用 Swift 解析 XML 和 JSON 文档,但首先让我们创建一些我们将用于本章中 XML 和 JSON 示例的常见文件。
常见文件
让我们先创建一个结构,该结构将用于定义我们 XML 和 JSON 文档中有效的标签。这些标签将是:
-
books: 这是包含所有其他元素的根元素 -
book: 此元素包含关于特定书籍的所有信息 -
author: 此元素包含作者的姓名 -
publisher: 此元素包含出版者的名称 -
category:此元素包含书籍的分类 -
description:此元素包含书籍的描述 -
name:这是 XML 示例中书籍元素的属性,也是 JSON 示例中的标准元素。此元素包含书籍的名称
DocTags 结构将定义包含这七个标签名称的七个静态属性。以下代码展示了如何定义这个结构:
struct DocTags {
static let BOOKS_TAG = "books"
static let BOOK_TAG = "book"
static let AUTHOR_TAG = "author"
static let PUBLISHER_TAG = "publisher"
static let NAME_TAG = "name"
static let CATEGORY_TAG = "category"
static let DESCRIPTION_TAG = "description"
}
注意
而不是使用结构体,我们可以在枚举中定义我们的文档标签。我们使用哪一个实际上完全取决于个人喜好。
在 DocTags 结构体中定义的七个属性都是使用 static 和 let 关键字定义的。static 关键字将属性定义为 static 属性。一个 static 属性是不与结构体的任何给定实例相关联的,对 static 属性的任何更改都会反映在结构体的所有实例中。与 instance 属性相比,static 属性的优势在于我们不需要创建结构体的实例就可以使用它。
接下来,我们需要创建一个包含每本书信息的类。我们将把这个类命名为 Book:
class Book {
var name = ""
var author = ""
var publisher = ""
var category = ""
var description = ""
}
注意
虽然 Apple 建议我们使用值类型(结构体和枚举)而不是引用类型(类),但在本章的示例中,我们更倾向于使用引用类型,这样我们就可以在构建过程中传递我们书籍类型的实际实例,而不是值。
如我们所见,Book 类包含五个属性。这些属性将包含关于每本书的信息。
当我们解析 XML 和 JSON 文档时,我们将能够检索文档的每个元素及其存储在元素中的值;因此,我们需要一种方法来获取这些信息并设置 Book 属性的值。考虑到这一点,让我们创建一个辅助函数,该函数将接受元素的名称及其关联的值作为参数。然后,我们将根据元素的名称设置适当的属性。让我们把这个函数命名为 addValue 并将其添加到我们的 Book 类中:
func addValue(tagName: String, withValue value: String) {
switch tagName {
case DocTags.NAME_TAG:
self.name = value
case DocTags.AUTHOR_TAG:
self.author = value
case DocTags.PUBLISHER_TAG:
self.publisher = value
case DocTags.CATEGORY_TAG:
self.category = value
case DocTags.DESCRIPTION_TAG:
self.description = value
default:
break
}
}
addValue 函数将使用 switch 语句将元素名称与 DocTags 结构体中定义的每个标签进行比较。如果找到匹配项,它将设置相应属性的值。如果没有找到匹配项,它将跳过该元素;通常,我们应该能够简单地忽略额外的标签。
注意
通常,当我们处理一个类时,如本例中的情况,解析器代码并不太糟糕。当你开始处理更复杂的 XML 和 JSON 文档时,可能需要多个类,拥有像 addValue 方法这样的辅助方法可以显著清理解析代码,并使其更容易阅读。这些辅助函数可以放在它们自己的类中,或者作为数据存储类的一部分(如前例所示),具体取决于对特定应用程序的最佳做法。通常,我更喜欢将辅助函数与数据存储类分开。
XML 和 NSXMLParser 类
在 Swift 中解析 XML 文档时,我们将使用 Apple 的 NSXMLParser 类。虽然 NSXMLParser 有几种替代方案,每种方案都有其自身的优缺点,但我一直发现 NSXMLParser 简单易懂且易于使用。它还设计得与 Apple 的其他 API 保持一致,这意味着如果我们熟悉 Apple 的其他 API,NSXMLParser 将显得相当直观。
NSXMLParser 类是一个 简单 XML API (SAX) 解析器。SAX 解析器提供了一种按顺序解析 XML 文档的机制。与将整个文档读入内存然后进行解析的 文档对象模型 (DOM) 解析器不同,SAX 解析器在解析过程中报告每个解析事件。这允许在解析过程中占用更小的内存空间。这也意味着我们需要编写代码来处理解析 XML 文档所需的每个解析事件。
NSXMLParser 类可以从 URL、NSData 对象或通过流解析 XML 文档。为了从各种来源解析 XML 文档,我们将使用适当的初始化器初始化 NSXMLParser 类:
-
Init(contentsOfURL:):使用提供的 URL 引用的内容初始化NSXMLParser类 -
Init(data:):使用NSData对象的内容初始化NSXMLParser类 -
Init(stream:):使用提供的流的内容初始化NSXMLParser类
对于本章的 XML 示例,我们将使用 init(data:) 初始化器来解析 XML 文档的字符串表示形式。NSData 类旨在与二进制数据一起使用。初始化器可以很容易地替换为其他任何初始化器,以从 URL 或流中解析 XML 文档。XML 解析示例将被设计为解析以下 XML 文档:
<?xml version="1.0"?>
<books>
<book name="iOS and OS X Network Development Cookbook">
<author>Jon Hoffman</author>
<publisher>PacktPub</publisher>
<category>Programming</category>
<description>Network development for iOS and OS X</description>
</book>
<book name="Mastering Swift">
<author>Jon Hoffman</author>
<publisher>PacktPub</publisher>
<category>Programming</category>
<description>Learning Swift</description>
</book>
</books>
使用 NSXMLParserDelegate 协议
NSXMLParserDelegate 协议定义了几个可选方法,这些方法可以被 NSXMLParser 代理定义。当 NSXMLParser 解析 XML 文档时发生某些解析事件时,会调用这些方法。NSXMLParserDelegate 方法还可以定义几个可选方法,用于处理 文档类型定义 (DTD) 标记。DTD 标记通过定义一系列有效元素和属性来定义 XML 文档的合法结构。
在本章中,我们将实现以下代理方法来处理 XML 示例:
-
parserDidStartDocument(_:): 当解析器开始解析 XML 文档时,会调用此方法 -
parserDidEndDocument(_:): 解析器成功解析整个 XML 文档后,会调用此方法 -
parser(_: didStartElement: namespaceURI: qualifiedName: attributes:): 当解析器遇到元素的开始标签时,会调用此方法 -
parser(_: didEndElement: namespaceURI: qualifiedName:): 当解析器遇到元素的结束标签时,会调用此方法 -
parser(_:parseErrorOccurred:): 当解析器遇到一个关键错误且无法解析文档时,会调用此方法 -
parser(_:foundCharacters:): 当解析器需要提供当前元素的所有或部分字符的数据的字符串表示时,会调用此方法
让我们看看我们将如何使用 NSXMLParser 和 NSXMLParserDelegate 来解析 XML 文档。
解析 XML 文档
要解析一个 XML 文档,我们首先创建一个符合 NSXMLParseDelegate 协议的类或结构体。在我们的例子中,我们将这个类命名为 MyXMLParser。我们的 MyXMLParser 类定义将看起来像这样:
class MyXMLParser: NSObject, NSXMLParserDelegate {
}
在 MyXMLParser 类内部,我们将添加三个属性,这些属性将在解析文档时被解析器使用。这三个属性是:
-
books: 这个属性将是一个可选的数组,包含 XML 文档中定义的书籍列表 -
book: 这将是一个可选的Book类实例,代表 XML 文档中正在解析的当前书籍 -
elementData: 这将是一个字符串类的实例,包含正在解析的当前元素的值
这些属性将定义如下:
var books: [Book]?
var book: Book?
var elementData = ""
现在我们需要添加 NSXMLParserDelegate 方法。我们首先添加的是 parseXmlString 方法,它将用于启动 NSXMLParser 类:
func parseXmlString(xmlString: String) {
let xmlData = xmlString.dataUsingEncoding(NSUTF8StringEncoding)
let parser = NSXMLParser(data: xmlData!)
parser.delegate = self
parser.parse()
}
我们从将 xmlString 变量转换为 NSData 对象开始 parseXmlString() 方法,使用 dataUsingEncoding() 方法。dataUsingEncoding() 方法来自 NSString 类,但我们可以使用它与我们的 Swift 字符串类型,因为 Swift 自动将 Swift 字符串类型桥接到 NSString 类。
我们然后使用 init(data:) 初始化器来初始化 NSXMLParser。这个初始化器调用如下:
NSXMLParser(data: xmlData!)
然后,我们将 NSXMLParser 代理设置为当前 MyXmlParser 类的实例。我们可以这样做,因为 MyXmlParser 类符合 NSXMLParserDelegate 协议。这允许类的当前实例在文档解析时接收警报。
最后,调用 parse() 方法开始解析 XML 文档。
现在,让我们添加 parserDidStartDocument() 方法。当 NSXMLParser 开始解析 XML 文档时,会调用此方法:
func parserDidStartDocument(parser: NSXMLParser!) {
println("Started XML parser")
}
在我们的例子中,在解析文档之前我们不需要进行任何设置;因此,parserDidStartDocument() 方法只是将 Started XML Parser 消息打印到控制台。
现在,让我们看看 parser(_: didStartElement: namespaceURI: qualifiedName: attributes:) 代理方法。在我们实现这个代理方法之前,我们需要弄清楚哪些元素在我们遇到它们的开始标签时需要我们执行任务。在我们的例子中,我们需要检查两个元素的开始标签——books 和 book。
books 元素是包含 XML 文档中所有项目的根元素。当我们遇到 books 元素的开始标签时,我们需要初始化 books 数组。这个 books 数组将包含在解析 XML 文档时生成的书籍实例列表。
当我们遇到 book 元素的开始标签时,我们需要创建 Book 类的新实例,因为开始标签意味着我们开始了一本新书。我们可能会认为在创建新实例之前,我们应该也将当前 book 属性的实例保存到 books 数组中,但我们将这样做是在遇到 book 结束标签而不是开始标签时。基于结束标签而不是开始标签保存信息时,实现总是看起来更干净。
下面是 parser(_: didStartElement: namespaceURI: qualifiedName: attributes:) 代理方法的代码:
func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
if elementName == DocTags.BOOKS_TAG {
books = []
} else if elementName == DocTags.BOOK_TAG {
book = Book()
if let name = attributeDict[DocTags.NAME_TAG] {
book!.addValue(DocTags.NAME_TAG, withValue: name as!)
}
}
}
我们从这个方法开始,查看 elementName 参数是否等于 books 标签,如果是,我们创建一个新数组,该数组将包含 XML 文档中定义的书籍。如果 elementName 参数不等于 books 标签,我们检查它是否等于 Book 标签。如果它等于 book 标签,我们将 book 属性设置为 book 类的新实例,清除任何之前保存的信息。然后我们检查元素是否有具有 name 键(name)的属性,如果有,我们将 book 实例的 name 属性设置为该属性的值。这个 Book 类的实例将包含关于书籍的信息。
我们将要实现的下一个代理方法是 parser(_:foundCharacters:) 代理方法。此方法接收正在解析的元素的值或部分值。
func parser(parser: NSXMLParser, foundCharacters string: String) {
elementData += string
}
由于任何给定元素的值可能相当大,我们可能以片段的形式接收该值而不是一次性接收。这意味着 parser(_:foundCharacters:) 方法可能多次为同一元素调用。在我们的例子中,我们使用 elementData 属性来跟踪当前元素的值;因此,在 parser(_:foundCharacters:) 方法中,我们只需将字符串参数的值追加到 elementData 属性。当我们遇到元素的结束标签时,我们将清除 elementData 属性。
接下来,让我们看看当解析器遇到一个元素的结束标签时,如何使用parser(_: didEndElement: namespaceURI: qualifiedName:)代理方法。在我们实现这个方法之前,我们需要弄清楚在遇到这些元素的结束标签时,我们需要哪些元素来执行任务。在我们的例子中,我们需要检查是否遇到了book元素的结束标签。如果我们遇到了除了book元素结束标签之外的任何标签,我们将使用book实例的addValue()方法来确定如何处理这个值。
当我们遇到book元素的结束标签时,我们需要将当前book属性的实例添加到books数组中。如果这是一个其他元素的结束标签,我们将调用当前书实例的addValue()方法,记住addValue()方法将忽略任何它不认识的元素名称。
每当我们遇到一个元素的结束时,我们还需要清除elementData属性,以确保前一个元素的信息不会破坏下一个元素。
以下示例展示了我们将如何实现parser(_: didEndElement: namespaceURI: qualifiedName:)代理方法:
func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if elementName == DocTags.BOOK_TAG {
if let myBook = book {
if var _ = books {
books!.append(myBook)
}
}
book = Book()
} else if let myBook = book {
myBook.addValue(elementName, withValue: elementData)
}
elementData = ""
}
当解析器完成对文档的解析后,它将调用parser(_:parseErrorOccurred:)代理方法。在我们的例子中,我们将使用这个方法简单地打印出books数组中每本书的名称和作者到屏幕上,如下面的代码所示:
func parserDidEndDocument(parser: NSXMLParser) {
if let myBooks = books {
for myBook in myBooks {
println("Found - \(myBook.name) \(myBook.author)")
}
}
}
如果在解析文档时遇到错误,将调用parser:parseErrorOccurred:代理方法来处理错误。在我们的例子中,我们将打印错误到控制台,但通常我们需要正确处理错误:
func parser(parser: NSXMLParser parseErrorOccurred parseError: NSError {
print"Parse Error occurred (parseError)")
}
要解析一个 XML 文档,我们将使用MyXmlParser类,如下所示:
var xmlParser = MyXMLParser()
xmlParser.parseXmlString(xmlString)
现在我们已经看到了如何解析 XML 文档,让我们看看我们如何创建一个 XML 文档。
XML 和 NSXMLDocument
NSXMLDocument类及其相关类使得从我们的自定义对象创建 XML 文档变得非常容易;然而,在撰写本书时,它们仅适用于基于 OS X 的项目。希望苹果公司中有人会意识到我们需要一种好的方法来为 iOS 项目构建 XML 文档,而无需使用第三方框架或手动创建文档。
要为基于 OS X 的应用程序使用 Swift 构建 XML 文档,我们需要使用三个基础类。这些类如下:
-
NSNode:这个类是NSXMLDocument和NSXMLElement类的超类。这将用于向NSXMLElement类的实例添加属性。 -
NSXMLDocument:这个类是 XML 文档的最高级对象。 -
NSXMLElement:XML 文档中的所有元素都是NSXMLElement类的实例。
让我们看看如何使用这三个类来构建一个 XML 文档。为此,我们将创建一个名为buildXMLString(books:)的函数,它接受一个Book对象数组作为其唯一参数:
func buildXMLString(books: [Book]?)] -> String {
if let myBooks = books {
let xmlRoot = NSXMLElement(name: DocTags.BOOKS_TAG)
let xmlData = NSXMLDocument(rootElement: xmlRoot)
for book in myBooks {
let bookElement = NSXMLElement(name: DocTags.BOOK_TAG)
xmlRoot.addChild(bookElement)
let nameAttribute = NSXMLNode.attributeWithName(DocTags.NAME_TAG, stringValue:book.name) as NSXMLNode
bookElement.addAttribute(nameAttribute)
bookElement.addChild(NSXMLElement(name: DocTags.AUTHOR_TAG, stringValue: book.author))
bookElement.addChild(NSXMLElement(name: DocTags.CATEGORY_TAG, stringValue: book.category))
bookElement.addChild(NSXMLElement(name: DocTags.DESCRIPTION_TAG, stringValue: book.description))
bookElement.addChild(NSXMLElement(name: DocTags.PUBLISHER_TAG, stringValue: book.publisher))
}
return xmlData.XMLString
}
else {
return ""
}
}
由于books参数被定义为可选的,我们在buildXMLString()函数的开始处使用if let myBooks = books行来验证它是否不为空。如果它是空的,我们返回一个空字符串;否则,我们开始构建 XML 文档。
我们正在使用的NSXMLDocument类的初始化器需要 XML 文档的根元素;因此,我们将首先使用BOOKS_TAG常量创建xmlRoot常量,然后使用它来创建NSXMLDocument类的一个实例。接下来,我们将遍历books数组中的每个Book类实例。
对于Book类的每个实例,我们创建一个新的NSXMLElement类实例,包含书名,这将包含关于书籍的信息。这个元素将是我们的 XML 文档中的<Book></Book>元素。关于书籍的所有信息要么是这个元素的属性,要么是这个元素的子元素。
书名是book元素的属性;因此,我们需要创建一个包含属性名称和值的NSXMLNode类实例。我们使用NXMLNode.attributeWithName()函数来完成此操作。然后我们使用addAttribute()函数将该属性添加到book元素中。
接下来,我们使用addChild()函数将书籍的其余信息(author、category、description和publisher)作为子节点添加到book元素中。
最后,我们使用XMLString属性将NSXMLDocument类转换为字符串,并将该字符串返回给调用该函数的代码。
XML 和手动构建 XML 文档
由于我们无法在 iOS 项目中使用NSXMLNode、NSXMLDocument和NSXMLElement类,我们通常需要手动构建 XML 字符串或使用第三方库。这种方法容易出错,并且需要我们非常了解 XML 文档是如何构建的,但如果我们小心,我们可以通过这种方式创建简单的 XML 文档。
让我们看看如何手动创建一个 XML 文档。为此,我们将创建一个名为builXMLString()的函数,它接受一个Book对象数组作为其唯一参数。我们还将创建一个名为getElementString()的辅助类,该类将创建一个 XML 元素的字符串表示形式。getElementString()函数将接受两个元素:元素名称和值。让我们看看以下代码:
func buildXMLString(books: [Book]?) -> String {
var xmlString = ""
if let myBooks = books {
xmlString = "<\(DocTags.BOOKS_TAG)>"
for book in myBooks {
xmlString += "<\(DocTags.BOOK_TAG) \(DocTags.NAME_TAG)=\"\(book.name)\">"
xmlString += getElementString(DocTags.AUTHOR_TAG, elementValue: book.author)
xmlString += getElementString(DocTags.CATEGORY_TAG, elementValue: book.category)
xmlString += getElementString(DocTags.DESCRIPTION_TAG, elementValue: book.description)
xmlString += getElementString(DocTags.PUBLISHER_TAG, elementValue: book.publisher)
xmlString += "<\\\(DocTags.BOOK_TAG)>"
}
xmlString += "<\\\(DocTags.BOOKS_TAG)>"
}
return xmlString
}
func getElementString(elementName: String, elementValue: String) ->String {
return "<\(elementName)>\"\(elementValue)\"<\\\(elementName)>"
}
由于books参数被定义为可选的,我们在buildXMLString()函数的开始处使用if let myBooks = books行来验证它是否不为空。如果它是空的,函数将返回一个空字符串;否则,我们开始构建 XML 文档。
在这个类中,我们只是创建表示 XML 标签的字符串,并将它们附加到xmlString变量。在函数结束时,xmlString变量将包含 XML 文档。
getElementString()函数创建一个包含元素开始标签、元素值和元素结束标签的字符串。此函数用于添加本例中的大多数 XML 元素。
如我们所见,如果没有对 XML 文档语法的深入了解,几乎不可能使用这种方法构建复杂的文档。我们还需要非常小心,不要忘记在元素末尾的结束标签。
JSON 和 NSJSONSerialization
为了序列化和反序列化 JSON 文档,我们将使用NSJSONSerialization类。正如我们将看到的,使用NSJSONSerialization类与 JSON 文档相比,使用NSXMLParser类与 XML 文档相比要容易得多;然而,当我们尝试访问信息时,它可能会更容易出错。只需记住,在访问之前始终检查定义为可选的值是否为 null。
与NSXMLParser类不同,NSJSONSerialization类将解析整个 JSON 文档内存,然后返回一个 JSON 对象;因此,需要编写的代码要少得多,但它的内存消耗也更大。
NSJSONSerialization类可以从NSData对象或通过流解析 JSON 文档。为了从各种来源解析 JSON 文档,我们使用NSJSONSerialization类和适当的静态方法:
-
JSONObjectWithData(_: options: error:): 此初始化器将解析存储为NSData对象的 JSON 文档 -
JSONObjectWithStream(_: options: error:): 此初始化器将从流中解析 JSON 文档
这两个方法的文档说明它们返回一个AnyObject类型的可选值。通常,这些方法的返回结果是在NSDictionary或NSArray类的实例中,具体取决于 JSON 文档。如果您不确定正在创建的对象类型,可以插入以下代码,其中jsonResponse变量是从两个静态方法返回的结果:
switch jsonResponse {
case is NSDictionary:
// Code to parse a NSDictionary
case is NSArray:
// Code to parse an NSArray
default:
// Code to handle unknown type
}
在前面的代码中,我们使用is运算符来检查响应是否为NSDictionary或NSArray类型。
与NSXMLParser类不同,NSJSONSerialization类还可以用于从集合对象创建 JSON 文档。为此,我们将使用dataWithJSONObject(_: options: error:)初始化器,它将从集合对象序列化 JSON 文档。虽然可以使用除集合对象以外的其他对象来创建 JSON 文档,但一个合适的 JSON 文档通常是以字典或数组的格式。
在本章的 JSON 示例中,我们将向您展示如何解析以下 JSON 文档。该文档包含与 XML 示例中相同的信息,但现在它存储为一个 JSON 文档,如下面的代码所示:
{
"books": [
{
"name": "iOS and OS X Network Development Cookbook",
"author": "Jon Hoffman",
"publisher": "PacktPub",
"category": "Programming",
"description": "Network development for iOS and OS X"
},
{
"name": "Mastering Swift",
"author": "Jon Hoffman",
"publisher": "PacktPub",
"category": "Programming",
"description": "Learning Swift"
}
]
}
让我们看看如何解析存储为字符串的 JSON 文档。
解析 JSON 文档
在本节中,我们将使用 NSJSONSerialization 类来解析之前显示的 JSON 文档。此函数中的 jsonString 变量代表之前显示的 JSON 文档。此函数将根据 JSON 文档中的信息创建一个 Book 对象数组。函数结束时,我们将打印出有关书籍的信息,以显示它们已正确解析自文档,以及返回书籍数组,如下面的代码所示:
func parseJson() throws {
var myBooks: [Book] = []
let jsonData = jsonString.dataUsingEncoding(NSUTF8StringEncoding)
if let data = jsonData {
let jsonDoc : AnyObject = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)
if let books = jsonDoc.objectForKey(DocTags.BOOKS_TAG) as? NSArray {
for var i=0; i < books.count; i++ {
if let dict = books.objectAtIndex(i) as? NSDictionary {
let book = Book()
addValueToBook(book, elementName: DocTags.AUTHOR_TAG, elementValue: (dict.objectForKey(DocTags.AUTHOR_TAG) as? String))
addValueToBook(book, elementName: DocTags.CATEGORY_TAG, elementValue: (dict.objectForKey(DocTags.CATEGORY_TAG) as? String))
addValueToBook(book, elementName: DocTags.DESCRIPTION_TAG, elementValue: (dict.objectForKey(DocTags.DESCRIPTION_TAG) as? String))
addValueToBook(book, elementName: DocTags.NAME_TAG, elementValue: (dict.objectForKey(DocTags.NAME_TAG) as? String))
addValueToBook(book, elementName: DocTags.PUBLISHER_TAG, elementValue: (dict.objectForKey(DocTags.PUBLISHER_TAG) as? String))
myBooks.append(book)
}
}
for book in myBooks {
print("Found - \(book.name) \(book.author)")
}
}
}
}
parseJson() 函数首先将包含 JSON 文档的 jsonString 变量转换为 NSData 对象,以便我们可以使用 NSJSONSerialization 对象来解析它。由于转换结果是一个可选值,我们需要验证它是否不为空。我们使用以下代码来完成此操作:
if let data = jsonData
如果转换成功,我们就可以使用 NSJSONSerialization 类的 JSONObjectWithData() 方法从我们刚刚创建的 NSData 对象中创建 JSON 对象。
知道在我们的 JSON 文档中,books 的根标签包含一个书籍数组,我们使用以下行尝试从我们刚刚创建的 JSON 对象中检索数组:
if let books = jsonDoc.objectForKey(DocTags.BOOKS_TAG) as? NSArray
这行代码检查对象是否不为空,并且也是 NSArray 类的实例。如果 JSON 对象应该包含一个 NSDictionary 对象,我们只需将 as? NSArray 替换为 as? NSDictionary。
如果我们能够成功从 JSON 对象中检索到 NSArray 类,那么我们将遍历 NSArray 类中的每个项目。在我们的例子中,NSArray 类的每个项目都是一个 NSDictionary 类的实例;然而,始终验证这一点是个好主意。为了验证每个项目是 NSDictionary 类的实例,我们使用以下代码:
if let dict = books[i] as? NSDictionary
一旦我们有了 NSDictionary 对象,我们就使用 addValueToBook() 函数(我们将在下一分钟看到)来填充 Book 类的属性。
最后,我们通过打印出从 JSON 文档中提取的每本书的名称和作者来结束函数。让我们看看我们用来填充 Book 类属性的 addValueToBook() 函数:
func addValueToBook(book: Book, elementName: String, elementValue: String?) {
if let value = elementValue {
book.addValue(elementName, withValue: value)
}
}
如果我们尝试从一个键不存在的 NSDictionary 对象中提取值,NSDictionary 对象将返回一个空对象。在这种情况下,我们需要在将其分配给不接受空值的属性之前验证该值是否不为空。addValueToBook() 函数在将值添加到 Book 类的实例之前验证这些值是否不为空。
注意
在使用 NSJSONSerialization 类时,最好进行过多的检查而不是不足。只需记住,如果我们尝试将非可选变量设置为 nil 或使用 nil 的对象,我们的应用程序将会崩溃。JSON 文档不是类型安全的;因此,建议检查返回值的类型,以确保它们是预期的类型。
现在,让我们看看如何使用 NSJSONSerialization 类创建 JSON 文档。
创建 JSON 文档
使用 NSJSONSerialization 类创建 JSON 文档非常简单,但同样,我们需要进行几个检查以确保没有出错。以下代码将从任何可以转换为 JSON 数据的对象(如字典和/或数组)创建有效的 JSON 文档:
func buildJSON(value: AnyObject) throws -> String {
if NSJSONSerialization.isValidJSONObject(value) {
let data = try NSJSONSerialization.dataWithJSONObject(value, options: [])
if let string = NSString(data: data, encoding: NSUTF8StringEncoding) {
return string as String
}
}
return ""
}
在 buildJSON() 函数中,我们首先验证 value 参数是否为可以转换为 JSON 对象的类型。我们使用 NSJSONSerialization 类的 isValidJSONObject() 函数来完成此操作。如果 value 参数可以转换,则该函数将返回布尔值 true;否则,它将返回布尔值 false。
如果 value 参数可以转换为 JSON 对象,那么我们使用 NSJSONSerialization 类的 dataWithJSONObject() 函数将 value 参数转换为 JSON 数据。如果转换过程中出现问题,dataWithJSONObject() 函数将抛出一个错误,该错误将被抛回调用 buildJSON() 函数的代码。
最后,我们将 JSON 数据转换为 String 对象,并将其返回给调用该函数的代码。如果发生任何错误,我们将返回一个空字符串。
摘要
在本章中,我们了解到使用 NSJSONSerialization 类解析/构建 JSON 对象所需的代码比解析/构建 XML 对象少得多。然而,我们确实对使用 NSXMLParser 类如何解析文档有更多的控制。在使用 NSJSONSerialization 类和 NSXMLParser 类时,需要记住的关键点是,在尝试使用之前,我们需要检查可选变量是否不包含 nil 值。
虽然看起来大多数新的服务都在使用 JSON 格式而不是 XML,但了解这两种格式都是有益的,因为仍然有大量服务使用 XML。
第九章。自定义下标
自定义下标于 2012 年添加到 Objective-C 中。当时,Chris Lattner 已经开发了 Swift 两年,并且像 Objective-C 的其他良好特性一样,下标成为了 Swift 语言的一部分。我在 Objective-C 中并不经常使用自定义下标,但我知道当需要时它们是语言的一部分。在我看来,Swift 中的下标似乎更像是语言的自然部分,这可能是由于它们在发布时就是语言的一部分,而不是后来添加的。
在本章中,你将学习以下主题:
-
什么是自定义下标
-
如何为类、结构体或枚举添加自定义下标
-
如何创建读写和只读下标
-
如何在不使用自定义下标的情况下使用外部名称
-
如何使用多维下标
介绍下标
下标是访问集合、列表或序列元素的快捷方式。它们用于通过索引设置或检索值,而不是使用获取器和设置器方法。如果使用正确,下标可以显著提高我们自定义类型的可用性和可读性。
我们可以为单个类型定义多个下标,并且将根据传递给下标的索引类型选择适当的下标。我们还可以为我们的下标设置外部参数名称,这有助于区分具有相同类型的下标。
使用自定义下标类似于使用数组和字典的下标。例如,要访问数组中的一个元素,我们将使用 anArray[index] 语法,要访问字典中的一个元素,我们将使用相同的语法,即 aDictionary[key]。当我们为我们的自定义类型定义自定义下标时,我们也将使用相同的语法访问它们,即 ourType[key]。
在创建自定义下标时,我们应该努力使它们感觉像是类、结构体或枚举的自然部分。如前所述,下标可以显著提高我们代码的可用性和可读性,但如果我们过度使用下标,它们将不会感觉自然,并且难以使用。
在本章中,我们将探讨几个如何创建和使用自定义下标的示例。我们还将展示一个不使用下标的示例。在我们展示如何使用自定义下标之前,让我们回顾一下如何在 Swift 数组中使用下标,以了解 Swift 语言中下标的用法。我们应该以类似于 Apple 在语言本身中使用下标的方式使用下标,使我们的自定义下标易于理解和使用。
Swift 数组中的下标
以下示例展示了如何使用下标访问和更改数组的值:
var arrayOne = [1,2,3,4,5,6]
print(arrayOne[3]) //Displays '4'
arrayOne[3] = 10
print(arrayOne[3]) //Displays '10'
在先前的例子中,我们创建了一个整数数组,然后使用下标语法显示并更改数组中元素编号为3的项。下标主要用于从集合中获取或检索信息。我们通常不使用下标来应用特定逻辑以确定要选择哪个项。例如,我们不会使用下标向数组末尾添加项或检索数组中的项数。要向数组末尾添加项或获取数组中的项数,我们将使用类似这样的函数或属性:
arrayOne.append(7) //append 7 to the end of the array
arrayOne.count //returns the number of items in an array
在我们自定义的类型中,下标应该遵循 Swift 语言本身设定的相同标准,这样使用我们类型的其他开发者就不会因为实现方式而感到困惑。知道何时使用下标,何时不使用的关键在于理解下标将如何被使用。
读写自定义下标
让我们看看如何定义一个用于读取和写入后端数组的下标。读取和写入后端存储类是自定义下标最常见的使用之一,但正如我们将在本章中看到的,我们不需要有一个后端存储类。以下代码是一个用于读取和写入数组的下标:
class MyNames {
private var names:[String] = ["Jon", "Kim", "Kailey", "Kara"]
subscript(index: Int) -> String {
get {
return names[index]
}
set {
names[index] = newValue
}
}
}
如我们所见,语法与我们使用get和set关键字在类中定义属性的方式相似。区别在于我们使用subscript关键字声明下标。然后我们指定一个或多个输入和返回类型。
我们现在可以使用自定义下标,就像我们使用数组或字典的下标一样。以下代码展示了如何在先前的例子中使用下标:
var nam = MyNames()
print(nam[0]) //Displays 'Jon'
nam[0] = "Buddy"
print(nam[0]) //Displays 'Buddy'
在先前的代码中,我们创建了一个MyNames类的实例。然后我们显示索引0处的原始名称,更改索引0处的名称,并重新显示它。在这个例子中,我们使用在MyNames类中定义的下标来检索和设置MyNames类中names数组的元素。
虽然我们可以直接让names数组属性对外部代码开放,以便直接读写,但这会将我们的代码锁定在必须使用数组来存储数据。如果我们想将后端存储机制更改为字典对象,甚至 SQLite 数据库,我们将无法做到,因为所有外部代码也必须进行更改。下标非常擅长隐藏我们在自定义类型中存储信息的方式;因此,使用我们自定义类型的外部代码不依赖于任何特定的存储实现。
如果我们直接提供对数组的访问权限,我们也将无法验证外部代码是否将有效信息插入数组。使用下标,我们可以在将数据添加到数组之前通过设置器添加验证来确保传入的数据是正确的。这无论我们是在创建框架还是库时都非常有用。
只读自定义下标
我们也可以通过不在下标中声明 setter 方法或没有隐式声明 getter 或 setter 方法来使下标只读。以下代码展示了如何通过不声明 setter 方法来声明只读属性:
//No getter/setters implicitly declared
subscript(index: Int) ->String {
return names[index]
}
以下示例展示了如何通过只声明 getter 方法来声明只读属性:
//Declaring only a getter
subscript(index: Int) ->String {
get {
return names[index]
}
}
在第一个例子中,我们没有定义 getter 或 setter 方法。所以,Swift 将下标设置为只读,代码的行为就像是在 getter 定义中。在第二个例子中,我们特别在 getter 定义中设置了代码。这两个例子都是有效的只读下标。
计算下标
虽然前面的例子与在类或结构中使用存储属性非常相似,但我们也可以以类似计算属性的方式使用下标。让我们看看如何做到这一点:
struct MathTable {
var num: Int
subscript(index: Int) -> Int {
return num * index
}
}
在前面的例子中,我们使用数组作为下标的后端存储机制。在这个例子中,我们使用下标的值来计算返回值。我们将使用这个下标如下:
var table = MathTable(num: 5)
print(table[4])
这个例子将显示初始化中定义的数字5乘以下标值4的计算值,等于 20。
下标值
在前面的下标示例中,所有的下标都接受整数作为下标的值;然而,我们并不限于整数。在以下示例中,我们将使用字符串类型作为下标的值。下标也将返回字符串类型:
struct Hello {
subscript (name: String) ->String {
return "Hello \(name)"
}
}
在这个例子中,下标接受一个字符串作为下标内的值,并返回一条消息,说Hello。让我们看看如何使用这个下标:
var hello = Hello()
print(hello["Jon"])
这个例子将在控制台显示消息,Hello Jon。
带有范围的下标
类似于我们如何使用数组中的range运算符,我们也可以让我们的自定义下标使用range运算符。让我们扩展之前创建的MathTable结构,包括一个将接受range运算符的第二个下标,看看它是如何工作的:
struct MathTable {
var num: Int
subscript(index: Int) -> Int {
return num * index
}
subscript(aRange: Range<Int>) -> [Int] {
var retArray: [Int] = []
for i in aRange {
retArray.append(self[i])
}
return retArray
}
}
我们例子中的新下标接受范围作为下标的值,然后返回一个整数数组。在下标内部,我们生成一个数组,将通过我们之前创建的另一个下标方法将范围的每个值乘以num属性,返回给调用代码。
以下示例展示了如何使用这个新的下标:
var table = MathTable(num: 5)
print(table[2...5])
如果我们运行这个例子,我们将看到一个包含值10、15、20和25的数组。
下标的外部名称
如我们本章前面提到的,我们可以为自定义类型有多个下标签名。将根据传入下标的索引类型选择合适的下标。有时我们可能希望定义多个具有相同类型的下标。为此,我们可以使用类似于为函数参数定义外部名称的外部名称。
让我们重写原始的MathTable结构,以包含两个下标,每个下标都接受一个整数作为下标类型;然而,一个将执行乘法操作,另一个将执行加法操作:
struct MathTable {
var num: Int
subscript(multiply index: Int) -> Int {
return num * index
}
subscript(addition index: Int) -> Int {
return num + index
}
}
如我们所见,在这个例子中,我们定义了两个下标,每个下标都是整数类型。两个下标之间的区别在于定义中的外部名称。在第一个下标中,我们定义了一个外部名称为multiply,因为我们在这个下标中将下标的值乘以下标内的num属性。在第二个下标中,我们定义了一个外部名称为addition,因为我们在这个下标中将下标的值加以下标内的num属性。
让我们看看如何使用这两个下标:
var table = MathTable(num: 5)
print(table[multiply: 4]) //Displays 20 because 5*4=20
print(table[addition: 4]) //Displays 9 because 5+4=9
如果我们运行这个示例,我们将看到根据下标内的外部名称使用了正确的下标。
如果我们需要多个相同类型的下标,使用我们的下标内的外部名称非常有用;除非它们需要用来区分多个下标,否则我不建议使用外部名称。
多维下标
虽然最常见的下标是只接受单个参数的下标,但下标并不限于单个参数。它们可以接受任何数量的输入参数,并且这些参数可以是任何类型。
让我们看看我们如何使用多维下标来实现 Tic-Tac-Toe 棋盘。Tic-Tac-Toe 棋盘看起来类似于这个:

棋盘可以用一个二维数组来表示,其中每个维度有三个元素。然后,每位玩家将轮流在棋盘上放置他的/她的棋子(通常是 X 或 O),直到有玩家在行中放置了三个棋子或者棋盘被填满。
让我们看看我们如何使用多维数组和多维下标实现 Tic-Tac-Toe 棋盘:
struct TicTacToe {
var board = [["","",""],["","",""],["","",""]]
subscript(x: Int, y: Int) -> String {
get {
return board[x][y]
}
set {
board[x][y] = newValue
}
}
}
我们通过定义一个 3x3 的数组来开始 Tic-Tac-Toe 结构,这个数组将代表游戏棋盘。然后我们定义一个下标,可以用来设置和检索棋盘上的玩家棋子。下标将接受两个整数值。通过在括号中放置值类型来定义多种类型。在我们的例子中,我们定义了下标,参数为(x: Int, y: Int)。然后我们可以在我们的下标中使用x和y变量名来访问传递进来的值。
让我们看看如何使用这个下标来设置用户在棋盘上的棋子:
var board = TicTacToe()
board[1,1] = "x"
board[0,0] = "o"
如果我们运行这段代码,我们将看到我们在中心方格添加了玩家x的棋子,在左上角方格添加了玩家o的棋子,因此我们的游戏棋盘看起来将类似于这个:

在我们的多维下标中,我们不仅限于使用单一类型,因此我们可以使用多种类型。例如,我们可能有一个(x: Int, y: Double, z: String)类型的下标。
我们还可以为我们的多维下标类型添加外部名称,以帮助识别使用哪些值,并区分具有相同类型的下标。让我们通过创建一个将根据下标值返回字符串实例数组的下标来查看如何使用多个类型和外部名称与下标一起使用:
struct SayHello {
subscript(messageText message:String, messageName name:String, number number:Int) -> [String]{
var retArray: [String] = []
for var i=0; i < number; i++ {
retArray.append("\(message) \(name)")
}
return retArray
}
}
在SayHello结构体中,我们定义我们的下标如下:
subscript(messageText message:String,messageName name:String, number number:Int) -> [String]
这定义了一个包含三个元素的下标。每个元素都有一个外部名称(message、name和number)和一个内部名称(message、name和number)。前两个元素是字符串类型,最后一个元素是整型。我们使用前两个元素为用户创建一个消息,该消息将重复由最后一个(number)元素定义的次数。我们将使用此下标如下:
var message = SayHello()
var ret = message[messageText:"Bonjour",messageName:"Jon",number:5]
如果我们运行此代码,我们将看到ret变量包含一个包含五个字符串的数组,每个字符串等于Bonjour Jon。
何时不应使用自定义下标
正如我们在本章中看到的,创建自定义下标可以真正增强我们的代码;然而,我们应该避免过度使用它们或以不符合标准下标用法的方式使用它们。避免过度使用下标的方法是检查 Swift 标准库中下标的用法。
让我们看看以下示例:
class MyNames {
private var names:[String] = ["Jon", "Kim", "Kailey", "Kara"]
var number: Int {
get {
return names.count
}
}
subscript(add name: String) -> String {
names.append(name)
return name
}
subscript(index: Int) -> String {
get {
return names[index]
}
set {
names[index] = newValue
}
}
}
在前面的示例中,在MyNames类中,我们定义了一个数组,该数组在我们的应用程序中使用。例如,假设在我们的应用程序中,我们显示这个名字列表,并允许用户向其中添加名字。在MyNames类中,我们定义以下下标,允许我们向数组中添加新名字:
subscript(add name: String) -> String {
names.append(name)
return name
}
这将是一个对下标语法的糟糕使用,因为它的使用与 Swift 语言中下标的用法不一致。这可能会在将来使用该类时造成混淆。更合适的方式是将这个下标重写为一个函数,如下所示:
func append(name: String) {
names.append(name)
}
记住,当你使用自定义下标时,确保你正在适当地使用它们。
摘要
正如我们在本章中看到的,为我们的自定义类型添加对下标的支持可以极大地增强其可读性和可用性。我们看到了下标可以用来在后台存储类和外部代码之间添加一个抽象层。下标也可以以类似的方式用于计算属性,其中下标用于计算一个值。正如我们指出的,下标的关键是适当地使用它们,并且以与 Swift 语言中的下标一致的方式使用。
第十章。使用可选类型
当我开始使用 Swift 时,我遇到的最大难题是可选类型。由于我的背景是 Objective-C、C、Java 和 Python,我能够将 Swift 的许多特性与我所知道的另一种语言中的工作方式联系起来,但可选类型却不同。在我使用的其他语言中,真的没有什么与可选类型相似的东西,因此我花了很多时间去完全理解它们。虽然我在第二章中简要介绍了可选类型,即“学习变量、常量、字符串和运算符”,这足以让我们开始学习,但我们还需要涵盖更多信息,才能真正理解可选类型是什么,如何正确使用它们,以及为什么它们在 Swift 语言中如此重要。
在本章中,我们将涵盖以下主题:
-
可选类型的简介
-
Swift 中可选类型的必要性
-
解包可选类型
-
可选绑定
-
可选链
介绍可选类型
当我们在 Swift 中声明变量时,它们默认是非可选的,这意味着它们必须包含一个有效、非 nil 的值。如果我们尝试将非可选变量设置为 nil,将会导致一个 Type '{type}' does not conform to protocol 'NilLiteralConvertible' 错误,其中 {type} 是变量的类型。
例如,以下代码在尝试将 message 变量设置为 nil 时将抛出错误,因为 message 是一个非可选类型:
var message: String = "My String"
message = nil
非常重要的是要理解 Swift 中的 nil 与 Objective-C 中的 nil 非常不同。在 Objective-C 中,nil 是指向不存在对象的指针;然而,在 Swift 中,nil 是值的缺失。这个概念对于完全理解 Swift 中的可选类型非常重要。
定义为可选的变量可以包含一个有效值,也可以表示值的缺失。我们通过将其分配一个特殊的 nil 值来表示值的缺失。任何类型的可选类型都可以设置为 nil,而在 Objective-C 中,只有对象可以设置为 nil。
要真正理解可选类型背后的概念,让我们看看定义可选类型的一行代码:
var myString: String?
末尾的问号表示 myString 变量是一个可选类型。当我们查看这段代码时,将这一行代码读作“myString 变量是一个可选的字符串类型”是不正确的。实际上,我们应该将这一行代码读作“myString 变量是一个可选类型,它可能包含一个字符串类型,也可能不包含任何值”。这两行代码之间的细微差别实际上在理解可选类型的工作方式上有着很大的不同。
可选类型是 Swift 中的一个特殊类型。当我们定义 myString 变量时,我们实际上将其定义为可选类型。为了理解这一点,让我们看看一些更多的代码:
var myString1: String?
var myString2: Optional<String>
这两个声明是等效的。两行都声明了一个可能包含字符串类型或可能没有值的可选类型。在 Swift 中,我们可以将值的缺失视为设置为 nil,但始终要记住,这与在 Objective-C 中设置 nil 是不同的。在这本书中,当我们提到 nil 时,我们是指 Swift 如何使用 nil,而不是 Objective-C 如何使用 nil。
可选类型是一个有两个可能值的枚举,None 和 Some(T),其中 T 是相关类型的关联值。如果我们将可选设置为 nil,那么它将具有 None 的值,如果我们设置一个值,那么可选将具有 Some 的值,并带有相关类型的关联值。在 第二章,学习变量、常量、字符串和运算符 中,我们解释了 Swift 中的枚举可以有关联值。关联值允许我们在枚举成员值中存储额外的信息。
内部,可选类型定义如下:
enum Optional<T> {
case None
case Some(T)
}
在这里,T 是与可选类型关联的类型。T 符号用于定义泛型。我们可以在 第十一章,使用泛型 中了解更多关于泛型的信息。
Swift 中可选类型的需求
现在,最迫切的问题是为什么 Swift 需要可选类型?为了理解为什么 Swift 有可选类型,我们应该检查可选类型旨在解决什么问题。
在大多数语言中,可以在不提供初始化值的情况下创建一个变量。例如,在 Objective-C 中,两行代码都是有效的:
int i;
MyObject *m;
现在,假设 MyObject 类有如下方法:
-(int)myMethodWithValue:(int)i {
return i*2;
}
这个方法接受从 i 参数传递的值,将其乘以 2,并返回结果。让我们尝试使用以下代码调用这个方法:
MyObject *m;
NSLog(@"Value: %d",[m myMethodWithValue:5]);
我们的第一反应可能是认为这段代码会显示 Value: 10;然而,这是错误的。实际上,这段代码会显示 Value: 0,因为我们没有在使用之前初始化 m 对象。
当我们忘记初始化一个对象或为变量设置值时,我们可能会在运行时得到意外的结果,就像我们刚才演示的那样。有时,这些意外的结果可能非常难以追踪。
使用可选类型,Swift 能够在编译时检测到这类问题,并在运行时提醒我们。如果我们期望在使用变量或对象之前它始终包含一个值,我们将变量声明为非可选类型(这是默认声明)。然后,在编译时,如果我们尝试在使用之前初始化它,我们会收到一个错误。让我们看看这个例子。以下代码会在编译时显示错误,因为我们试图在使用之前初始化一个非可选变量:
var myString: String
print(myString)
如果一个变量被声明为可选类型,在尝试使用它之前验证它是否包含有效的值是良好的编程实践。我们只应该声明变量为可选类型,如果确实有理由让变量不包含值。这也是 Swift 默认将变量声明为非可选类型的原因。
现在我们(希望)已经很好地理解了可选类型是什么以及它们旨在解决哪些类型的问题,让我们看看如何使用它们。
定义可选类型
通常,要定义可选类型,我们在变量声明中的类型后放置一个问号。请注意,我们在变量声明中定义的类型实际上是可选枚举中的关联值。以下代码展示了我们通常如何声明可选类型:
var myOptional: String?
这段代码声明了一个可能包含字符串或无值(nil)的可选变量。当一个变量以这种方式声明时,默认情况下,它被设置为无值。
使用可选类型
我们可以在代码中用多种方式使用可选类型。使用可选类型的关键是在访问之前始终验证它们是否包含有效的值。我们使用解包这个术语来指代检索可选类型的值。
强制解包可选类型
要解包或检索可选类型的值,我们在变量名后放置一个感叹号(!)。以这种方式强制解包可能非常危险,并且只有在我们确定值不是 nil 的情况下才应使用。
当我们使用感叹号来解包可选类型时,我们是在告诉编译器我们知道可选类型不包含 nil 值,因此请继续并给我们值。让我们看看如何做到这一点:
var myString1: String?
myString1 = "test"
var test: String = myString1!
这段代码将按预期工作,其中test变量将包含test字符串;然而,如果设置myString1可选类型为test的行被移除,当我们运行应用程序时,我们会收到运行时错误。请注意,编译器不会提醒我们问题,因为我们正在使用感叹号来解包可选类型;因此,编译器假设我们知道我们在做什么,并会愉快地为我们编译代码。我们应该在解包myString1可选类型之前验证它是否包含有效的值。以下示例是这样做的一种方式:
var myString1: String?
myString1 = "test"
if myString1 != nil {
var test:String = myString1!
}
现在,如果设置myString1可选类型为test的行被移除,我们不会收到运行时错误,因为我们只解包了myString可选类型,如果它包含一个有效的(非 nil)值。
如我们刚才所描述的,解包可选类型并不是最理想的方式,我们不推荐以这种方式解包可选类型。我们可以将验证和解包合并为一步,称为可选绑定。
可选绑定
可选绑定是解包可选推荐的方式。使用可选绑定,我们执行检查以查看可选是否包含有效值,如果是,则将其解包到临时变量或常量中。这一切都在一个步骤中完成。
可选绑定使用if或while条件语句执行。如果我们想将可选的值放入常量中,其格式如下:
if let constantName = optional {
statements
}
如果我们需要将值放入变量而不是常量中,可以使用var关键字而不是let关键字,如下例所示:
if var variableName = optional {
statements
}
以下示例展示了如何执行可选绑定:
var myString3: String?
myString3 = "Space"
if let tempVar = myString3 {
print(tempVar)
} else {
print("No value")
}
在示例中,我们将myString3变量定义为可选类型。如果myString3可选包含有效值,则我们将新变量tempvar设置为myString3可选的值,并将值打印到控制台。如果myString3可选不包含值,则打印No值到控制台。
从 Swift 1.2 开始,我们能够使用可选绑定在同一可选绑定行中解包多个可选。例如,如果我们有三个名为optional1、optional2和optional3的可选,我们可以使用以下代码一次性尝试解包所有三个:
If let tmp1 = optional1, tmp2 = optional2, tmp3 = optional3 {
}
如果三个可选中的任何一个解包失败,整个可选绑定语句将失败。
使用可选绑定将值赋给同名变量是完全可接受的。以下代码说明了这一点:
if let myOptional = myOptional {
print(myOptional)
} else {
print("myOptional was nil")
}
需要注意的一点是,temp变量仅在条件块内作用域,不能在条件块外使用。为了说明临时变量的作用域,让我们看一下以下代码:
var myOptional: String?
myOptional = "test"
if var tmp = myOptional {
print("Inside: \(tmp)")
}
// This next line will cause a compile time error
print("Outside: \(tmp)")
此代码无法编译,因为tmp变量仅在条件块内有效,而我们试图在条件块外使用它。
使用可选绑定比手动验证可选是否有值然后强制解包可选要干净和简单得多。
从函数、方法和子脚本返回可选
我们可以将函数或方法的返回类型设置为可选类型。这允许我们从函数或方法返回 nil(无值)。要设置返回类型为可选类型,我们将在函数或方法声明中的类型名称后插入一个问号。
以下示例展示了我们如何从函数或方法返回可选:
func getName(index: Int) -> String? {
let names = ["Jon", "Kim", "Kailey", "Kara"]
if index >= names.count || index < 0 {
return nil
} else {
return names[index]
}
}
在示例中,我们将返回类型定义为可选,可以是字符串值或无值。在函数内部,如果索引在数组范围内,我们将返回名称;如果索引超出数组范围,则返回nil。
以下代码展示了如何调用此函数,其中返回值是可选的:
var name = getName(2)
var name2 = getName(5)
在之前的代码中,name变量将包含Kailey,而name2变量将包含nil(无值)。注意,我们不需要将变量定义为可选的(使用问号),因为 Swift 知道它是一个可选类型,因为这是函数定义的返回类型。
我们还可以定义一个返回可选类型的下标。我们定义下标的方式与定义函数的方式相同。以下是一个返回可选的下标示例模板:
subscript(index: Int) -> String? {
//some statements
}
根据这个定义,我们能够从我们的下标返回一个nil(无值)。
在函数或方法中使用可选作为参数
我们还可以将可选作为函数或方法的参数。这允许我们在需要时将nil(无值)传递给函数或方法。以下示例展示了如何为函数定义可选参数:
func optionalParam(myString: String?) {
if let temp = myString {
print("Contains value \(temp)")
}
else {
print("Does not contain value")
}
}
要将参数定义为可选类型,我们在参数定义中使用问号。在这个示例中,我们使用可选绑定来检查可选是否包含值。如果包含值,我们在控制台打印Contains value;否则,我们打印Does not contain value。
元组中的可选类型
我们可以将整个元组定义为可选的,或者将元组内的任何元素定义为可选的。当从函数或方法返回元组时,使用可选与元组结合特别有用。这允许我们返回元组的一部分(或全部)作为nil。以下示例展示了如何将元组定义为可选的,以及如何将元组的单个元素定义为可选类型:
var tuple1: (one: String, two: Int)?
var tuple2: (one: String, two: Int?)
第一行将整个元组定义为可选类型。第二行将元组内的第二个值定义为可选,而第一个值是非可选的。
可选链
可选绑定允许我们一次解包一个可选,但如果可选类型嵌套在其他可选类型中会怎样?这将迫使我们必须在其他可选绑定语句中嵌入可选绑定语句。有一种更好的方法来处理这个问题,那就是使用可选链。在我们查看可选链之前,让我们看看这是如何与可选绑定一起工作的:
class Collar {
var color: String
init(color: String) {
self.color = color
}
}
class Pet {
var name: String
var collar: Collar?
init(name: String) {
self.name = name
}
}
class Person {
var name: String
var pet: Pet?
init(name: String) {
self.name = name
}
}
在这个示例中,我们首先定义一个Collar类,它有一个属性定义。这个属性名为color,其类型为字符串。我们可以看到color属性不是可选的;因此,我们可以安全地假设它将始终有一个有效的值。
接下来,我们定义一个Pet类,它有两个属性定义。这些属性分别命名为name和collar。name属性是字符串类型,而collar属性是可选的,可能包含一个Collar类型对象。
最后,我们定义一个Person类,它也有两个属性。这些属性分别命名为name和pet。name属性是字符串类型,而pet属性是可选的,可能包含一个Pet类型对象。
对于接下来的示例,让我们使用以下代码来初始化类:
var jon = Person(name: "Jon")
var buddy = Pet(name: "Buddy")
jon.pet = buddy
var collar = Collar(color: "red")
buddy.collar = collar
现在,假设我们想获取某人的宠物的颜色;然而,这个人可能没有宠物(pet 属性为 nil)或者宠物可能没有项圈(collar 属性为 nil)。我们可以使用可选绑定来钻入每一层,如下例所示:
if let tmpPet = jon.pet, tmpCollar = tmpPet.collar {
print("The color of the collar is \(tmpCollar.color)")
}
else {
print("Cannot retrieve color")
}
虽然这个例子完全有效,并且会打印出消息The color of the collar is red,但代码相当混乱且难以理解,因为我们嵌套了可选绑定语句。
可选链允许我们在一行代码中钻入多个可选类型层级的属性、方法和子脚本。这些层级可以连在一起,如果任何一层返回 nil,整个链将优雅地失败并返回 nil。如果没有值返回 nil,则返回链的最后一个值。由于可选链的结果可能是一个 nil 值,结果总是以可选类型返回,即使我们检索的最终值不是可选类型。
要指定可选链,我们将在链中的每个可选值后面放置一个问号 (?)。以下示例展示了如何使用可选链使前一个示例更简洁、更容易阅读:
if let color = jon.pet?.collar?.color {
print("The color of the collar is \(color)")
} else {
print("Cannot retrieve color")
}
在这个例子中,我们在 pet 和 collar 属性后面加上问号,表示它们是可选类型,如果任一值是 nil,整个链将返回 nil。这段代码也会打印出消息The color of the collar is red;然而,它比使用可选绑定的前一个例子更容易阅读。
空值合并运算符
空值合并运算符与我们在本书第二章学习变量、常量、字符串和运算符中讨论的三元运算符类似。三元运算符根据比较运算符或布尔值的评估给变量赋值。空值合并运算符解包可选值,如果它包含一个值,则返回该值,如果可选值为 nil,则返回默认值。
让我们看看空值合并运算符的原型:
optionalA ?? defaultValue
在这个例子中,我们演示了当可选值包含 nil 和包含值时如何使用空值合并运算符:
var defaultName = "Jon"
var optionalA: String?
var optionalB: String?
optionalB = "Buddy"
var nameA = optionalA ?? defaultName
var nameB = optionalB ?? defaultName
在这个例子中,我们首先将 defaultName 变量初始化为 Jon。然后我们定义了两个名为 optionalA 和 optionalB 的可选值。optionalA 变量将被设置为 nil,而 optionalB 变量将被设置为 Buddy。
nil 合并运算符在最后两行中使用。由于 optionalA 变量包含一个 nil,nameA 变量将被设置为 defaultName 变量的值,即 Jon。nameB 变量将被设置为 optionalB 变量的值,因为它包含一个值。
摘要
虽然 Swift 语言中使用的可选类型的概念一开始可能有些陌生,但随着你使用它们的频率增加,它们会变得更加有意义。可选类型的一个最大优点是我们得到了额外的编译时检查,这会提醒我们在使用之前是否忘记初始化非可选变量。
本章要掌握的一个要点是可选的概念。为了加强这个概念,让我们回顾一下本章的一些段落。
非常重要的是要理解 Swift 中的 nil 与 Objective-C 中的 nil 非常不同。在 Objective-C 中,nil 是一个指向不存在对象的指针;然而,在 Swift 中 nil 是一个值的缺失。这个概念对于完全理解 Swift 中的可选类型非常重要。
被定义为可选的变量可以包含一个有效值,或者它可能没有值。我们通过将 Swift 的特殊 nil 值赋给变量来设置变量为无值状态。任何类型的可选变量都可以设置为 nil,而在 Objective-C 中,只有对象可以被设置为 nil。
可选类型是一个有两个可能值的枚举,None 和 Some (T),其中 T 是相关类型的关联值。如果我们将可选设置为 nil,它将具有 None 的值,如果我们设置一个值,可选将具有 Some 的值,并带有相关类型的关联值。在 第二章,学习变量、常量、字符串和运算符 中,我们解释了 Swift 中的枚举可以有关联值。关联值允许我们在枚举成员值中存储额外的信息。
第十一章。泛型的工作方式
我第一次接触泛型是在 2004 年,当时它们首次在 Java 编程语言中引入。我仍然记得我拿起我的 The Java Programming Language 第四版,它涵盖了 Java 5,并阅读了关于 Java 泛型实现的内容。从那时起,我在许多项目中使用了泛型,不仅是在 Java 中,还在其他语言中也是如此。如果你熟悉其他语言中的泛型,如 Java,Swift 使用的语法将对你来说很熟悉。泛型允许我们编写非常灵活和可重用的代码;然而,就像与下标一样,我们需要确保我们正确地使用它们,并且不要过度使用它们。
在本章中,我们将涵盖以下主题:
-
泛型的介绍
-
创建和使用泛型函数
-
创建和使用泛型类
-
使用协议中的关联类型
泛型的介绍
泛型的概念已经存在了一段时间,因此对于来自像 Java 或 C# 这样的语言的开发者来说,这应该不是一个新的概念。Swift 对泛型的实现与这些语言非常相似。对于那些来自像 Objective-C 这样的没有泛型的语言的开发者来说,它们可能一开始会显得有些陌生。
泛型允许我们编写非常灵活和可重用的代码,避免了重复。在像 Swift 这样的类型安全语言中,我们经常需要编写适用于多种类型的函数或类型。例如,我们可能需要编写一个交换两个变量值的函数;然而,我们可能使用这个函数来交换两个字符串类型、两个整型类型和两个双精度浮点类型。没有泛型,我们将需要编写三个单独的函数;但是,有了泛型,我们可以编写一个泛型函数,为多种类型提供交换功能。泛型允许我们告诉一个函数或类型——我知道 Swift 是一个类型安全语言,但我不知道还需要哪种类型。我现在会给你一个占位符,稍后我会告诉你需要强制执行的类型。
在 Swift 中,我们有能力定义泛型函数和泛型类型。让我们首先看看泛型函数。
泛型函数
让我们从检查泛型试图解决的问题开始,然后我们将看到泛型是如何解决这个问题的。假设我们想要创建交换两个变量值的函数(如介绍中所述);然而,对于我们的应用程序,我们需要交换两个整型、两个双精度浮点型和两个字符串。没有泛型,这将需要我们编写三个单独的函数。以下代码显示了这些函数可能看起来像什么:
func swapInts (inout a: Int, inout b: Int) {
let tmp = a
a = b
b = tmp
}
func swapDoubles(inout a: Double, inout b: Double) {
let tmp = a
a = b
b = tmp
}
func swapStrings(inout a: String, inout b: String) {
let tmp = a
a = b
b = tmp
}
使用这三个函数,我们可以交换两个整数的原始值、两个双精度浮点数和两个字符串的值。现在,假设,随着我们进一步开发应用程序,我们发现我们还需要交换两个UInt32、两个浮点数或甚至是一些自定义类型的值。我们可能会轻易地得到八个或九个交换函数。最糟糕的部分是,这些函数中每个都包含重复的代码。这些函数之间的唯一区别是变量类型的改变。虽然这个解决方案是可行的,但泛型提供了一个更加优雅和简单的解决方案,它消除了代码的重复。让我们看看我们如何将这些三个先前的函数压缩成一个泛型函数:
func swap<T>(inout a: T, inout b: T) {
let tmp = a
a = b
b = tmp
}
让我们看看我们是如何定义swap()函数的。函数本身看起来与普通函数非常相似,只是有一个大写的T。在swap()函数中使用的大写T是一个占位符类型,它告诉 Swift 我们将在稍后定义该类型。当我们定义类型时,我们定义的类型将替换所有占位符。
要定义一个泛型函数,我们在函数名后面包含两个尖括号<T>中的占位符类型。然后我们可以使用该占位符类型来代替参数定义、返回类型或函数本身中的任何类型定义。需要牢记的是,一旦占位符被定义为类型,所有其他占位符都会假定该类型。因此,使用该占位符定义的任何变量或常量都必须符合该类型。
大写的T并没有什么特殊之处,我们可以用任何有效的标识符来代替T。以下定义是完全有效的:
func swap<G>(inout a: G, inout b: G) {
//Statements
}
func swap<xyz>(inout a: xyz, inout b: xyz) {
//Statements
}
在大多数文档中,泛型占位符通常使用T(表示类型)或E(表示元素)来定义。对于标准用途,我们将在本书中使用T来定义泛型占位符。在代码中使用T来定义泛型占位符也是一个好的实践,这样当我们稍后查看代码时,占位符可以很容易地被识别。
如果我们需要使用多个泛型类型,我们可以通过逗号分隔来创建多个占位符。以下示例展示了如何为单个函数定义多个占位符:
func testGeneric<T,E>(a:T, b:E) {
}
在这个例子中,我们定义了两个泛型占位符,T和E。在这种情况下,我们可以将T占位符设置为一种类型,将E占位符设置为另一种类型。
让我们看看如何调用一个泛型函数。以下代码将使用swapGeneric<T>(inout a: T, inout b: T)函数交换两个整数:
var a = 5
var b = 10
swap(&a, b: &b)
print("a: \(a) b: \(b)")
如果我们运行这段代码,控制台将打印出a: 10 b: 5这一行。我们可以看到,调用一个泛型函数不需要做任何特殊的事情。函数会从第一个参数推断类型,然后将所有剩余的占位符设置为该类型。现在,如果我们需要交换两个字符串的值,我们将以这种方式调用相同的函数:
var c = "My String 1"
var d = "My String 2"
swapGeneric(&c, b: &d)
print("c: \(c) d: \(d)")
我们可以看到,我们调用函数的方式与我们想要交换两个整数时调用的方式完全相同。我们无法做的一件事是将两种不同的类型传递给swap()函数,因为我们只定义了一个泛型占位符。如果我们尝试运行以下代码,我们会收到一个错误:
var a = 5
var c = "My String 1"
swapGeneric(&a, b: &c)
我们将收到的错误是cannot invoke 'swap' with an argument list of type '(inout Int, b: inout String),这告诉我们我们试图在函数只需要类型时使用一个字符串值和一个整数值。函数寻找 Int 值的原因是我们传递给函数的第一个参数是一个 Int 值;因此,函数中的所有泛型类型都变成了 Int 类型。
现在,假设我们有一个以下具有多个泛型类型定义的函数:
func testGeneric<T,E>(a:T, b:E) {
print("\(a) \(b)")
}
此函数将接受不同类型的参数;然而,由于它们的类型不同,我们将无法交换值,因为类型不同。泛型还有一些其他限制。例如,我们可能会认为以下泛型函数是有效的;然而,如果我们尝试实现它,我们会收到一个错误:
func genericEqual<T>(a: T, b: T) -> Bool{
return a == b
}
我们收到的错误是binary operator '==' cannot be applied to two 'T' operands。由于在编译代码时参数的类型是未知的,Swift 不知道它是否能够在这些类型上使用等号运算符;因此,抛出了错误。我们可能会认为这是一个将使泛型难以使用的限制;然而,我们有一种方法可以告诉 Swift 我们期望由占位符表示的类型将具有某些功能。这是通过类型约束来完成的。
类型约束指定了一个泛型类型必须从特定的类继承或符合特定的协议。这允许我们在泛型函数中使用由父类或协议定义的方法和属性。让我们通过重写genericEqual()函数以使用可比较协议来查看如何使用类型约束:
func testGenericComparable<T: Comparable>(a: T, b: T) -> Bool{
return a >= b
}
要指定类型约束,我们在泛型占位符之后放置类或协议约束,其中泛型占位符和约束由冒号分隔。这个新函数按我们预期的方式工作,并且它将比较两个参数的值,如果它们相等则返回true,如果不相等则返回false。
我们可以声明多个约束,就像我们声明多个泛型类型一样。以下示例展示了如何声明具有不同约束的两个泛型类型:
func testFunction<T: MyClass, E: MyProtocol>(a: T, b: E) {
}
在此函数中,由T占位符定义的类型必须继承自MyClass类,而由E占位符定义的类型必须实现MyProtocol协议。现在我们已经了解了泛型函数,让我们来看看泛型类型。
泛型类型
我们在查看 Swift 数组和字典时已经对泛型类型的工作方式有了总的介绍。泛型类型是一个类、结构体或枚举,它可以与任何类型一起工作,就像 Swift 数组和字典一样工作。回想起来,Swift 数组和字典被编写成可以包含任何类型。问题是我们在数组或字典中不能混合使用不同类型。当我们创建泛型类型的实例时,我们定义了实例将与之一起工作的类型。在定义了该类型之后,我们无法更改该实例的类型。
为了演示如何创建泛型类型,让我们创建一个简单的 List 类。这个类将使用 Swift 数组作为列表的后端存储,并允许我们向列表中添加项目或从列表中检索值。
让我们先看看如何定义我们的泛型列表类型:
class List<T> {
}
上述代码定义了泛型列表类型。我们可以看到,我们使用 <T> 标签来定义一个泛型占位符,就像我们在定义泛型函数时做的那样。这个 T 占位符可以然后在类型的任何地方使用,而不是具体的类型定义。
要创建此类型的实例,我们需要定义列表将包含的项目类型。以下示例展示了如何为各种类型创建泛型列表类型的实例:
var stringList = List<String>()
var intList = List<Int>()
var customList = List<MyObject>()
上述示例创建了 List 类的三个实例。stringList 实例可以用于 String 类型,intList 实例可以用于 Int 类型,而 customList 实例可以用于 MyObject 类型的实例。
我们不仅限于使用泛型与类一起。我们还可以将结构体和枚举定义为泛型。以下示例展示了如何定义泛型结构体和泛型枚举:
struct GenericStruct<T> {
}
enum GenericEnum<T> {
}
我们 List 类的下一步是添加后端存储数组。存储在这个数组中的项目需要与我们初始化类时定义的类型相同;因此,当我们定义数组的类型时,我们将使用 T 占位符。以下代码显示了带有名为 items 的数组的 List 类。items 数组将使用 T 占位符定义,因此它将包含我们为类定义的相同类型:
class List<T> {
var items = [T]()
}
此代码定义了我们的泛型列表类型,并使用 T 作为类型占位符。然后我们可以在类的任何地方使用 T 占位符来定义项目的类型。那个项目将是我们在创建 List 类的实例时定义的相同类型。因此,如果我们创建一个类似这样的列表类型实例 var stringList = List<String>(),项目数组将是一个字符串实例的数组。如果我们创建一个类似这样的列表类型实例 var intList = List<Int>(),项目数组将是一个 Int 实例的数组。
现在,我们需要添加一个addItems()方法,该方法将用于向列表中添加一个项目。我们将在方法声明中使用T占位符来定义项目参数将与我们在初始化类时声明的类型相同。因此,如果我们创建一个使用字符串类型的列表类型实例,我们就必须使用字符串类型作为addItems()方法的参数。然而,如果我们创建一个使用整型类型的列表类型实例,我们就必须使用整型类型作为addItems()方法的参数。
下面是addItems()函数的代码:
func addItem(item: T) {
items.append(item)
}
要创建一个独立的泛型函数,我们在函数名称后添加<T>声明来声明它是一个泛型函数;然而,当我们在一个泛型类型中使用泛型方法时,我们不需要<T>声明。相反,我们只需要使用我们在类声明中定义的类型。如果我们想引入另一个泛型类型,我们可以在方法声明中定义它。
现在,让我们添加一个getItemAtIndex()方法,该方法将从后端数组中返回指定索引的项目:
func getItemAtIndex(index: Int) -> T? {
if items.count > index {
return items[index]
} else {
return nil
}
}
getItemAtIndex()方法接受一个参数,即我们要检索的项目索引。然后我们使用T占位符来指定我们的返回类型是一个可能为T类型或nil的可选类型。如果后端存储数组在指定的索引处包含一个项目,我们将返回该项目;否则,我们不返回任何值。
现在,让我们看看我们的整个泛型列表类:
class List<T> {
var items = [T]()
func addItem(item: T) {
items.append(item)
}
func getItemAtIndex(index: Int) -> T? {
if items.count > index {
return items[index]
} else {
return nil
}
}
}
如我们所见,我们最初在类声明中定义了泛型T占位符类型。然后我们在类中使用这个占位符类型。在我们的List类中,我们在三个地方使用这个占位符。我们将其用作项目数组的类型,用作addItem()方法的参数类型,以及用作getItemAtIndex()方法中可选返回类型的关联值。
现在,让我们看看如何使用List类。当我们使用泛型类型时,我们将在类内部使用尖括号定义要使用的类型,例如<type>。以下代码显示了如何使用List类来存储字符串类型:
var list = List<String>()
list.addItem("Hello")
list.addItem("World")
print(list.getItemAtIndex(1))
在此代码中,我们首先创建一个名为list的列表类型实例,并将其设置为存储String类型。然后我们使用addItem()方法两次将两个项目存储在列表实例中。最后,我们使用getItemAtIndex()方法检索索引号为1的项目,它将在控制台显示Optional(World)。
我们还可以使用多个占位符类型定义我们的泛型类型,类似于我们在泛型方法中使用多个占位符的方式。要使用多个占位符类型,我们需要用逗号将它们分开。以下示例显示了如何定义多个占位符类型:
class MyClass<T,E>{
}
然后,我们创建一个使用String和Int类型的MyClass类型实例,例如:
var mc = MyClass<String, Int>()
我们还可以使用泛型类型的类型约束。再次强调,为泛型类型使用类型约束与为泛型函数使用类型约束完全相同。以下代码展示了如何使用类型约束来确保泛型类型符合可比较协议:
class MyClass<T: Comparable>{}
到目前为止,在本章中,我们已经看到了如何使用占位符类型与函数和类型一起使用。有时,在协议中声明一个或多个占位符类型可能很有用。这些类型被称为关联类型。
关联类型
关联类型声明了一个占位符名称,可以在协议中使用该名称代替类型。实际要使用的类型直到协议被采用时才指定。在创建泛型函数和类型时,我们使用了非常相似的语法。然而,为协议定义关联类型却非常不同。我们使用typealias关键字来指定关联类型。
当我们定义协议时,让我们看看如何使用关联类型。在这个例子中,我们将定义一个QueueProtocol协议,该协议将定义需要由实现它的队列实现的能力:
protocol QueueProtocol {
typealias QueueType
mutating func addItem(item: QueueType)
mutating func getItem() -> QueueType?
func count() -> Int
}
在这个协议中,我们定义了一个名为QueueType的关联类型。然后我们在协议中两次使用这个关联类型——一次作为addItem()方法的参数类型,一次当我们定义getItem()方法的返回类型为一个可能返回关联类型QueueType或nil的可选类型。
任何实现QueueProtocol协议的类型都必须能够指定用于QueueType占位符的类型,并且必须确保在协议中使用QueueType占位符的地方只使用该类型的项。
让我们看看如何在非泛型类IntQueue中实现QueueProtocol。这个类将使用Int类型实现QueueProtocol协议:
class IntQueue: QueueProtocol {
var items = [Int]()
func addItem(item: Int) {
items.append(item)
}
func getItem() -> Int? {
if items.count > 0 {
return items.removeAtIndex(0)
}
else {
return nil
}
}
func count() -> Int {
return items.count
}
}
在IntQueue类中,我们首先定义我们的后端存储机制为一个Int类型的数组。然后我们实现协议中定义的每个方法,用 Int 类型替换协议中定义的QueueType占位符。在addItem()方法中,参数类型被定义为Int类型,而在getItem()方法中,返回类型被定义为可能返回Int类型或无值的可选类型。
我们使用IntQueue类的方式就像使用任何其他类一样。以下代码展示了这一点:
var intQ = IntQueue()
intQ.addItem(2)
intQ.addItem(4)
print(intQ.getItem())
intQ.addItem(6)
我们首先创建一个名为intQ的IntQueue类实例。然后我们调用addItem()方法两次,向intQ实例添加两个整型的值。然后我们通过调用getItem()方法检索intQ实例中的第一个项。这一行将打印数字Optional(2)到控制台。代码的最后一行添加了另一个整型到intQ实例。
在前面的示例中,我们以非泛型的方式实现了QueueProtocol协议。这意味着我们用实际类型替换了占位符类型(QueueType被替换为Int类型)。我们还可以使用泛型类型实现QueueProtocol协议。让我们看看如何以泛型类型GenericQueue实现QueueProtocol协议:
class GenericQueue<T>: QueueProtocol {
var items = [T]()
func addItem(item: T) {
items.append(item)
}
func getItem() -> T? {
if items.count > 0 {
return items.removeAtIndex(0)
} else {
return nil
}
}
func count() -> Int {
return items.count
}
}
如我们所见,GenericQueue的实现与IntQueue的实现非常相似,除了我们定义了要使用的泛型占位符T。然后我们可以像使用任何泛型类一样使用GenericQueue类。让我们看看如何使用GenericQueue类:
var intQ2 = GenericQueue<Int>()
intQ2.addItem(2)
intQ2.addItem(4)
print(intQ2.getItem())
intQ2.addItem(6)
我们首先创建一个GenericQueue类的实例,该实例将使用Int类型。这个实例被命名为intQ2。接下来,我们调用addItem()方法两次,向intQ2实例添加两个Int类型。然后我们使用getItem()方法检索添加的第一个Int类型,并将其值打印到控制台。这一行将在控制台打印数字2。
在使用泛型时,我们应该注意避免在应该使用协议的情况下使用泛型。在我看来,这是在其他语言中泛型最常见误用的一个例子。让我们看看一个例子,以便我们知道要避免什么。
假设我们定义了一个名为WidgetProtocol的协议,如下所示:
protocol WidgetProtocol {
//Code
}
现在,假设我们想要创建一个自定义类型(或函数),该类型将使用WidgetProtocol协议的各种实现。我见过一些开发者使用类型约束与泛型一起创建类似这样的自定义类型:
class MyClass<T: WidgetProtocol> {
var myProp: T?
func myFunc(myVar: T) {
//Code
}
}
虽然这是泛型的一个完全有效的使用,但我们建议避免这种实现方式。如果我们使用WidgetProtocol而不使用泛型,代码会更加清晰且易于阅读。例如,我们可以这样编写MyClass类型的非泛型版本:
class MyClass {
var myProp: WidgetProtocol?
func myFunc(myVar: WidgetProtocol) {
}
}
MyClass类型的第二个非泛型版本更容易阅读和理解;因此,这应该是实现类的首选方式。然而,没有任何阻止我们使用MyClass类型的任何实现。
摘要
泛型类型可以非常有用,它们也是 Swift 标准集合类型(数组和字典)的基础;然而,正如本章引言中提到的,我们必须小心正确地使用它们。
我们在本章中看到了几个示例,展示了泛型如何让我们的生活变得更简单。本章开头展示的swapGeneric()函数是一个泛型函数的良好应用,因为它允许我们只实现一次交换代码,就能交换任何类型我们选择的两值。
通用列表类型也是一个很好的例子,说明了如何创建自定义集合类型,这些类型可以用来存储任何类型的元素。在本章中,我们实现通用列表类型的方式与 Swift 实现泛型数组和大字典的方式相似。
第十二章。使用闭包
现在,大多数主要的编程语言都有类似于闭包提供的功能。其中一些实现非常难以使用(Objective-C blocks),而其他则很容易(Java lambda 和 C# delegates)。我发现闭包提供的功能在开发框架时特别有用。我还在通过网络连接与远程服务通信时广泛使用了它们。虽然 Objective-C 中的 blocks 非常有用(我相当多地使用了它们),但它们用于声明 block 的语法绝对糟糕。幸运的是,当 Apple 开发 Swift 语言时,他们使闭包的语法更容易使用和理解。
在本章中,我们将涵盖以下主题:
-
闭包简介
-
定义闭包
-
使用闭包
-
闭包的一些有用示例
-
如何避免闭包中的强引用循环
闭包简介
闭包是自包含的代码块,可以在我们的应用程序中传递和使用。我们可以将 int 类型视为存储整数的类型,而将 string 类型视为存储字符串的类型。在这种情况下,闭包可以被视为一个包含代码块的类型。这意味着我们可以将闭包赋值给变量,将它们作为函数的参数传递,也可以从它们中返回函数。
闭包能够捕获并存储它们定义上下文中任何变量或常量的引用。这被称为覆盖变量或常量,最好的是,在大多数情况下,Swift 会为我们处理内存管理。唯一的例外是我们创建强引用循环,我们将在本章的 创建闭包中的强引用循环 部分中探讨如何解决这个问题。
Swift 中的闭包与 Objective-C 中的 blocks 类似;然而,Swift 中的闭包更容易使用和理解。让我们看看定义 Swift 中闭包所使用的语法:
{
(parameters) -> return-type in
statements
}
如我们所见,创建闭包所使用的语法与我们在 Swift 中创建函数所使用的语法非常相似,实际上,在 Swift 中,全局和嵌套函数都是闭包。闭包和函数之间格式上的最大区别是 in 关键字。in 关键字用于代替花括号,将闭包的参数和返回类型的定义与闭包体部分隔开。
闭包有许多用途,我们将在本章后面详细讨论它们,但首先我们需要了解闭包的基本知识。让我们先看看一些非常基础的闭包用法,以便我们更好地理解它们是什么,如何定义它们,以及如何使用它们。
简单闭包
我们将从创建一个非常简单的闭包开始,这个闭包不接受任何参数,也不返回任何值。它所做的只是将 Hello World 打印到控制台。让我们看看以下代码:
let clos1 = {
() -> Void in
print("Hello World")
}
在这个例子中,我们创建了一个闭包并将其赋值给常量 clos1。由于括号之间没有定义任何参数,这个闭包将不接受任何参数。此外,返回类型被定义为 Void;因此,这个闭包不会返回任何值。闭包的主体包含一行代码,将 Hello World 打印到控制台。
使用闭包的方法有很多;在这个例子中,我们只想执行它。我们将像这样执行这个闭包:
clos1()
当我们执行闭包时,我们会看到 Hello World 被打印到控制台。在这个时候,闭包可能看起来并不那么有用,但随着我们继续阅读本章,我们将看到它们是多么有用和强大。
让我们看看另一个简单的闭包示例。这个闭包将接受一个名为 name 的字符串参数,但仍然不会返回任何值。在闭包的主体内部,我们将通过 name 参数打印出对传递给闭包的名称的问候语。以下是第二个闭包的代码:
let clos2 = {
(name: String) -> Void in
print("Hello \(name)")
}
与这个例子中定义的 clos2 和之前的 clos1 闭包相比,最大的不同之处在于我们在闭包中定义了一个单独的字符串参数。正如我们所看到的,我们定义闭包参数的方式就像定义函数参数一样。
我们可以用执行 clos1 的相同方式执行这个闭包。以下代码显示了如何做到这一点:
clos2("Jon")
当执行这个例子时,它将在控制台打印消息 Hello Jon。让我们看看另一种使用 clos2 闭包的方法。
我们对闭包的原始定义是,“闭包是自包含的代码块,可以在我们的应用程序代码中传递和使用”。这告诉我们,我们可以将我们的闭包从它们被创建的上下文中传递到代码的其他部分。让我们看看如何将我们的 clos2 闭包传递给一个函数。我们将定义一个接受我们的 clos2 闭包的函数,如下所示:
func testClosure(handler:(String)->Void) {
handler("Dasher")
}
我们定义函数的方式就像定义任何其他函数一样;然而,在我们的参数列表中,我们定义了一个名为 handler 的参数,并为 handler 参数定义的类型是 (String)->Void。如果我们仔细观察,我们可以看到 handler 参数的 (String)->Void 定义与为我们定义的 clos2 闭包的参数和返回类型相匹配。这意味着我们可以将 clos2 闭包传递给函数。让我们看看如何做到这一点:
testClosure(clos2)
我们像调用任何其他函数一样调用 testClosure() 函数,传入的闭包看起来就像任何其他变量。由于 clos2 闭包在 testClosure() 函数中执行,因此当这段代码执行时,我们将看到消息 Hello Dasher 被打印到控制台。
正如我们将在本章稍后看到的那样,将闭包传递给函数的能力使得闭包变得如此令人兴奋和强大。
作为闭包问题的最后一部分,让我们看看如何从闭包中返回一个值。以下示例展示了这一点:
let clos3 = {
(name: String) -> String in
return "Hello \(name)"
}
clos3 闭包的定义看起来与我们定义 clos2 闭包的方式非常相似。区别在于我们将 Void 返回类型更改为 String 类型。然后,在闭包的主体中,我们不是将消息打印到控制台,而是使用返回语句返回消息。现在我们可以像前两个闭包一样执行 clos3 闭包,或者像 clos2 闭包一样将其传递给函数。以下示例展示了如何执行 clos3 闭包:
var message = clos3("Buddy")
执行这一行代码后,message 变量将包含 Hello Buddy 字符串。
之前的三个闭包示例展示了闭包的格式以及如何定义一个典型的闭包。熟悉 Objective-C 的人可以注意到 Swift 中闭包的格式要干净得多,也更容易使用。在本章中我们展示的创建闭包的语法相当简短;然而,我们还可以进一步缩短它。在接下来的这一节中,我们将探讨如何做到这一点。
闭包的简写语法
在本节中,我们将探讨几种缩短闭包定义的方法。
注意
使用闭包的简写语法完全是个人喜好问题。有很多开发者喜欢把他们的代码写得尽可能小和紧凑,并且为此感到非常自豪。然而,有时这可能会让其他开发者难以阅读和理解代码。
我们将要查看的第一个闭包简写语法是最受欢迎的,也是我们在使用数组中的算法时看到的语法,即 第三章,使用集合和 Cocoa 数据类型。这种格式主要在我们想要向函数发送一个非常小的闭包(通常是一行)时使用,就像我们使用数组算法那样。在我们查看这种简写语法之前,我们需要编写一个函数,该函数将接受一个闭包作为参数:
func testFunction(num: Int, handler:()->Void) {
for var i=0; i < num; i++ {
handler()
}
}
这个函数接受两个参数——第一个参数是一个名为 num 的整数,第二个参数是一个名为 handler 的闭包,该闭包没有参数且不返回任何值。在函数内部,我们创建一个 for 循环,该循环将使用 num 整数来定义循环的次数。在 for 循环中,我们调用传递给函数的 handler 闭包。
我们可以创建一个闭包,并将其像这样传递给 testFunction():
let clos = {
() -> Void in
print("Hello from standard syntax")
}
testFunction(5,handler: clos)
这段代码非常容易阅读和理解;然而,它需要五行代码。现在,让我们看看如何通过在函数调用内编写闭包来缩短这段代码:
testFunction(5,handler: {print("Hello from Shorthand closure")})
在这个示例中,我们使用与数组算法相同的语法,在函数调用内联创建闭包。闭包放置在两个花括号({})之间,这意味着创建我们的闭包的代码是 {print("Hello from Shorthand closure")}。当这段代码执行时,它将在屏幕上打印出消息,Hello from Shorthand closure,五次。
在 第三章,使用集合和 Cocoa 数据类型 中,我们看到了我们可以使用 $0、$1、$2 等参数将参数传递给数组算法。让我们看看如何使用简写语法来使用参数。我们将首先创建一个新的函数,该函数将接受一个带有单个参数的闭包。我们将把这个函数命名为 testFunction2。以下示例显示了新的 testFunction2 函数的作用:
func testFunction2(num: Int, handler:(name: String)->Void) {
for var i=0; i < num; i++ {
handler(name: "Me")
}
}
在 testFunction2 函数中,我们这样定义我们的闭包:(name: String)->Void。这个定义意味着闭包接受一个参数并且不返回任何值。现在,让我们看看如何使用相同的简写语法来调用这个函数:
testFunction2(5,handler: {print("Hello from \($0)")})
这个闭包定义与上一个定义之间的区别在于 $0。$0 参数是传递给函数的第一个参数的简写。如果我们执行这段代码,它将打印出消息,Hello from Me,五次。
使用美元符号($)后跟一个数字的简写语法允许我们在不将参数列表放入定义的情况下定义闭包。美元符号后面的数字定义了参数在参数列表中的位置。让我们更详细地检查这个格式,因为我们不仅限于在简写闭包中使用美元符号($)和数字简写格式。这种简写语法还可以用来缩短闭包定义,允许我们省略参数名称。以下示例演示了这一点:
let clos5: (String, String) ->Void = {
print("\($0) \($1)")
}
在这个示例中,我们的闭包定义了两个字符串参数,但我们没有给它们命名。参数的定义如下:(String, String)。我们可以在闭包体内部使用 $0 和 $1 访问这些参数。此外,请注意,闭包的定义是在冒号(:)之后,使用与定义变量类型相同的语法,而不是在花括号内。当我们使用匿名参数时,这就是我们定义闭包的方式。以下定义方式是不正确的:
let clos5b = {
(String, String) -> Void in
print("\($0) \($1)")
}
在这个示例中,我们将收到 匿名闭包参数不能在具有显式参数的闭包中使用 错误。
我们将像这样使用 clos5 闭包:
clos5("Hello","Kara")
由于 Hello 是参数列表中的第一个字符串,因此它使用 $0 访问,而 Kara 是参数列表中的第二个字符串,因此它使用 $1 访问。当我们执行这段代码时,我们将看到消息,Hello Kara,打印到控制台。
在这个下一个例子中,当闭包不返回任何值时使用。我们不必将返回类型定义为 Void,我们可以使用括号,如下例所示:
let clos6: () -> () = {
print("Howdy")
}
在这个例子中,我们定义闭包为 () -> ()。这告诉 Swift 该闭包不接受任何参数,也不返回任何值。我们将这样执行这个闭包:
clos6()
在我们开始展示一些有用的闭包示例之前,我们还有一个简写闭包示例要演示。在这个最后的例子中,我们将演示如何在不需要包含单词return的情况下从闭包中返回一个值。
如果整个闭包体只包含一个语句,那么我们可以省略 return 关键字,并且该语句的结果将被返回。让我们看看一个这样的例子:
let clos7 = {
(first: Int, second: Int) -> Int in
first + second
}
在这个例子中,闭包接受两个 Int 类型的参数,并将返回 Int 类型的值。闭包体内的唯一语句是将第一个参数加到第二个参数上。然而,如果你注意到,我们在加法语句之前没有包含 return 关键字。Swift 会看到这是一个单语句闭包,并将自动返回结果,就像我们在加法语句之前放置了 return 关键字一样。我们确实需要确保我们语句的结果类型与闭包的返回类型相匹配。
在前两个部分中展示的所有示例都是为了展示如何定义和使用闭包。单独来看,这些示例并没有真正展示闭包的力量,也没有展示闭包是多么有用。本章的剩余部分旨在展示 Swift 中闭包的力量和实用性。
使用 Swift 的数组算法与闭包
在第三章中,我们探讨了几个可以与 Swift 的数组一起使用的内置算法。在第三章中,我们简要地看到了如何使用非常基本的闭包为这些算法中的每一个添加简单的规则。现在,我们对闭包有了更好的理解,让我们看看如何使用更高级的闭包来扩展这些算法。
在本节中,我们将主要使用 map 算法以保持一致性;然而,我们可以使用任何算法中展示的基本思想。我们将首先定义一个数组来使用:
let guests = ["Jon", "Kim", "Kailey", "Kara"]
这个数组包含一个名字列表,这个数组被命名为 guests。这个数组将在本节的所有示例中使用,除了最后几个。
现在我们有了 guests 数组,让我们添加一个闭包,该闭包将打印一个问候语到 guests 数组中的每个名字:
guests.map({
(name: String) -> Void in
print("Hello \(name)")
})
由于映射算法将闭包应用于数组中的每个项目,因此此示例将为guests数组中的每个名称打印一条问候语。在本章的第一部分之后,我们应该对闭包是如何工作的有一个相当好的理解。使用我们在上一节中看到的简写语法,我们可以将前面的示例简化为以下单行代码:
guests.map({print("Hello \($0)")})
在我看来,这可能是少数几个简写语法可能比标准语法更容易阅读的情况之一。
现在,假设我们不想将问候语打印到控制台,而是想返回一个包含问候语的新数组。为此,我们将在闭包中返回一个字符串类型,如下面的示例所示:
var messages = guests.map({
(name:String) -> String in
return "Welcome \(name)"
})
当此代码执行时,messages数组将包含对guests数组中每个名称的问候,而guests数组将保持不变。
本节前面的示例展示了如何将闭包内联添加到映射算法中。如果我们只想使用一个闭包与映射算法一起,这是很好的;但如果我们有多个闭包想要使用,或者我们想要多次使用闭包或用不同的数组重用它们,我们应该怎么做呢?为此,我们可以将闭包分配给一个常量或变量,然后根据需要使用其常量或变量名称传递闭包。让我们看看如何做到这一点。我们将首先定义两个闭包。其中一个闭包将为guests数组中的每个名称打印一条问候语,另一个闭包将为guests数组中的每个名称打印一条告别语:
let greetGuest = {
(name:String) -> Void in
print("Hello guest named \(name)")
}
let sayGoodbye = {
(name:String) -> Void in
print("Goodbye \(name)")
}
现在我们有了两个闭包,我们可以根据需要使用它们与映射算法一起。以下代码显示了如何将这些闭包与guests数组交互使用:
guests.map(greetGuest)
guests.map(sayGoodbye)
无论何时我们使用greetGuest闭包与guests数组一起,问候消息都会打印到控制台;无论何时我们使用sayGoodbye闭包与guests数组一起,告别消息都会打印到控制台。如果我们还有一个名为guests2的数组,我们可以使用相同的闭包来处理该数组,如下面的示例所示:
guests.map(greetGuest)
guests2.map(greetGuest)
guests.map(sayGoodbye)
guests2.map(sayGoodbye)
在本节中,到目前为止的所有示例要么是将消息打印到控制台,要么是从闭包返回一个新数组。我们的闭包并不局限于这样的基本功能。例如,我们可以在闭包内过滤数组,如下面的示例所示:
let greetGuest2 = {
(name:String) -> Void in
if (name.hasPrefix("K")) {
print("\(name) is on the guest list")
} else {
print("\(name) was not invited")
}
}
在此示例中,我们根据名称是否以字母 K 开头打印不同的消息。
如我们本章前面提到的,闭包具有捕获并存储它们定义的上下文中任何变量或常量的引用的能力。让我们来看一个这样的例子。假设我们有一个函数,它包含了一个特定位置过去七天的最高温度,并且这个函数接受一个闭包作为参数。这个函数将在温度数组上执行闭包。这个函数可以写成这样:
func temperatures(calculate:(Int)->Void) {
var tempArray = [72,74,76,68,70,72,66]
tempArray.map(calculate)
}
这个函数接受一个定义为 (Int)->Void 的闭包。然后我们使用映射算法为 tempArray 数组中的每个项目执行这个闭包。在这种情况下正确使用闭包的关键是理解 temperatures 函数并不知道或关心 calculate 闭包内部发生的事情。此外,请注意,闭包也无法更新或更改函数上下文中的项目,这意味着闭包不能更改温度函数中的任何其他变量;然而,它可以更新它在创建的上下文中的变量。
让我们看看我们将创建闭包的函数。我们将把这个函数命名为 testFunction。让我们看一下以下代码:
func testFunction() {
var total = 0
var count = 0
let addTemps = {
(num: Int) -> Void in
total += num
count++
}
temperatures(addTemps)
print("Total: \(total)")
print("Count: \(count)")
print("Average: \(total/count)")
}
在这个函数中,我们首先定义了两个名为 total 和 count 的变量,这两个变量都是整数类型。然后我们创建了一个名为 addTemps 的闭包,它将被用来将 temperatures 函数中的所有温度相加。addTemps 闭包还将计算数组中有多少个温度。为此,addTemps 闭包计算数组中每个项目的总和,并将总和保存在函数开头定义的 total 变量中。addTemps 闭包还通过为每个项目递增 count 变量来跟踪数组中的项目数量。请注意,total 和 count 变量都不是在闭包内部定义的;然而,我们能够在闭包中使用它们,因为它们是在与闭包相同的上下文中定义的。
然后,我们调用 temperatures 函数,并传递 addTemps 闭包。最后,我们将总温度、计数和平均温度打印到控制台。当 testFunction 执行时,我们会在控制台看到以下输出:
Total: 498
Count: 7
Average: 71
如我们从输出中可以看到的,addTemps 闭包能够在创建它的上下文中更新和使用定义的项目,即使闭包在另一个上下文中使用。
现在我们已经探讨了使用闭包与数组映射算法相结合的方法,让我们来看看如何单独使用闭包。我们还将探讨我们可以采取的方法来清理我们的代码,使其更容易阅读和使用。
独立闭包和良好的风格指南
闭包让我们能够真正地将代码中的数据部分与用户界面和业务逻辑部分分离。这使我们能够创建仅关注数据检索的可重用类。这对于开发旨在从外部服务(如网络服务、数据库或文件)检索数据的类和框架尤其有用。本节将展示如何开发一个类,当数据准备好返回时,将执行一次闭包。
让我们从创建一个包含代码数据部分的类开始。在这个例子中,这个类将被命名为 Guests,它将包含一个嘉宾名字的数组。让我们看一下以下代码:
class Guests {
var guestNames = ["Jon","Kim","Kailey","Kara","Buddy","Lily","Dash"]
typealias UseArrayClosure = [String] -> Void
func getGuest(handler:UseArrayClosure) {
handler(guestNames)
}
}
Guests 类的第一行定义了一个名为 guestNames 的数组。guestNames 数组包含七个名字。在我们定义 guestNames 数组之后,我们接着创建一个类型别名。类型别名定义了一个现有类型的命名别名。就像函数一样,闭包有由参数类型和返回类型组成的类型,这些类型可以被别命名。这允许我们定义一次闭包,然后在代码的任何地方使用这个别名。使用类型别名可以减少我们需要输入的代码量,并防止错误。因此,建议我们使用它们,而不是在代码中多次重写闭包定义。它还允许我们在一个位置更改定义,然后整个代码将更新。
在这个例子中,我们的类型别名被命名为 UseArrayClosure,它被定义为一个接受一个字符串数组作为唯一参数且不返回任何值的闭包。现在我们可以在整个代码中使用这个类型别名作为闭包定义的简写。
最后,我们定义了一个 getGuest() 方法,它接受一个名为 handler 的闭包作为其唯一参数。在 getGuests() 方法内部,我们唯一要做的事情就是执行这个处理器。通常,在这个方法中,我们将有从外部数据源检索数据的逻辑;然而,在这个例子中,我们有一个硬编码的包含嘉宾名单的数组。因此,我们只需要使用 guestsNames 数组作为唯一参数来执行这个闭包。
现在,假设我们想在 UITableView 视图中显示这个名字数组。UITableView 是一个 iOS 视图,用于显示信息列表。在视图控制器中,我们需要创建一个数组来存储在 UITableView 中显示的数据,以及一个变量来链接到显示中的 UITableView。这两个都将是我们视图控制器类中的类变量,并且它们的定义如下:
@IBOutlet var tableView:UITableView?
var tableData: [String]?
现在,让我们创建一个名为 getData() 的函数,它将用于检索嘉宾列表并更新表格视图:
func getData() {
let dataClosure: Guests.UseArrayClosure = {
self.tableData = $0
if let tView = self.tableView {
tView.reloadData()
}
}
let guests = Guests()
guests.getGuest(dataClosure)
}
我们从定义一个名为dataClosure的闭包开始getData()函数。这个闭包使用了我们在Guests类中定义的UseArrayClosure类型别名来定义闭包。在闭包定义中,我们将定义在视图控制器本身内部(而不是在闭包中)的tableData数组设置为传递给闭包的字符串数组。然后我们验证tableView变量是否包含UITableView类的实例,如果是,我们重新加载数据。最后,我们创建一个Guests类的实例,并通过传递dataClosure闭包来调用getGuest()方法。
请记住,定义名称列表的guestNames数组是在Guest类中定义的,而tableView、UITableView和tableData数组是在视图控制器类中定义的。
当将dataClosure闭包传递给getGuests()方法时,它将从Guests类中加载名称数组到tableData数组中。然后tableData数组在视图控制器类中使用,作为UITableView数组的数据元素。在这个例子中需要注意的关键点是,我们能够将一个上下文(Guests类)中的数据加载到与闭包定义相同的上下文(视图控制器)中的变量,并且还有能力在定义在闭包相同上下文中的类的实例上调用方法(tableView和UITableView)。
我们很容易在Guest类中创建一个返回guestNames数组的方法。对于像Guest类中那样的硬编码数组,这个方法会工作得很好。然而,如果我们从需要一些时间来加载的 Web 服务加载数据,这将不会很好地工作,因为我们的 UI 会在等待数据加载时冻结。通过使用本例中所示的方法,我们可以异步调用 Web 服务,然后当数据返回时,闭包将被执行,UI 会自动更新而不会冻结我们的 UI。
注意
这本书主要是为了教授 Swift 语言,而不是专门针对 iOS 开发;因此,我们在这个例子中不涵盖 Cocoa Touch 框架中的 UI 元素是如何工作的。如果您想看到完整的 iOS 示例,请下载本书的代码示例。
功能更改
闭包还赋予我们动态更改类功能的能力。我们在第十一章中看到,使用泛型,泛型赋予我们编写适用于多种类型的函数的能力。通过闭包,我们能够编写功能和闭包作为参数传递给它时可以改变的功能的函数和类。在本节中,我们将展示如何编写一个可以通过闭包改变功能的函数。
让我们先定义一个类,用于演示如何替换功能。我们将这个类命名为 TestClass:
class TestClass {
typealias getNumClosure = ((Int, Int) -> Int)
var numOne = 5
var numTwo = 8
var results = 0
func getNum(handler: getNumClosure) -> Int {
results = handler(numOne,numTwo)
return results
}
}
我们从这个类开始,定义一个名为 getNumClosure 的闭包类型别名。任何定义为 getNumClosure 闭包的闭包都将接受两个整数并返回一个整数。在这个闭包中,我们假设它会对我们传入的整数做些处理以得到返回的值,但实际上并不需要。说实话,这个类并不关心闭包做了什么,只要它符合 getNumClosure 类型。接下来,我们定义三个名为 numOne、NumTwo 和 results 的整数。
最后,我们定义一个名为 getNum() 的方法。该方法接受一个确认 getNumClosure 类型的闭包作为其唯一参数。在 getNum() 方法中,我们通过传递 numOne 和 numTwo 类变量以及整数来执行闭包,返回的整数被放入 results 类变量中。
现在,让我们看看几个符合 getNumClosure 类型的闭包,我们可以使用这些闭包与 getNum() 方法一起使用:
var max: TestClass.getNumClosure = {
if $0 > $1 {
return $0
} else {
return $1
}
}
var min: TestClass.getNumClosure = {
if $0 < $1 {
return $0
} else {
return $1
}
}
var multiply: TestClass.getNumClosure = {
return $0 * $1
}
var second: TestClass.getNumClosure = {
return $1
}
var answer: TestClass.getNumClosure = {
var tmp = $0 + $1
return 42
}
在此代码中,我们定义了五个符合 getNumClosure 类型的闭包:
-
max: 这返回传入的两个整数的最大值 -
min: 这返回传入的两个整数的最小值 -
multiply: 这将传入的两个值相乘并返回乘积 -
second: 这返回传入的第二个参数 -
answer: 这返回生命、宇宙和万物的答案
在 answer 闭包中,有一行看起来好像没有作用:var tmp = $0 + $1。我们故意这样做,因为以下代码是不合法的:
var answer: TestClass.getNumClosure = {
return 42
}
这个类给我们提供了 error: tuple types '(Int, Int)' and '()' have a different number of elements (2 vs. 0) 错误。正如错误所示,Swift 认为除非我们在闭包体中使用 $0 和 $1,否则我们的闭包不接受任何参数。在名为 second 的闭包中,Swift 假设有两个参数,因为 $1 指定了第二个参数。
现在,我们可以将这些闭包逐个传递给我们的 TestClass 的 getNum 方法,以改变函数的功能以适应我们的需求。以下代码说明了这一点:
var myClass = TestClass()
myClass.getNum(max)
myClass.getNum(min)
myClass.getNum(multiply)
myClass.getNum(second)
myClass.getNum(answer)
当运行此代码时,我们将为每个闭包收到以下结果:
-
max: 结果 = 8 -
min: 结果 = 5 -
multiply: 结果 = 40 -
second: 结果 = 8 -
answer: 结果 = 42
本章将要展示的最后一个示例是在框架中经常使用的一个,尤其是在那些设计为异步运行的功能中。
根据结果选择闭包
在最后的例子中,我们将向一个方法传递两个闭包,然后根据某些逻辑,执行其中一个或两个闭包。通常情况下,如果方法成功执行,则调用其中一个闭包;如果方法失败,则调用另一个闭包。
让我们从创建一个类开始,这个类将包含一个方法,该方法将接受两个闭包,并根据定义的逻辑执行其中一个闭包。我们将这个类命名为TestClass。以下是TestClass类的代码:
class TestClass {
typealias ResultsClosure = ((String) -> Void)
func isGreater(numOne: Int, numTwo:Int, successHandler: ResultsClosure, failureHandler: ResultsClosure) {
if numOne > numTwo {
successHandler("\(numOne) is greater than \(numTwo)")
}
else {
failureHandler("\(numOne) is not greater than \(numTwo)")
}
}
}
我们从这个类开始创建一个类型别名,该别名定义了我们将在成功和失败闭包中使用的闭包。我们将这个类型别名命名为ResultsClosure。这个例子还将说明为什么使用类型别名而不是重新输入闭包定义可以节省我们大量的输入,并防止我们出错。在这个例子中,如果我们没有使用类型别名,我们就需要四次重新输入闭包定义,如果我们需要更改闭包定义,我们就需要在四个地方进行更改。使用类型别名,我们只需要输入一次闭包定义,然后在剩余的代码中使用别名。
我们接着创建一个名为isGreater的方法,该方法接受两个整数作为前两个参数,然后接受两个闭包作为接下来的两个参数。第一个闭包命名为successHandler,第二个闭包命名为failureHandler。在isGreater方法内部,我们检查第一个整数参数是否大于第二个参数。如果第一个整数大于第二个,则执行successHandler闭包;否则,执行failureHandler闭包。
现在,让我们创建两个闭包。这两个闭包的代码如下:
var success: TestClass. ResultsClosure = {
print("Success: \($0)")
}
var failure: TestClass. ResultsClosure = {
print("Failure: \($0)")
}
注意,这两个闭包都被定义为TestClass.ResultsClosure类型。在每个闭包中,我们只是简单地打印一条消息到控制台,让我们知道哪个闭包被执行。通常情况下,我们会在闭包中放置一些功能。
然后,我们将像这样使用两个闭包调用该方法:
var test = TestClass()
test.isGreater(8, numTwo: 6, successHandler:success, failureHandler:failure)
注意,在方法调用中,我们发送了成功闭包和失败闭包。在这个例子中,我们会看到消息,“成功:8 大于 6”。如果我们反转数字,我们会看到消息,“失败:6 不大于 8”。这种用法在调用异步方法时非常好,例如从网络服务加载数据。如果网络服务调用成功,则调用成功闭包;否则,调用失败闭包。
使用这种闭包的一个大优点是,在等待网络服务调用完成时,UI 不会冻结。这也涉及到并发部分,我们将在本书的第十四章,Swift 中的并发与并行中稍后进行介绍。作为一个例子,如果我们尝试以这种方式从网络服务检索数据:
var data = myWebClass.myWebServiceCall(someParameter)
当我们等待响应返回时,我们的 UI 会冻结,或者我们必须在单独的线程中发起调用,这样 UI 就不会挂起。使用闭包,我们将闭包传递给网络框架,并依赖于框架在完成时执行适当的闭包。这确实依赖于框架正确实现并发来异步调用,但一个不错的框架应该为我们处理这一点。
使用闭包创建强引用循环
在本章的早期,我们说,“最好的是,大多数情况下,Swift 会为我们处理内存管理”。这句话中的“大多数情况下”部分意味着,如果一切按照标准方式编写,Swift 会为我们处理闭包的内存管理。然而,就像类一样,有时内存管理会让我们失望。到目前为止,本章中我们看到的所有示例的内存管理都将正常工作。有可能创建一个强引用循环,这将阻止 Swift 的内存管理正常工作。让我们看看如果我们使用闭包创建强引用循环会发生什么。
如果我们将一个闭包分配给类实例的一个属性,并在该闭包中捕获该类的实例,可能会发生强引用循环。这种捕获发生是因为我们使用self访问该特定实例的属性,例如self.someProperty,或者将self赋值给一个变量或常量,如let c = self。通过捕获实例的属性,我们实际上捕获了实例本身,从而创建了一个强引用循环,内存管理器将不知道何时释放实例。结果,内存将无法正确释放。
让我们从创建一个具有闭包和字符串类型实例作为其两个属性的类开始。我们还将在这个类中创建闭包类型的别名,并定义一个deinit()方法,该方法将打印一条消息到控制台。deinit()方法在类被释放和内存释放时被调用。当deinit()方法的消息打印到控制台时,我们将知道类何时被释放。这个类将被命名为TestClassOne。让我们看一下以下代码:
class TestClassOne {
typealias nameClosure = (() -> String)
var name = "Jon"
lazy var myClosure: nameClosure = {
return self.name
}
deinit {
print("TestClassOne deinitialized")
}
}
现在,让我们创建第二个类,该类将包含一个接受nameClosure类型闭包的方法,该闭包是在TestClassOne类中定义的。这个类也将有一个deinit()方法,这样我们也可以看到它何时被释放。我们将这个类命名为TestClassTwo。让我们看一下以下代码:
class TestClassTwo {
func closureExample(handler: TestClassOne.nameClosure) {
print(handler())
}
deinit {
print("TestClassTwo deinitialized")
}
}
现在,让我们通过创建每个类的实例,然后尝试通过将它们设置为nil来手动释放实例,来看看这段代码的实际效果:
var testClassOne: TestClassOne? = TestClassOne()
var testClassTwo: TestClassTwo? = TestClassTwo()
testClassTwo?.closureExample(testClassOne!.myClosure)
testClassOne = nil
print("testClassOne is gone")
testClassTwo = nil
print("testClassTwo is gone")
在这段代码中,我们创建了两个可选变量,它们可能包含我们的两个测试类的实例或nil。我们需要将这些变量创建为可选的,因为我们在代码的后面将它们设置为nil,这样我们就可以看到实例是否被正确释放。
然后我们调用TestClassTwo实例的closureExample()方法,并将TestClassOne实例的myClosure属性传递给它。我们现在尝试通过将它们设置为nil来释放TestClassOne和TestClassTwo实例。记住,当一个类的实例被释放时,如果存在,它会尝试调用该类的deinit()方法。在我们的例子中,两个类都有一个deinit()方法,它会向控制台打印一条消息,这样我们就可以知道实例是否真正被释放了。
如果我们运行这个项目,我们将看到以下消息打印到控制台:
testClassOne is gone
TestClassTwo deinitialized
testClassTwo is gone
如我们所见,我们确实尝试释放TestClassOne实例,但类的deinit()方法从未被调用,这表明它实际上并没有被释放;然而,TestClassTwo实例被正确释放,因为那个类的deinit()方法被调用了。
要查看在没有强引用循环的情况下它是如何工作的,将myClosure闭包更改为返回在闭包内部定义的字符串类型,如下所示:
lazy var myClosure: nameClosure = {
return "Just Me"
}
现在,如果我们运行项目,我们应该看到以下输出:
TestClassOne deinitialized
testClassOne is gone
TestClassTwo deinitialized
testClassTwo is gone
这表明TestClassOne和TestClassTwo实例的deinit()方法都得到了正确调用,表明它们都被正确释放了。
在第一个例子中,我们在闭包中捕获了TestClassOne类的一个实例,因为我们使用self.name访问了TestClassOne类的一个属性。这从闭包到TestClassOne类的一个实例创建了一个强引用,阻止了内存管理释放该实例。
Swift 确实提供了一个非常简单且优雅的方式来解决闭包中的强引用循环。我们只需通过创建捕获列表来告诉 Swift 不要创建强引用。捕获列表定义了在闭包中捕获引用类型时要使用的规则。我们可以声明每个引用为弱引用或unowned引用,而不是强引用。
当在引用的生命周期中存在引用可能变为nil的可能性时,使用weak关键字;因此,类型必须是可选的。当没有引用变为nil的可能性时,使用unowned关键字。
我们通过将weak或unowned关键字与一个类实例的引用配对来定义捕获列表。这些配对在方括号[ ]内书写。因此,如果我们更新myClosure闭包并定义一个指向self的unowned引用,我们应该消除强引用循环。以下代码显示了新的myClosure闭包将类似的样子:
lazy var myClosure: nameClosure = {
[unowned self] in
return self.name
}
注意新的一行——[unowned self] in。这一行表示我们不希望创建对 self 实例的强引用。如果我们现在运行项目,我们应该看到以下输出:
TestClassOne deinitialized
testClassOne is gone
TestClassTwo deinitialized
testClassTwo is gone
这表明 TestClassOne 和 TestClassTwo 实例都得到了适当的释放。
摘要
在本章中,我们看到了我们可以像定义整型或字符串类型一样定义闭包。我们可以将闭包分配给变量,将它们作为函数的参数传递,也可以从函数中返回它们。
闭包捕获了从定义闭包的上下文中引用的任何常量或变量的存储引用。我们必须小心使用这个功能,以确保我们不会创建一个强引用循环,这会导致我们的应用程序中出现内存泄漏。
Swift 闭包与 Objective-C 中的块非常相似,但它们的语法更加简洁和优雅。这使得它们更容易使用和理解。
对闭包有良好的理解对于掌握 Swift 编程语言至关重要,这将使开发易于维护的 OS X 和 iOS 应用程序变得更加容易。这对于创建可以用于创建 OS X 和 iOS 应用程序的一等框架也是必不可少的。
本章中我们看到的三个用例绝不是闭包的唯一三个有用用途。我可以向你保证,你越在 Swift 中使用闭包,你将发现它们的应用越多。闭包无疑是 Swift 语言中最强大和最有用的特性之一,苹果在语言中实现它们做得非常出色。
第十三章。使用混合匹配
当苹果在 2014 年的 WWDC 上首次介绍 Swift 时,我的第一个想法是开发者需要花费多少工作量来将已经用 Objective-C 编写的应用程序重写为 Swift。我也想知道为什么开发者会重写他们的应用程序为 Swift。这些应用程序相当复杂,重写它们需要相当大的努力。在 Swift 的某个演示中,苹果提到了混合匹配,它允许 Swift 和 Objective-C 在同一项目中交互。混合匹配听起来确实像是一个理想的解决方案,因为开发者可以在需要更新时,用 Swift 重写代码的某些部分,而不是不得不重写整个应用程序。我最大的问题是混合匹配实际上会工作得有多好,我非常惊讶;它不仅工作得很好,而且实现起来也很简单。
在本章中,我们将涵盖以下主题:
-
什么是混合匹配
-
如何在同一项目中同时使用 Swift 和 Objective-C
-
如何将 Swift 添加到 Objective-C 项目中
-
如何在 Swift 项目中使用 Objective-C
什么是混合匹配
Swift 与 Objective-C 的兼容性允许我们创建一个项目,并包含用另一种语言编写的文件。这个特性被称为 混合匹配。这可能是 Swift 诞生时最重要的特性之一。
这个特性之所以如此重要,是因为在苹果的 App Store 中,有超过一百万个用 Objective-C 编写的应用程序,开发者花费资源将这些应用程序从 Objective-C 转换为 Swift 是不可行的。没有混合匹配,Swift 语言的适应速度将会非常缓慢。有了混合匹配,开发者可以在不将整个代码库转换为 Swift 的情况下,开始在他们用 Objective-C 编写的现有应用程序中使用 Swift。
使用混合匹配,我们可以使用 Swift 更新我们的当前 Objective-C 项目。我们还可以在我们的 Swift 项目中使用用 Objective-C 编写的任何框架,并在我们的 Objective-C 项目中使用用 Swift 编写的较新框架。
对于长时间使用苹果产品的开发者来说,他们可能会发现混合匹配和苹果从 OS X 10.4.4 Tiger 开始包含的 Rosetta 之间存在相似之处。OS X 10.4.4 是苹果操作系统首次与苹果的第一款基于英特尔的处理器的机器一同发布的版本。Rosetta 的编写是为了允许许多 PowerPC 应用程序在新基于英特尔的处理器的机器上无缝运行。
对于那些刚开始接触苹果产品的开发者来说,你可能没有听说过 Rosetta。这是因为 Rosetta 自 OS X 10.7 Lion 以来就没有被包含或支持。之所以提到这一点,是因为如果混合匹配采取与 Rosetta 相似的道路,它可能永远不再是语言的一部分,并且根据苹果的说法,Swift 是未来。从技术角度来看,随着 Swift 语言的演变和成熟,苹果可能不会希望与 Objective-C 保持兼容性。
如果你维护的是用 Objective-C 编写的遗留应用程序,利用混合匹配慢慢将你的代码库升级到 Swift 可能是个好主意。
让我们看看 Swift 和 Objective-C 如何相互交互。为此,我们将创建一个非常基本的 iOS 项目,其语言将是 Objective-C,然后我们将为该项目添加一些 Swift 代码。在本书的下载代码中,我们包含了一个包含 Swift 代码的 Objective-C 项目和一个包含 Objective-C 代码的 Swift 项目。需要注意的是,无论我们的项目是 Objective-C 还是 Swift 项目,Swift 和 Objective-C 之间的交互方式都是相同的。
在同一项目中使用 Swift 和 Objective-C
在本节中,我们将介绍如何将 Swift 添加到 Objective-C 项目中。同样的步骤也可以用来将 Objective-C 代码添加到 Swift 项目中。在本书的下载代码中,你可以找到 Objective-C 和 Swift 项目。这些项目展示了如何将 Swift 代码添加到 Objective-C 项目中,以及如何将 Objective-C 代码添加到 Swift 项目中。在这些项目中,我们可以看到无论使用什么类型的项目,混合匹配功能都是完全相同的。
创建项目
让我们从创建一个用于工作的 iOS 项目开始。当我们第一次启动 Xcode 时,我们应该会看到一个类似于以下截图的屏幕:

从这个菜单中,我们将想要选择创建一个新的 Xcode 项目选项。这个选项将引导我们创建一个新的 Xcode 项目。一旦选择了这个选项,Xcode 将启动,我们会看到以下菜单。作为一个快捷方式,如果我们没有看到这个菜单,我们也可以在顶部的菜单栏中导航到文件 | 新建 | 项目,这将显示以下屏幕:

这个菜单允许我们选择将要创建的项目类型以及我们针对的平台(iOS 或 OS X)。在这个例子中,我们将针对 iOS 平台创建一个简单的单视图应用程序。一旦我们做出选择,我们应该会看到以下菜单:

在此菜单上,我们将定义有关项目的一些属性。我们需要关注的两个属性是项目的语言和产品名称。对于这个特定的项目,我们将选择 Objective-C 作为语言,并将其命名为 ObjectiveCProject。一旦我们定义了所有属性,我们可以点击 下一步 按钮。在最后一个菜单上,我们选择希望保存项目文件的路径,一旦完成,Xcode 将为我们创建项目模板文件,然后我们可以开始。
我们将要创建的应用程序将允许用户输入一个名字,然后向他们发送一个个性化的消息。用户界面将包括一个用户可以输入名字的 UITextField 字段,一个用户输入名字后需要按下的 UIButton,以及一个将显示个性化消息的 UITextView。由于这本书是关于 Swift 编程的,我们不会深入讲解用户界面的布局。完整的可工作应用程序作为本书可下载源代码的一部分提供。
由于我们正在逐步分析 Objective-C 项目,用户界面和将生成消息的 Messages 类将用 Objective-C 编写。将个性化消息的消息构建器将用 Swift 编写。这将展示我们如何在 Objective-C 项目中从 Objective-C 代码访问 Swift 类以及从 Swift 代码访问 Objective-C 资源。
让我们总结一下 Objective-C 到 Swift 的交互。用户界面后端是用 Objective-C 编写的,将调用 Swift 编写的 MessageBuilder 类的 getPersonalizedMessage() 方法。MessageBuilder 类的 getPersonalizedMessage() 方法将调用 Objective-C 编写的 Messages 类的 getMessage() 函数。
将 Swift 文件添加到 Objective-C 项目中
让我们首先创建 Swift 的 MessageBuilder 类。这个类将用于构建用户的个性化消息。在 Objective-C 项目中,我通常创建一个名为 SwiftFiles 的单独组来存放 Swift 文件。这使我能够很容易地看到哪些文件是用 Swift 编写的,哪些是用 Objective-C 编写的。要将 Swift 文件添加到我们的项目中,右键单击我们想要添加文件的组图标,我们应该会看到以下菜单:

从此菜单中选择 新建文件… 选项。此选项将引导我们创建项目的新文件。一旦选择该选项,你应该会看到以下菜单:

此菜单允许我们选择要添加到项目中的文件类型。在这种情况下,我们希望将 Swift 文件添加到项目中;因此,我们将选择 Swift 文件 选项。一旦我们选择了此选项,我们应该会看到以下菜单:

这个菜单允许我们命名文件并定义一些属性,例如我们将保存文件的位置以及它将属于哪个组。在这种情况下,我们命名文件为 MessageBuilder。一旦完成,我们将点击 创建 按钮。如果这是第一次将 Swift 文件添加到 Objective-C 项目(或第一次将 Objective-C 文件添加到 Swift 项目),我们应该会看到一个以下菜单弹出:

这个弹出窗口提供创建桥接头文件供使用的选项。选择 创建桥接头 以创建文件。
Objective-C 桥接头文件 – 第一部分
为了让我们的 Objective-C 文件暴露给 Swift 代码,我们依赖于一个 Objective-C 头文件。当我们第一次将 Objective-C 文件添加到 Swift 项目或 Swift 文件添加到 Objective-C 项目时,Xcode 会为我们创建这个文件。让 Xcode 创建和配置这个文件比手动操作更容易,因此建议当 Xcode 提供创建它时选择 是。
如果由于某种原因我们需要手动创建 Objective-C 桥接头文件,以下步骤显示了我们将如何进行:
-
使用我们之前看到的 新建文件… 选项在我们的项目中创建一个 Objective-C 头文件。这个文件的推荐命名约定是
[MyProjectName]-Bridging-Header.h,其中[MyProjectName]是我们项目的名称。这将是我们导入任何 Objective-C 头文件的文件,这些头文件是我们想要我们的 Swift 代码访问的 Objective-C 类。 -
在项目的 构建设置 中,找到 Swift 编译器 - 代码生成 部分。在这个部分中,找到标题为 Objective-C 桥接头 的设置。我们将希望将其设置为我们在步骤 1 中创建的桥接头文件的路径。路径将从项目根目录开始。
我们正在工作的当前项目的 Objective-C 桥接头 设置看起来类似于以下截图:

即使桥接头位于 SwiftFiles 组中,我们也可以在设置中看到文件本身位于项目的根目录。如果我们想要将头文件放在项目中的另一个目录中,我们只需要更改这个设置中的路径。
将 Objective-C 文件添加到项目中
现在我们已经有了 Objective-C 的桥接头文件和 MessageBuilder Swift 文件,让我们创建一个 Objective-C 类,该类将向用户生成一个通用的消息。我们将把这个类命名为 Messages。为了创建这个文件,右键点击我们想要添加文件的组文件夹,我们应该会看到以下菜单:

从此菜单中选择 New File… 选项。此选项将引导我们创建一个新文件用于我们的项目。一旦选择该选项,你应该会看到以下菜单:

在我们之前添加 MessageBuilder Swift 文件时,我们在该菜单上选择了 Swift File。这次,我们将添加一个 Objective-C 文件,因此我们将选择 Cocoa Touch Class 选项。一旦选择该选项,我们应该会看到一个类似于下面的屏幕:

在此菜单中,我们可以输入类名,还可以为类设置语言。确保语言设置为 Objective-C。最后,我们点击 Next 按钮,这将带我们到一个菜单,允许我们选择保存 Objective-C 文件的位置。一旦我们选择了保存文件的位置,头文件和实现文件都将添加到我们的项目中。
现在我们已经创建了所有文件,让我们开始编写代码,让 Swift 和 Objective-C 一起工作。我们将首先向 Objective-C 的 Messages 头文件和实现文件中添加代码。
Messages Objective-C 类
Messages Objective-C 类将包含一个消息数组,并公开一个名为 getMessage 的方法,该方法将从数组中随机选择一条消息并返回。
以下代码显示了 Messages 头文件:
#import <Foundation/Foundation.h>
@interface Messages : NSObject
-(NSString *)getMessage;
@end
在此头文件中,我们公开了一个名为 getMessage 的方法,当被调用时会返回一条消息。以下代码显示了 Messages 类的实现文件:
#import "Messages.h"
@implementation Messages
NSMutableArray *theMessages;
-(id)init {
if ( self = [super init] ) {
theMessages = [NSMutableArray new];
[theMessages addObject:@"You should learn from your mistakes"];
[theMessages addObject:@"It is in the now that we must live"];
[theMessages addObject:@"The greatest risk is not taking
one"];
[theMessages addObject:@"You will be a Swift programmer"];
}
return self;
}
-(NSString *)getMessage {
int num = arc4random() % theMessages.count;
return theMessages[num];
}
@end
在此代码中,我们创建了一个包含多个消息的 NSArray 对象。我们还创建了一个 getMessage 方法,该方法从 NSArray 对象中随机选择一条消息并返回它。
我们刚刚在 Objective-C 中创建的 Messages 类将需要被我们将要编写的 Swift 中的 MessageBuilder 类访问。要从 Swift 代码中访问 Objective-C 类,我们需要编辑 Objective-C Bridging Header 文件。
Objective-C bridging header 文件 – 第二部分
现在我们已经创建了 Messages Objective-C 类,我们需要将其公开给我们的 Swift 代码。熟悉 Objective-C(或任何基于 C 的语言)的人会知道,在使用它之前,我们需要使用 #import 或 #include 指令导入类头文件。在相同的情况下,我们还需要在 Objective-C 头文件中导入任何 Objective-C 类的头文件,以便在 Swift 代码中使用该类。因此,为了允许我们的 Swift 代码访问 Messages Objective-C 类,我们需要将以下行添加到 Xcode 为我们创建的 Objective-C bridging header 文件中:
#import "Messages.h"
嗯,就是这样。很简单。现在,让我们看看我们如何编写将使用 Messages Objective-C 类的 MessageBuilder Swift 类。
MessageBuilder Swift 类 – 从 Swift 访问 Objective-C 代码
MessageBuilder Swift 类将包含一个名为getPersonalizedMessage()的方法。这个方法将使用Messages Objective-C 类中的getMessage()方法来检索一条消息,然后在将其返回给调用它的函数之前对这条消息进行定制。以下是MessageBuiler Swift 类的代码:
import Foundation
class MessageBuilder: NSObject {
func getPersonalizedMessage(name: String) -> String {
let messages = Messages()
let retMessage = "To: " + name + ", " + messages.getMessage()
return retMessage;
}
}
当我们定义这个类时,我们将其创建为NSObject类的子类。如果 Swift 类将从 Objective-C 代码中访问,那么这个类需要是NSObject类的子类。如果我们忘记这样做,当我们尝试在 Objective-C 代码中访问该类时,我们将收到Use of undeclared identifier '{Class Name}'错误。
现在,让我们看看我们如何在 Swift 代码中创建Messages Objective-C 类的实例。以下行创建了实例,let messages = Messages()。正如我们所看到的,我们创建Messages Objective-C 类的实例,就像我们创建任何 Swift 类的实例一样。然后我们像访问任何 Swift 类的属性一样访问Messages类的getMessages()方法。
如我们从这段代码中可以看到,当我们从用 Swift 编写的类中访问 Objective-C 类时,Objective-C 类既被初始化也被使用,就像它们是用 Swift 编写的。这使我们能够以一致的方式访问我们的 Objective-C 和 Swift 类型。
现在我们已经创建了MessageBuilder Swift 类,我们需要一种方法来从 Objective-C 的ViewController类中调用getPersonalizedMessage()方法。
Objective-C 类 – 从 Objective-C 访问 Swift 代码
一旦用户输入他们的名字并按下获取消息按钮,我们将创建一个MessageBuilder Swift 类的实例,在 Objective-C 中,并调用getPersonlizedMessage()方法来生成要显示的消息。
当我们从 Objective-C 访问 Swift 代码时,我们依赖于 Xcode 生成的头文件来公开 Swift 类。这个自动生成的头文件声明了 Swift 类的接口。这个头文件的名称是项目名称,后跟–Swift.h。因此,我们项目的头文件名称是ObjectiveCProject-Swift.h。因此,从 Objective-C 访问 Swift 代码的第一步是导入这个头文件,如下面的代码行所示:
#import "ObjectiveCProject-Swift.h"
现在我们已经导入了头文件以公开我们的 Swift 类,我们可以在 Objective-C 代码中使用MessageBuilder Swift 类。我们创建MessageBuilder Swift 类的实例的方式,就像我们创建任何标准 Objective-C 类的实例一样。我们也会像调用 Objective-C 类的属性和方法一样调用 Swift 类的属性和方法。以下示例显示了我们将如何创建MessageBuilder Swift 类的实例,以及我们将如何调用该类的getPersonalizedMessage()方法:
MessageBuilder *mb = [[MessageBuilder alloc] init];
self.messageView.text = [mb getPersonalizedMessage:@"Jon"];
从这个代码示例中我们可以看出,当我们从 Objective-C 访问 Swift 类时,Swift 类被当作 Objective-C 类来处理。再次强调,这使我们能够以一致的方式访问我们的 Objective-C 和 Swift 类型。
摘要
正如我们在本章中看到的,Apple 使混合匹配变得非常容易和方便使用。为了从我们的 Objective-C 代码中访问 Swift 类,我们只需要导入 Xcode 生成的暴露 Swift 类的头文件。虽然我们不会将这个头文件视为代码的一部分,但 Xcode 会自动为混合语言项目创建它。这个头文件的名称采用格式 {Project Name}-Swift.h,其中 {Project Name} 是我们项目的名称。
在我们的 Swift 代码中使用 Objective-C 类也非常简单。为了将 Objective-C 类暴露给我们的 Swift 代码,我们只需要将 Objective-C 头文件添加到 Objective-C 桥接头文件中。Xcode 在我们第一次将 Objective-C 文件添加到 Swift 项目中,或者第一次将 Swift 文件添加到 Objective-C 项目中时,会为我们创建这个桥接头文件。
虽然 Apple 表示 iOS 和 OS X 平台的应用程序开发未来将采用 Swift,但我们可以使用混合匹配来逐步将当前的 Objective-C 代码库迁移到 Swift。混合匹配还允许我们在 Swift 项目中使用 Objective-C 框架,或者在 Objective-C 项目中使用 Swift 框架。
第十四章. Swift 中的并发与并行
当我开始学习 Objective-C 时,我已经对并发和多任务处理有了很好的理解,这得益于我在 C 和 Java 等其他语言中的背景知识。这个背景使我能够很容易地使用 Objective-C 中的线程创建多线程应用程序。然后,当苹果在 OS X 10.6 和 iOS 4 中发布 Grand Central Dispatch (GCD) 时,他们为我改变了一切。起初,我感到否认;GCD 根本不可能比我更好地管理我的应用程序的线程。然后我进入了愤怒阶段,GCD 难以使用和理解。接下来是讨价还价阶段,也许我可以使用 GCD 与我的线程代码一起使用,这样我仍然可以控制线程的工作方式。然后是抑郁阶段,也许 GCD 确实比我更好地处理线程。最后,我进入了哇哦阶段;这个 GCD 真的很容易使用,并且工作得非常出色。在使用了 Grand Central Dispatch 和 Operation Queues 与 Objective-C 一起之后,我看不到使用 Swift 中的手动线程的理由。
在本章中,我们将学习以下主题:
-
并发与并行的基础知识
-
如何使用 GCD 创建和管理并发调度队列
-
如何使用 GCD 创建和管理串行调度队列
-
如何使用各种 GCD 函数将任务添加到调度队列中
-
如何使用
NSOperation和NSOperationQueues为我们的应用程序添加并发
并发与并行
并发是多个任务在同一时间段内启动、运行和完成的理念。这并不一定意味着任务是在同时执行的。为了使任务能够同时执行,我们的应用程序需要在多核或多处理器系统上运行。并发使我们能够与多个任务共享处理器或核心;然而,单个核心一次只能执行一个任务。
并行是两个或更多任务同时运行的概念。由于我们的处理器每个核心一次只能执行一个任务,因此同时执行的任务数量限制在我们处理器中的核心数量。因此,如果我们有一个四核处理器,那么我们只能同时运行四个任务。今天的处理器可以非常快速地执行任务,这可能会让人误以为更大的任务是在同时执行。然而,在系统中,较大的任务实际上是在核心上轮流执行子任务。
为了理解并发与并行的区别,让我们看看一个杂技演员如何抛接球。如果你观察一个杂技演员,他们似乎在任何给定时间都在同时接住和抛出多个球;然而,仔细观察会发现,他们实际上每次只接住和抛出一个球。其他球在空中等待被接住和抛出。如果我们想要能够同时接住和抛出多个球,我们需要添加多个杂技演员。
这个例子非常好,因为我们可以把杂技演员看作是处理器的核心。拥有单核处理器的系统(一个杂技演员),无论看起来如何,一次只能执行一个任务(接住并抛出一个球)。如果我们想同时执行多个任务,我们需要使用多核处理器(多个杂技演员)。
在那些处理器都是单核的古老日子里,要有一个能够同时执行任务的系统,唯一的办法是在系统中拥有多个处理器。这也需要专门的软件来利用多个处理器。在当今世界,几乎每个设备都有一个拥有多个核心的处理器,iOS 和 OS X 操作系统都是设计用来利用多个核心来同时运行任务的。
传统上,应用程序添加并发的方式是创建多个线程;然而,这种模型在任意数量的核心上扩展性不好。使用线程的最大问题是我们的应用程序运行在各种各样的系统(和处理器的)上,为了优化我们的代码,我们需要知道在给定时间可以高效使用多少核心/处理器,这有时在开发时是未知的。
为了解决这个问题,许多操作系统,包括 iOS 和 OS X,开始依赖异步函数。这些函数通常用于启动可能需要很长时间才能完成的任务,例如发起 HTTP 请求或写入数据到磁盘。异步函数通常在长时间运行的任务开始后立即返回,在任务完成之前。通常,这个任务在后台运行,并在任务完成时使用回调函数(例如 Swift 中的闭包)。
这些异步函数对于操作系统提供的任务非常有效,但如果我们需要创建自己的异步函数而不想自己管理线程呢?为此,Apple 提供了一些技术。在本章中,我们将介绍其中两种技术。这些是 GCD 和操作队列。
GCD 是一个基于 C 的低级 API,允许将特定任务排队执行,并在任何可用的处理器核心上调度执行。操作队列与 GCD 类似;然而,它们是 Cocoa 对象,并且内部使用 GCD 实现。
让我们从查看最大公约数(GCD)开始。
大中枢调度(Grand Central Dispatch)
大中枢调度提供了所谓的调度队列来管理提交的任务。队列管理这些提交的任务,并以先进先出(FIFO)的顺序执行它们。这确保了任务是以它们提交的顺序开始的。
任务只是我们应用程序需要执行的一些工作。例如,我们可以创建执行简单计算、读取/写入磁盘数据、发起 HTTP 请求或我们应用程序需要做的任何其他任务的作业。我们通过将代码放在函数或闭包内部并将它们添加到调度队列中来定义这些任务。
GCD 提供了三种类型的队列:
-
串行队列:串行队列中的任务(也称为私有队列)按提交顺序逐个执行。只有在先前的任务完成后,才会启动每个任务。串行队列通常用于同步访问特定资源,因为我们有保证,串行队列中的两个任务永远不会同时运行。因此,如果访问特定资源的唯一方式是通过串行队列中的任务,那么两个任务将不会同时尝试访问该资源或出现顺序混乱。
-
并发队列:并发队列中的任务(也称为全局调度队列)是并发执行的;然而,任务的启动顺序仍然是它们被添加到队列中的顺序。在任何给定时刻可以执行的任务的确切数量是可变的,并且取决于系统的当前条件和资源。何时启动任务的决定权在 GCD,而不是我们可以在应用程序内部控制的事情。
-
主调度队列:主调度队列是一个全局可用的串行队列,它在应用程序的主线程上执行任务。由于放入主调度队列的任务在主线程上运行,因此它通常在后台处理完成并且用户界面需要更新时从后台队列中调用。
调度队列相对于传统线程提供了许多优势。首要的优势是,使用调度队列时,系统处理线程的创建和管理,而不是应用程序本身。系统可以根据系统的总体可用资源和当前系统条件动态地调整线程的数量。这意味着调度队列可以比我们更有效地管理线程。
调度队列的另一个优点是我们能够控制任务启动的顺序。使用串行队列,我们不仅控制了任务启动的顺序,还确保在先前的任务完成之前不会启动下一个任务。使用传统的线程,这可能会非常繁琐且难以实现,但正如我们在本章后面将看到的,使用调度队列则相当容易。
创建和管理调度队列
让我们看看如何创建和使用调度队列。以下三个函数用于创建或检索队列。这些函数如下:
-
dispatch_queue_create: 这将创建一个并发或串行类型的调度队列 -
dispatch_get_global_queue: 这返回一个具有指定服务质量系统定义的全局并发队列 -
dispatch_get_main_queue: 这返回与应用程序主线程关联的串行调度队列
我们还将查看几个将任务提交到队列以执行的功能。这些函数如下:
-
dispatch_async: 这提交一个任务以异步执行并立即返回。 -
dispatch_sync: 这提交一个任务以同步执行,并在返回之前等待其完成。 -
dispatch_after: 这提交一个任务以在指定时间执行。 -
dispatch_once: 这提交一个任务,在应用程序运行期间只执行一次。如果应用程序重新启动,它将再次执行该任务。
在我们查看如何使用调度队列之前,我们需要创建一个类来帮助我们演示各种类型队列的工作方式。此类将包含两个基本函数。第一个函数将简单地执行一些基本计算,然后返回一个值。以下是此函数的代码,该函数名为 doCalc():
func doCalc() {
var x=100
var y = x*x
_ = y/x
}
另一个名为 performCalculation() 的函数接受两个参数。一个是名为 iterations 的整数,另一个是名为 tag 的字符串。performCalculation() 函数会重复调用 doCalc() 函数,直到调用的次数与迭代参数定义的次数相同。我们还使用 CFAbsoluteTimeGetCurrent() 函数来计算执行所有迭代所需的时间,然后使用 tag 字符串将经过的时间打印到控制台。这将让我们知道函数何时完成以及完成它所需的时间。此函数的代码看起来类似于以下内容:
func performCalculation(iterations: Int, tag: String) {
let start = CFAbsoluteTimeGetCurrent()
for var i=0; i<iterations; i++ {
self.doCalc()
}
let end = CFAbsoluteTimeGetCurrent()
print("time for \(tag): \(end-start)")
}
这些函数将一起使用以保持我们的队列忙碌,这样我们就可以看到它们是如何工作的。让我们首先通过使用 dispatch_queue_create() 函数来创建并发和串行队列来查看 GCD 函数。
使用 dispatch_queue_create() 函数创建队列
dispatch_queue_create() 函数用于创建并发和串行队列。dispatch_queue_create() 函数的语法看起来类似于以下内容:
func dispatch_queue_t dispatch_queue_create(label: UnsafePointer<Int8>, attr: dispatch_queue_attr_t!) -> dispatch_queue_t!
它需要以下参数:
-
label: 这是一个附加到队列上的字符串标签,用于在调试工具(如 Instruments 和崩溃报告)中唯一标识它。建议我们使用反向 DNS 命名约定。此参数是可选的,可以是 nil。 -
attr: 这指定了要创建的队列类型。这可以是DISPATCH_QUEUE_SERIAL, DISPATCH_QUEUE_CONCURRENT或 nil。如果此参数为 nil,则创建一个串行队列。
此函数的返回值是新创建的调度队列。让我们通过创建一个并发队列并查看其工作方式来了解如何使用 dispatch_queue_create() 函数。
注意
一些编程语言使用反向 DNS 命名约定来命名某些组件。这个约定基于一个反转的已注册域名。例如,如果我们为名为mycompany.com的公司工作,该公司有一个名为widget的产品,那么反向 DNS 名称将是com.mycompany.widget。
使用dispatch_queue_create()函数创建并发调度队列
以下行创建了一个带有标签cqueue.hoffman.jon的并发调度队列:
let queue = dispatch_queue_create("cqueue.hoffman.jon", DISPATCH_QUEUE_CONCURRENT)
正如我们在本节开始时看到的,我们可以使用几个函数将任务提交到调度队列。当我们与队列一起工作时,我们通常希望使用dispatch_async()函数来提交任务,因为我们通常不希望等待响应。dispatch_async()函数具有以下签名:
func dispatch_async(queue: dispatch_queue_t!, block: dispatch_queue_block!)
以下示例显示了如何使用我们刚刚创建的并发队列的dispatch_async()函数:
let c = { performCalculation(1000, tag: "async0") }
dispatch_async(queue, c)
在前面的代码中,我们创建了一个闭包,它代表我们的任务,简单地调用DoCalculation实例的performCalculation()函数,请求它运行doCalc()函数的 1000 次迭代。最后,我们使用dispatch_async()函数将任务提交到并发调度队列。此代码将在并发调度队列中执行任务,该队列与主线程分开。
尽管前面的例子工作得很好,但实际上我们可以稍微缩短代码。下一个例子显示,我们实际上不需要像前面例子中那样创建一个单独的闭包;我们也可以像这样提交任务来执行:
dispatch_async(queue) {
calculation.performCalculation(10000000, tag: "async1")
}
这种简写版本是我们通常提交到队列的小代码块的方式。如果我们有更大的任务,或者需要多次提交的任务,我们通常希望创建一个闭包,并将闭包提交到队列,就像我们最初展示的那样。
让我们通过向队列中添加几个项目并查看它们的返回顺序和时间来查看并发队列实际上是如何工作的。以下代码将向队列添加三个任务。每个任务都会用不同的迭代次数调用performCalculation()函数。记住,performCalculation()函数将连续执行计算例程,直到执行次数等于传入的迭代次数。因此,我们传递给performCalculation()函数的迭代次数越大,它应该执行的时间就越长。让我们看一下以下代码:
dispatch_async(queue) {
calculation.performCalculation(10000000, tag: "async1")
}
dispatch_async(queue) {
calculation.performCalculation(1000, tag: "async2")
}
dispatch_async(queue) {
calculation.performCalculation(100000, tag: "async3")
}
注意到每个函数都是用不同的值在tag参数中调用的。由于performCalculation()函数会打印出带有经过时间的tag变量,我们可以看到任务完成的顺序以及执行所需的时间。如果我们执行前面的代码,我们应该看到以下结果:
time for async2: 0.000200986862182617
time for async3: 0.00800204277038574
time for async1: 0.461670994758606
注意
经过的时间会因运行而异,也会因系统而异。
由于队列以 FIFO(先进先出)顺序工作,标签为async1的任务首先执行。然而,正如我们从结果中看到的,它是最后一个完成的任务。由于这是一个并发队列,如果可能(如果系统有可用资源),代码块将并发执行。这就是为什么标签为async2和async3的任务在标签为async1的任务之前完成,尽管async1任务的执行开始在其他两个任务之前。
现在,让我们看看串行队列是如何执行任务的。
使用dispatch_queue_create()函数创建串行调度队列
串行队列的功能与并发队列略有不同。串行队列一次只会执行一个任务,并在开始下一个任务之前等待当前任务完成。这个队列,就像并发调度队列一样,遵循先入先出的顺序。以下代码行将创建一个标签为squeue.hoffman.jon的串行队列:
let queue2 = dispatch_queue_create("squeue.hoffman.jon", DISPATCH_QUEUE_SERIAL)
注意,我们使用DISPATCH_QUEUE_SERIAL属性创建串行队列。如果你还记得,当我们创建并发队列时,我们使用的是DISPATCH_QUEUE_CONCURRENT属性。我们也可以将此属性设置为nil,这将默认创建一个串行队列。然而,建议始终将属性设置为DISPATCH_QUEUE_SERIAL或DISPATCH_QUEUE_CONCURRENT,以便更容易识别我们正在创建哪种类型的队列。
正如我们通过并发调度队列所看到的,我们通常想使用dispatch_async()函数来提交任务,因为当我们向队列提交任务时,我们通常不希望等待响应。然而,如果我们确实想等待响应,我们会使用dispatch_synch()函数。
var calculation = DoCalculations()
let c = { calculation.performCalculation(1000, tag: "sync0") }
dispatch_async(queue2, c)
就像并发队列一样,我们不需要创建闭包来向队列提交任务。我们也可以这样提交任务:
dispatch_async(queue2) {
calculation.performCalculation(100000, tag: "sync1")
}
让我们通过向队列添加几个项目并查看它们的完成顺序和时间来了解串行队列的工作原理。以下代码将添加三个任务,这些任务将使用不同的迭代次数调用performCalculation()函数:
dispatch_async(queue2) {
calculation.performCalculation(100000, tag: "sync1")
}
dispatch_async(queue2) {
calculation.performCalculation(1000, tag: "sync2")
}
dispatch_async(queue2) {
calculation.performCalculation(100000, tag: "sync3")
}
就像并发队列示例一样,我们使用不同的迭代次数和不同的tag参数值调用performCalculation()函数。由于performCalculation()函数会打印出带有经过时间的tag字符串,我们可以看到任务的完成顺序和执行时间。如果我们执行此代码,我们应该看到以下结果:
time for sync1: 0.00648999214172363
time for sync2: 0.00009602308273315
time for sync3: 0.00515800714492798
注意
经过时间会因运行而异,也会因系统而异。
与并发队列不同,我们可以看到,尽管sync2和sync3任务完成所需时间明显更短,但完成的任务顺序与提交的顺序相同。这表明串行队列一次只执行一个任务,并且队列在开始下一个任务之前会等待每个任务完成。
现在我们已经看到了如何使用dispatch_queue_create()函数创建并发和串行队列,让我们看看如何使用dispatch_get_global_queue()函数获取四个系统定义的全局并发队列之一。
使用dispatch_get_global_queue()函数请求并发队列
系统为每个应用程序提供四个不同优先级级别的并发全局调度队列。不同的优先级级别是区分这些队列的因素。四个优先级如下:
-
DISPATCH_QUEUE_PRIORITY_HIGH:此队列中的项目以最高优先级运行,并且会在默认和低优先级队列的项目之前进行调度 -
DISPATCH_QUEUE_PRIORITY_DEFAULT:此队列中的项目以默认优先级运行,并且会在低优先级队列的项目之前,但在高优先级队列的项目之后进行调度 -
DISPATCH_QUEUE_PRIORITY_LOW:此队列中的项目以低优先级运行,并且只有在高优先级和默认队列的项目之后才会进行调度 -
DISPATCH_QUEUE_PRIORITY_BACKGROUND:此队列中的项目以后台优先级运行,优先级最低
由于这些是全局队列,我们实际上不需要创建它们;相反,我们请求具有所需优先级的队列的引用。要请求全局队列,我们使用dispatch_get_global_queue()函数。此函数具有以下语法:
func dispatch_get_global_queue(identifier: Int, flags: UInt) -> dispatch_queue_t!
在这里,以下参数被定义:
-
identifier:这是我们请求的队列的优先级 -
flags:此参数保留供将来扩展使用,目前应设置为零
我们使用dispatch_get_global_queue()函数请求一个队列,如下面的示例所示:
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
在此示例中,我们请求具有默认优先级的全局队列。然后我们可以像使用我们使用dispatch_queue_create()函数创建的并发队列一样使用此队列。使用dispatch_get_global_queue()函数返回的队列与使用dispatch_create_queue()函数创建的队列之间的区别在于,使用dispatch_create_queue()函数时,我们实际上是在创建一个新的队列。使用dispatch_get_global_queue()函数返回的队列是在我们的应用程序首次启动时创建的全局队列;因此,我们是在请求一个队列而不是创建一个新的队列。
当我们使用dispatch_get_global_queue()函数时,我们避免了创建队列的开销;因此,除非你有特定的理由来创建队列,否则我建议使用dispatch_get_global_queue()函数。
使用dispatch_get_main_queue()函数请求主队列
dispatch_get_main_queue()函数返回我们的应用程序的主队列。当应用程序启动时,为主线程自动创建主队列。这个主队列是一个串行队列;因此,队列中的项目将按它们提交的顺序逐个执行。我们通常想要避免使用这个队列,除非我们需要从后台线程更新用户界面。
dispatch_get_main_queue()函数的语法如下:
func dispatch_get_main_queue() -> dispatch_queue_t!
以下代码示例展示了如何请求主队列:
let mainQueue = dispatch_get_main_queue();
我们将像提交任何其他串行队列一样将任务提交到主队列。只需记住,提交到这个队列的任何内容都将运行在主线程上,这是所有用户界面更新运行的线程;因此,如果我们提交了一个长时间运行的任务,用户界面将冻结,直到该任务完成。
在前面的章节中,我们看到了dispatch_async()函数是如何将任务提交到并发和串行队列中的。现在,让我们看看两个额外的函数,我们可以使用它们将任务提交到我们的队列中。我们将首先查看的函数是dispatch_after()函数。
使用dispatch_after()函数
有时候我们需要在延迟后执行任务。如果我们使用线程模型,我们需要创建一个新的线程,执行某种延迟或睡眠函数,然后执行我们的任务。使用 GCD,我们可以使用dispatch_after()函数。dispatch_after()函数的语法如下:
func dispatch_after(when: dispatch_time_t, queue: dispatch_queue_t, block: dispatch_block_t)
在这里,dispatch_after()函数接受以下参数:
-
when:这是我们希望队列执行我们的任务的时间 -
queue:这是我们想要在队列中执行我们的任务。 -
block:这是要执行的任务
与dispatch_async()和dispatch_synch()函数一样,我们不需要将任务作为参数包含。我们可以在两个大括号之间包含要执行的任务,就像我们之前在dispatch_async()和dispatch_synch()函数中做的那样。
如我们从dispatch_after()函数中看到的,我们使用dispatch_time_t类型来定义执行任务的时间。我们使用dispatch_time()函数来创建dispatch_time_t类型。dispatch_time()函数的语法如下:
func dispatch_time(when: dispatch_time_t, delta:Int64) -> dispatch_time_t
在这里,dispatch_time()函数接受以下参数:
-
when:这个值用作执行任务的时间基础。我们通常传递DISPATCH_TIME_NOW值来创建基于当前时间的时。 -
delta:这是要添加到when参数中的纳秒数,以获取我们的时间。
我们将像这样使用dispatch_time()和dispatch_after()函数:
var delayInSeconds = 2.0
let eTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delayInSeconds * Double(NSEC_PER_SEC)))
dispatch_after(eTime, queue2) {
print("Times Up")
}
上述代码将在两秒后执行任务。在dispatch_time()函数中,我们创建了一个dispatch_time_t类型,表示未来两秒。NSEC_PER_SEC常量用于从秒计算纳秒。在两秒的延迟后,我们将消息Times Up打印到控制台。
使用dispatch_after()函数时要注意的一件事是。让我们看一下以下代码:
let queue2 = dispatch_queue_create("squeue.hoffman.jon", DISPATCH_QUEUE_SERIAL)
var delayInSeconds = 2.0
let pTime = dispatch_time(DISPATCH_TIME_NOW,Int64(delayInSeconds * Double(NSEC_PER_SEC)))
dispatch_after(pTime, queue2) {
print("Times Up")
}
dispatch_sync(queue2) {
calculation.performCalculation(100000, tag: "sync1")
}
在此代码中,我们首先创建了一个串行队列,然后向队列中添加了两个任务。第一个任务使用了dispatch_after()函数,第二个任务使用了dispatch_sync()函数。我们最初的设想可能是,当我们在这个串行队列中执行这段代码时,第一个任务会在两秒后执行,然后才是第二个任务;然而,这并不正确。第一个任务被提交到队列中并立即执行。它也立即返回,这使得队列在等待第一个任务正确执行的时间的同时执行下一个任务。因此,尽管我们在串行队列中运行任务,第二个任务却在第一个任务之前完成。以下是在运行前面的代码时的输出示例:
time for sync1: 0.00407701730728149
Times Up
我们将要查看的最后一个 GCD 函数是dispatch_once()。
使用dispatch_once()函数
dispatch_once()函数将只在应用程序的生命周期内执行一次任务,并且只执行一次。这意味着任务将被执行并标记为已执行,然后除非应用程序重新启动,否则该任务将不再执行。虽然dispatch_once()函数可以,并且已经被用来实现单例模式,但还有其他更简单的方法可以做到这一点。请参阅第十七章,在 Swift 中采用设计模式,了解如何实现单例设计模式的示例。
dispatch_once()函数非常适合执行在应用程序最初启动时需要运行的任务。这些初始化任务可以包括初始化我们的数据存储或变量和对象。以下代码显示了dispatch_once()函数的语法:
func dispatch_once(predicate: UnsafeMutablePointer<dispatch_once_t>,block: dispatch_block_t!)
让我们看看如何使用dispatch_once()函数:
var token: dispatch_once_t = 0
func example() {
dispatch_once(&token) {
print("Printed only on the first call")
}
print("Printed for each call")
}
在这个例子中,打印消息“仅在第一次调用时打印”的行只会执行一次,无论函数被调用多少次。然而,打印“每次调用都打印”消息的行会在每次函数调用时执行。让我们通过四次调用这个函数来观察这个行为,如下所示:
for i in 0..<4 {
example()
}
如果我们执行这个示例,我们应该看到以下输出:
Printed only on the first call
Printed for each call
Printed for each call
Printed for each call
Printed for each call
注意,在这个例子中,我们只看到了一次“仅在第一次调用时打印”的消息,而“每次调用都打印”的消息在四次调用函数时都出现了。
现在我们已经了解了 GCD,让我们来看看操作队列。
使用 NSOperation 和 NSOperationQueue 类型
NSOperation 和 NSOperationQueues 类型协同工作,为我们提供了在应用程序中添加并发的替代方案。操作队列是 Cocoa 对象,其功能类似于调度队列,并且内部,操作队列使用 GCD 实现。我们定义要执行的任务(NSOperations),然后将任务添加到操作队列(NSOperationQueue)。然后,操作队列将处理任务的调度和执行。操作队列是 NSOperationQueue 类的实例,操作是 NSOperation 类的实例。
操作代表一个单独的工作单元或任务。NSOperation 类型是一个抽象类,它提供了一个线程安全的结构来模拟状态、优先级和依赖关系。为了执行任何有用的操作,必须对该类进行子类化。
苹果确实提供了两种 NSOperation 类型的具体实现,我们可以直接使用,对于不需要构建自定义子类的情况。这些子类是 NSBlockOperation 和 NSInvocationOperation。
同时可以存在多个操作队列,实际上,总是至少有一个操作队列在运行。这个操作队列被称为 主队列。当应用程序启动时,主队列会自动为主线程创建,并且所有 UI 操作都在这里执行。
我们有几种方法可以使用 NSOperation 和 NSOperationQueues 类来为我们的应用程序添加并发。在本章中,我们将探讨三种不同的方法。我们将首先查看的是使用 NSOperation 抽象类的 NSBlockOperation 实现方式。
使用 NSOperation 的 NSBlockOperation 实现方式
在本节中,我们将使用与 Grand Central Dispatch 部分中相同的 DoCalculation 类,以保持我们的队列忙碌于工作,这样我们就可以看到 NSOperationQueues 类的工作原理。
NSBlockOperation 类是 NSOperation 类型的具体实现,可以管理一个或多个块的执行。这个类可以用来同时执行多个任务,而无需为每个任务创建单独的操作。
让我们看看如何使用 NSBlockOperation 类来为我们的应用程序添加并发。以下代码展示了如何使用单个 NSBlockOperation 实例将三个任务添加到操作队列中:
let calculation = DoCalculations()
let operationQueue = NSOperationQueue()
let blockOperation1: NSBlockOperation = NSBlockOperation.init(block: {
calculation.performCalculation(10000000, tag: "Operation 1")
})
blockOperation1.addExecutionBlock(
{
calculation.performCalculation(10000, tag: "Operation 2")
}
)
blockOperation1.addExecutionBlock(
{
calculation.performCalculation(1000000, tag: "Operation 3")
}
)
operationQueue.addOperation(blockOperation1)
在此代码中,我们首先创建 DoCalculation 类的一个实例和 NSOperationQueue 类的一个实例。接下来,我们使用 init 构造函数创建 NSBlockOperation 类的一个实例。这个构造函数接受一个参数,它是一个代码块,代表我们想在队列中执行的任务之一。然后,我们使用 addExecutionBlock() 方法向 NSBlockOperation 实例添加两个额外的任务。
这是调度队列和操作之间的一种区别。在调度队列中,如果资源可用,任务会按照它们被添加到队列的顺序执行。在操作中,各个任务不会执行,直到操作本身被提交到操作队列。
一旦我们将所有任务添加到 NSBlockOperation 实例中,我们接着将操作添加到我们在代码开头创建的 NSOperationQueue 实例中。此时,操作内的各个任务开始执行。
这个例子展示了如何使用 NSBlockOperation 将多个任务排队,然后将任务传递给操作队列。任务按照先进先出的顺序执行;因此,第一个添加到 NSBlockOperation 实例的任务将是第一个执行的任务。然而,由于如果我们有可用资源,任务可以并发执行,所以这段代码的输出应该看起来像这样:
time for Operation 2: 0.00546294450759888
time for Operation 3: 0.0800899863243103
time for Operation 1: 0.484337985515594
如果我们不希望我们的任务并发运行呢?如果我们希望它们像串行调度队列一样按顺序运行呢?我们可以在我们的操作队列中设置一个属性,该属性定义了队列中可以并发运行的任务数量。这个属性叫做 maxConcurrentOperationCount,其使用方式如下:
operationQueue.maxConcurrentOperationCount = 1
然而,如果我们把这一行添加到之前的例子中,它将不会按预期工作。为了了解为什么,我们需要理解这个属性实际上定义了什么。如果我们查看苹果的 NSOperationQueue 类参考,属性的说明是:“可以同时执行的最大队列操作数。”
这告诉我们,maxConcurrentOperationCount 属性定义了可以同时执行的操作数量(这是关键词)。我们添加所有任务到的 NSBlockOperation 实例代表了一个单一的操作;因此,不会执行添加到队列中的其他 NSBlockOperation,直到第一个完成,但操作内的各个任务可以并发执行。如果我们想按顺序执行任务,我们需要为每个任务创建一个单独的 NSBlockOperations 实例。
如果我们有一系列想要并发执行的任务,使用 NSBlockOperation 类的实例是好的,但它们不会开始执行,直到我们将操作添加到操作队列中。让我们看看使用队列的 addOperationWithBlock() 方法将任务添加到操作队列的更简单方法。
使用操作队列的 addOperationWithBlock() 方法
NSOperationQueue 类有一个名为 addOperationWithBlock() 的方法,这使得向队列中添加代码块变得简单。此方法会自动将代码块包装在操作对象中,然后将该操作传递给队列本身。让我们看看如何使用此方法将任务添加到队列中:
let operationQueue = NSOperationQueue()
let calculation = DoCalculations()
operationQueue.addOperationWithBlock() {
calculation.performCalculation(10000000, tag: "Operation1")
}
operationQueue.addOperationWithBlock() {
calculation.performCalculation(10000, tag: "Operation2")
}
operationQueue.addOperationWithBlock() {
calculation.performCalculation(1000000, tag: "Operation3")
}
在本章前面的NSBlockOperation示例中,我们将希望执行的任务添加到了一个NSBlockOperation实例中。在这个示例中,我们直接将任务添加到操作队列中,每个任务代表一个完整的操作。一旦我们创建了操作队列的实例,我们就使用addOperationWithBlock()方法将任务添加到队列中。
此外,在NSBlockOperation示例中,各个任务在所有任务都添加到NSBlockOperation对象并且该操作被添加到队列之前不会执行。这个addOperationWithBlock()示例与 GCD 示例类似,其中任务在添加到操作队列后立即开始执行。
如果我们运行前面的代码,输出应该类似于以下内容:
time for Operation2: 0.0115870237350464
time for Operation3: 0.0790849924087524
time for Operation1: 0.520610988140106
你会注意到操作是并发执行的。使用这个示例,我们可以通过使用我们之前提到的maxConcurrentOperationCount属性来按顺序执行任务。让我们通过以下方式初始化NSOperationQueue实例来尝试这一点:
var operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 1
现在,如果我们运行这个示例,输出应该类似于以下内容:
time for Operation1: 0.418763995170593
time for Operation2: 0.000427007675170898
time for Operation3: 0.0441589951515198
在这个示例中,我们可以看到每个任务在开始之前都等待前一个任务完成。
使用addOperationWithBlock()方法添加任务,通常比使用NSBlockOperation方法更容易;然而,任务将在它们被添加到队列后立即开始执行,这通常是期望的行为。
现在,让我们看看我们如何继承NSOperation类来创建一个可以直接添加到操作队列中的操作。
继承NSOperation类
前两个示例展示了如何将小块代码添加到我们的操作队列中。在这些示例中,我们调用了DoCalculation类中的performCalculations方法来执行我们的任务。这些示例说明了两种非常好的方法来为已经编写的功能添加并发性,但如果我们希望在设计时为DoCalculation类设计并发性,那会怎样呢?为此,我们可以继承NSOperation类。
NSOperation抽象类提供了大量的基础设施。这使得我们能够非常容易地创建一个子类而不需要做很多工作。我们至少应该提供一个initialization方法和一个main方法。main方法将在队列开始执行操作时被调用:
让我们看看如何将DoCalculation类实现为NSOperation类的子类;我们将这个新类称为MyOperation:
class MyOperation: NSOperation {
let iterations: Int
let tag: String
init(iterations: Int, tag: String) {
self.iterations = iterations
self.tag = tag
}
override func main() {
performCalculation()
}
func performCalculation() {
let start = CFAbsoluteTimeGetCurrent()
for var i=0; i<iterations; i++ {
self.doCalc()
}
let end = CFAbsoluteTimeGetCurrent()
print("time for \(tag): \(end-start)")
}
func doCalc() {
let x=100
let y = x*x
_ = y/x
}
}
我们首先定义 MyOperation 类是 NSOperation 类的子类。在类的实现中,我们定义了两个类常量,它们代表 performCalculations() 方法使用的迭代次数和标签。请记住,当操作队列开始执行操作时,它将不带参数调用 main() 方法;因此,我们需要传递的任何参数都必须通过初始化器传递。
在这个例子中,我们的初始化器接受两个参数,用于设置 iterations 和 tag 类常量。然后 main() 方法,操作队列将要调用来开始执行操作,简单地调用 performCalculation() 方法。
我们现在可以非常容易地将我们的 MyOperation 类的实例添加到操作队列中,如下所示:
var operationQueue = NSOperationQueue()
operationQueue.addOperation(MyOperation(iterations: 10000000, tag: "Operation 1"))
operationQueue.addOperation(MyOperation(iterations: 10000, tag: "Operation 2"))
operationQueue.addOperation(MyOperation(iterations: 1000000, tag: "Operation 3"))
如果我们运行此代码,我们将看到以下结果:
time for Operation 2: 0.00187397003173828
time for Operation 3: 0.104826986789703
time for Operation 1: 0.866684019565582
如我们之前所见,我们也可以通过添加以下行来顺序执行任务,该行设置操作队列的 maxConcurrentOperationCount 属性:
operationQueue.maxConcurrentOperationCount = 1
如果我们知道在编写代码之前需要并发执行某些功能,我建议像这个例子一样对 NSOperation 类进行子类化,而不是使用之前的例子。这给我们提供了最干净的实现;然而,使用 NSBlockOperation 类或本节之前描述的 addOperationWithBlock() 方法并没有什么问题。
摘要
在我们考虑将并发性添加到我们的应用程序之前,我们应该确保我们理解为什么我们要添加它,并问自己这是否是必要的。虽然并发性可以通过将工作从主应用程序线程卸载到后台线程来使我们的应用程序更响应,但它也增加了我们代码的额外复杂性和应用程序的开销。我甚至看到过许多在各种语言中运行得更好的应用程序,在移除一些并发代码之后。这是因为并发性没有经过深思熟虑或计划。考虑到这一点,在我们讨论应用程序预期行为时,思考和讨论并发性总是一个好主意。
在本章的开始,我们讨论了与顺序运行任务相比并行运行任务。我们还讨论了限制特定设备上可以并行运行多少任务的硬件限制。对这些概念有良好的理解对于理解如何在何时将并发性添加到我们的项目中非常重要。
虽然 GCD 并不仅限于系统级应用,但在我们将其应用于我们的应用程序之前,我们应该考虑操作队列是否会更简单、更适合我们的需求。一般来说,我们应该使用满足我们需求的最高级别的抽象。这通常会将我们引向使用操作队列;然而,实际上并没有什么阻止我们使用 GCD,它可能更适合我们的需求。
在操作队列中需要注意的一点是,由于它们是 Cocoa 对象,因此确实会增加额外的开销。对于大多数应用程序来说,这微小的额外开销不应成为问题,甚至可能不会被察觉;然而,对于一些项目,例如需要获取所有可用资源的游戏,这额外的开销可能确实成为一个问题。
第十五章. Swift 格式和风格指南
在我的开发经验中,每次我学习一门新的编程语言时,通常都会提到如何编写和格式化该语言的代码。在我的开发生涯早期(那是很久以前),这些推荐都是非常基础的格式化建议,比如如何缩进代码,或者每行只写一个语句。实际上,直到最近 10 到 12 年,我才开始看到不同编程语言的复杂和详细的格式和风格指南。如今,很难找到一个只有两三个开发者而没有为每种语言制定风格/格式指南的开发团队。即使那些没有创建自己风格指南的公司,通常也会参考其他公司发布的某些标准指南,如 Google、Oracle 或 Microsoft。这些风格指南帮助团队编写一致且易于维护的代码。
什么是编程风格指南?
编程风格非常个人化,每个开发者都有自己的首选风格。这些风格可以从语言到语言、从人到人,甚至随时间而变化。编程风格的个人性质使得在众多个人贡献代码时,保持一致的、易于阅读的代码库变得困难。
虽然大多数开发者可能都有自己的首选风格,但不同语言之间推荐或首选的风格可能有所不同。例如,在 C#中,当我们命名一个方法或函数时,首选使用驼峰式命名法,首字母大写。而在大多数语言中,如 C、Objective-C 和 Java,也推荐使用驼峰式命名法,但我们应该将首字母小写。
最佳的应用程序都是编写得当的,而所谓的“得当”,并不仅仅是指它们能够正确运行,还意味着它们易于维护,代码易于阅读。如果每个开发者都使用自己的编码风格,那么对于大型项目和拥有大量开发者的公司来说,要拥有易于维护和阅读的代码是非常困难的。这就是为什么拥有多个开发者的公司和项目通常会为使用的每种语言采用编程风格指南。
编程风格指南定义了一套规则和指南,开发者在编写项目或公司内部特定语言的程序时应该遵循这些规则和指南。这些风格指南在公司或项目之间可能差异很大,反映了该公司或项目期望代码的编写方式。这些指南也可能随时间而变化。遵循这些风格指南对于保持一致的代码库非常重要。
许多开发者不喜欢被告知应该如何编写代码的想法,并声称只要他们的代码能正确运行,为什么他们的代码格式很重要。我把这比作一个篮球队。如果所有球员都认为他们想要的方式是正确的,并认为当他们在做自己想做的事情时,球队会更好,那么这个球队很可能会输掉大多数比赛。一个篮球队(或者任何运动队,无论如何)除非他们一起工作,否则不可能赢得大多数比赛。确保每个人都在一起工作并执行相同的比赛计划的责任在于教练,就像开发项目的团队领导确保所有开发者都按照采用的风格指南编写代码一样。
您的风格指南
在本书中定义的风格指南只是一个指南。它反映了作者对如何编写 Swift 代码的看法,并旨在成为创建您自己的风格指南的良好起点。如果您真的喜欢这个指南并直接采用它,那很好。如果您不同意其中的某些部分并在您的指南中进行了修改,那也很好。您和您的团队感到舒适的风格就是最适合您和您的团队的风格,它可能与本书中的指南不同。我们也应该指出,Swift 是一种非常年轻的语言,人们仍在努力找出与 Swift 一起使用的适当风格;因此,今天推荐的东西明天可能就会受到批评。不要害怕根据需要调整您的风格指南。
在本章的风格指南中,以及大多数好的风格指南中,一个值得注意的事情是,对为什么每个项目被优先考虑或不被优先考虑的解释非常少。风格指南应该提供足够的细节,以便读者了解每个项目的优先和非优先方法,但同时也应该小巧紧凑,以便于阅读和理解。
如果开发者对为什么某个特定方法被优先考虑有疑问,他或她应该将这个疑问提出给开发团队。
考虑到这一点,让我们开始编写指南。
不要在语句末尾使用分号
与许多语言不同,Swift 不需要在语句末尾使用分号。因此,我们不应该使用它们。让我们看看以下代码:
//Preferred Method
var name = "Jon"
print(name)
//Non-preferred Method
var name = "Jon";
print(name);
不要为条件语句使用括号
与许多语言不同,条件语句周围不需要括号;因此,除非需要澄清,否则我们应该避免使用它们。让我们看看以下代码:
//Preferred Method
if speed == 300000000 {
print("Speed of light")
}
//Non-Preferred Method
if (speed == 300000000) {
print("Speed of light")
}
命名
我们应该始终使用描述性的名称,并使用驼峰式命名法为类、方法、变量、常量等命名。让我们看看一些通用的命名规则。
类
类名应该有一个描述性的名称,描述类的作用。名称应该以大写字母开头,并使用驼峰式命名法。以下是根据我们的风格指南提供的正确名称和非正确名称的示例:
// Proper Naming Convention
BaseballTeam
LaptopComputer
//Non-Proper Naming Convention
baseballTeam //Starts with a lowercase letter
Laptop_Computer //Uses an underscore
函数和方法
函数名应该是描述性的,描述函数或方法。它们应该以小写字母开头,并使用驼峰式命名法。以下是一些正确名称和非正确名称的示例:
//Proper Naming Convention
getCityName
playSound
//
//Non-Proper Naming Convention
get_city_name //All lowercase and has an underscore
PlaySound //Begins with an upper case letter
常量和变量
常量和变量应该有一个描述性的名称。通常,它们以小写字母开头,并使用驼峰式命名法。唯一的例外是当常量是全局的;在这种情况下,常量的名称应包含所有大写字母,单词之间用下划线分隔。我见过许多指南都反对使用全部大写名称,但我个人喜欢在全局作用域中的常量使用它们,因为它们突出显示了它们是全局作用域,而不是局部作用域。以下是一些正确名称和非正确名称的示例:
//Proper Names
playerName
driveSize
PLAYERS_ON_A_TEAM //Only for globally scoped constants
//Non-Proper Names
PlayerName //Starts with uppercase letter
drive_size //Has underscore in name
缩进
Xcode 中的缩进宽度默认定义为四个空格,制表符宽度也定义为四个空格。我们应该将其保留为默认设置。以下截图显示了 Xcode 中的缩进设置:

我们还应该在函数/方法之间添加一个额外的空行。我们还应该使用空行来分隔函数或方法内的功能。换句话说,在函数或方法中使用过多的空行可能意味着我们应该将函数分解成多个函数。
注释
我们应该根据需要使用注释来解释我们的代码是如何和为什么被编写的。我们应该在类和函数之前使用块注释,而我们应该使用双斜杠来注释行内代码。以下是如何编写注释的示例:
/**
* This is a block comment that should be used
* to explain a class or function
*/
public class EmployeeClass {
// This is an inline comment with double slashes
var firstName = ""
var lastName = ""
/**
Use Block comments for functions
:parm: paramName use this tag for parameters
:returns: explain what is returned
*/
func getFullName() -> String {
return firstName + " " + lastName
}
}
当我们评论方法时,我们也应该使用会在 Xcode 中生成文档的文档标签,正如前一个示例所示。至少,如果这些标签适用于我们的方法,我们应该使用以下标签:
-
:param: 这用于参数 -
:return: 这用于返回的内容
使用 self 关键字
由于 Swift 不要求我们在访问对象的属性或调用对象的方法时使用 self 关键字,因此除非我们需要区分对象的属性和局部变量,否则我们应该避免使用它。以下是一个应该使用 self 关键字的示例:
public class EmployeeClass {
var firstName = ""
var lastName = ""
func setName(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
这里是一个不使用 self 关键字的示例:
public class EmployeeClass {
var firstName = ""
var lastName = ""
func getFullName() -> String {
return self.firstName + " " + self.lastName
}
}
类型
在可能的情况下,我们应该始终使用 Swift 原生类型。如果我们记得,Swift 提供了与 Objective-C 类型的桥接,所以即使我们使用 Swift 原生类型,我们仍然可以访问 Objective-C 类型提供的全部方法集。以下代码显示了使用原生类型的推荐和非推荐方式:
//Preferred way
let amount = 25.34
let amountStr = (amount as NSNumber).stringValue
//Non-preferred way
let amount: NSNumber = 25.34
let amountStr = amount.stringValue
常量和变量
常量与变量的区别在于常量的值永远不会改变,而变量的值可能会改变。 wherever possible,我们应该定义常量而不是变量。
做这件事最简单的方法是将所有内容默认定义为常量,然后在代码中达到需要更改的点后再将其定义为变量。使用 Swift 2,如果你定义了一个变量但从未在代码中更改其值,你会收到一个警告。
可选类型
只有在绝对必要时才使用可选类型。如果没有绝对必要将 nil 值分配给变量,我们就不应该将其定义为可选的。
使用可选绑定
我们应该避免强制解包可选值,因为这很少是必要的。我们应该优先选择可选绑定或可选链来强制解包。
以下示例显示了定义 myOptional 变量为可选时的首选和非首选方法:
//Preferred Method Optional Binding
if let value = myOptional {
// code if myOptional is not nil
} else {
// code if myOptional is nil
}
//Non-Preferred Method
if myOptional != nil {
// code if myOptional is not nil
} else {
// code if myOptional is nil
}
如果我们需要解包多个可选值,我们应该在相同的一行中包含它们,而不是在单独的行中解包,除非我们的业务逻辑不需要在解包失败时采取不同的路径。以下示例显示了首选和非首选方法:
//Preferred Method Optional Binding
if let value1 = myOptional1, value2 = myOptional2 {
// code if myOptional1 and myOptional2 is not nil
} else {
// code if myOptional1 and myOptional2 is nil
}
//Non-Preferred Method Optional Binding
if let value1 = myOptional1 {
if let value2 = myOptional2 {
// code if myOptional is not nil
} else {
// code if myOptional2 is nil
}
} else {
// code if myOptional1 is nil
}
使用可选链而不是可选绑定进行多次解包
当我们需要解包多层时,我们应该使用可选链而不是多个可选绑定语句。以下示例显示了首选和非首选方法:
//Preferred Method
if let color = jon.pet?.collar?.color {
print("The color of the collar is \(color)")
} else {
print("Cannot retrieve color")
}
//Non-Preferred Method
if let tmpPet = jon.pet, tmpCollar = tmpPet.collar {
print("The color of the collar is \(tmpCollar.color)")
} else {
print("Cannot retrieve color")
}
使用类型推断
我们不应该定义变量类型,而应该让 Swift 推断类型。我们只有在定义变量或常量时没有为其赋值的情况下才应该定义变量或常量类型。让我们看看以下代码:
//Preferred method
var myVar = "String Type" //Infers a String type
var myNum = 2.25 //Infers a Double type
//Non-Preferred method
var myVar: String = "String Type"
var myNum: Double = 2.25
使用集合的简写声明
当声明原生 Swift 集合类型时,我们应该使用简写语法,除非绝对必要,否则我们应该初始化集合。以下示例显示了首选和非首选方法:
//Preferred Method
var myDictionary: [String: String] = [:]
var strArray: [String] = []
var strOptional: String?
//
//Non-Preferred Method
var myDictionary: Dictionary<String,String>
var strArray: Array<String>
var strOptional: Optional<String>
使用 for-in 循环而不是 for 循环
我们应该使用 for-in 循环而不是 for 循环,尤其是在遍历集合时。以下示例显示了首选和非首选方法:
//Preferred Method
for str in strArray {
print(str)
}
for num in 0...3 {
print(num)
}
//
//Non-Preferred Method
for var i = 0; i < strArray.count; i++ {
print(strArray[i])
}
for var num = 0; num <= 3; num++ {
print(num)
}
使用 switch 而不是多个 if 语句
wherever possible,我们应该优先使用单个 switch 语句而不是多个 if 语句。以下示例显示了首选和非首选方法:
//Preferred Method
let speed = 300000000
switch speed {
case 300000000:
print("Speed of light")
case 340:
print("Speed of sound")
default:
print("Unknown speed")
}
//Non-preferred Method
let speed = 300000000
if speed == 300000000 {
print("Speed of light")
} else if speed == 340 {
print("Speed of sound")
} else {
print("Unknown speed")
}
不要在应用程序中留下注释掉的代码
如果我们在尝试替换代码块时将其注释掉,一旦我们对更改感到满意,我们应该移除我们注释掉的代码。大量注释掉的代码块会使代码库看起来杂乱无章,难以跟踪。
大中央调度
在第十四章中讨论的 Grand Central Dispatch,Swift 中的并发与并行,是一个基于 C 的低级 API,它允许将特定任务排队执行,并在任何可用的处理器核心上调度执行。
在dispatch_queue_create()函数中设置属性
当使用dispath_queue_create()函数创建一个串行队列时,我们可以将attribute参数设置为nil(这定义了一个串行队列);然而,我们应该始终将属性设置为DISPATCH_QUEUE_SERIAL或DISPATCH_QUEUE_CONCURRENT以明确定义我们正在创建的队列类型。以下示例显示了首选和非首选方法:
//Preferred method
let queue2 = dispatch_queue_create("squeue.hoffman.jon", DISPATCH_QUEUE_SERIAL)
//Non-Preferred method
let queue2 = dispatch_queue_create("squeue.hoffman.jon", nil)
为dispatch_queue_create()函数的标签参数使用反向 DNS 名称
我们可以将dispatch_queue_create函数的tag参数设置为任何有效的字符串;然而,为了保持一致性和易于故障排除,我们应该始终使用反向 DNS 命名方案。以下代码显示了首选和非首选方法:
//Preferred method
let queue2 = dispatch_queue_create("squeue.hoffman.jon", DISPATCH_QUEUE_SERIAL)
let queue = dispatch_queue_create("cqueue.hoffman.jon", DISPATCH_QUEUE_CONCURRENT)
//Non-Preferred method
let queue2 = dispatch_queue_create("Serial_Queue", DISPATCH_QUEUE_SERIAL)
let queue = dispatch_queue_create("Concurrent_Queue", DISPATCH_QUEUE_CONCURRENT)
使用dispatch_get_global_queue()而不是dispatch_queue_create()
虽然使用dispatch_queue_create()函数创建一个新的并发队列是完全可接受的,但我们应更倾向于使用dispatch_get_global_queue()函数来检索一个已经创建并可供使用的并发队列。以下示例显示了首选和非首选方法:
//Preferred Method
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
//Non-preferred Method
let queue = dispatch_queue_create("cqueue.hoffman.jon", DISPATCH_QUEUE_CONCURRENT)
摘要
当我们在团队环境中开发应用程序时,拥有一个明确的编码风格,团队中的每个人都遵守,是非常重要的。这使我们能够拥有一个易于阅读和维护的代码库。
如果一个风格指南保持静态时间过长,这意味着它可能没有跟上语言中的最新变化。过长的时间因语言而异。例如,对于 C 语言,过长的时间将以年为单位定义,因为语言非常稳定;然而,对于 Swift,语言非常新,变化来得相当频繁,所以过长的时间可能被定义为几个月。
建议我们将我们的风格指南保存在版本控制系统中,这样我们就可以在需要时参考旧版本。这允许我们在查看旧代码时检索旧版本的风格指南。
第十六章:使用 Swift 进行网络开发
我在大学期间选修了几门网络课程,至今仍记得在那些课程中搭建我的第一个 Novell NetWare 网络。看到并学习计算机如何在网络上进行通信,我感到无比着迷。然后,在 20 世纪 90 年代初,我购买了第一台调制解调器,并开始拨打我所在地区的公告板服务。这真的很令人兴奋,因为现在我可以连接到我所在城市的公告板服务。这使我能够从这些公告板服务下载和上传文件,我开始下载我能找到的所有关于计算机通信的内容。这导致了我早期在网络安全和管理领域的职业生涯。然后,当我的第一个女儿出生时,我决定我不想总是处于待命状态,所以我回到了最初让我接触计算机的事情,那就是编程。然而,我仍然非常享受网络和网络安全的领域。我在网络和编程方面的背景使我能够对两者都有独特的理解。今天构成网络的东西(互联网和 TCP/IP 网络)与我在大学时构成网络的东西完全不同,但好事是,我们的应用程序通过网络进行通信的方式已经变得更加标准化,这使得编写通过网络进行通信的应用程序变得更加容易。
在本章中,你将学习以下主题:
-
如何使用
NSURLSessionAPI 进行 HTTP GET 请求 -
如何使用
NSURLSessionAPI 进行 HTTP POST 请求 -
如何使用系统配置 API 检查我们的网络连接
-
如何使用
RSNetworking2库轻松地将网络功能添加到你的应用程序中
网络开发是什么?
网络开发是编写代码,使我们的应用程序能够从远程服务或设备发送和接收数据。在本章的引言中,我提到了购买我的第一台调制解调器,并连接到我所在城市的公告板服务。这些公告板服务中的大多数使用单个调制解调器,这意味着在任何时候只有一个用户可以连接到它们。对于在互联网环境中长大的人来说,这些公告板看起来非常奇怪和过时;然而,在当时,它们是计算机共享信息的方式。当时,能够连接到镇上的计算机并上传/下载文件是非常令人惊叹的。然而,如今,我们与世界各地的服务和设备进行通信,却无需多想。
在我刚开始编写应用程序的时候,开发一个通过网络连接进行通信的应用程序是很罕见的,而且也很难找到有网络开发经验的开发者。在当今世界,几乎每个应用程序都有某种网络通信的要求。
在本章中,我们将向您展示如何连接到基于 表示状态转移 (REST) 的网络服务。表示状态转移是万维网的软件架构风格。通常,这些服务通过 HTTP 使用与网页浏览器相同的 HTTP 动词(get、put、delete 和 post)进行通信。
在本章中,我们将使用苹果提供的基于 REST 的服务,允许开发者搜索 iTunes Store。在本章中,我们将使用苹果的服务进行几个示例。苹果对这个服务进行了很好的文档记录。文档可以在www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html找到。
如果您想对网络开发进行更详细的讨论,我建议您阅读我的第一本书,书名为 iOS 和 OS X 网络编程食谱,由 Packt Publishing 出版。在本章中,我们将专注于如何连接到基于标准的 REST 服务。
在我们查看如何连接到 REST 服务之前,让我们看看我们将要使用的苹果网络 API 中的类。这些类是苹果强大的 URL 加载系统的一部分。
URL 会话类的概述
苹果的 URL 加载系统是一个框架,提供了一系列用于与 URL 交互的类。使用这些类一起,我们可以与使用标准互联网协议的服务进行通信。在本章中,我们将使用以下类来连接到并从 REST 服务检索信息:
-
NSURLSession: 这是主要的会话对象。它被编写为旧NSURLConnectionAPI 的替代品。 -
NSURLSessionConfiguration: 这用于配置NSURLSession对象的行为。 -
NSURLSessionTask: 这是一个处理从 URL 检索的数据的基类。苹果为NSURLSessionTask类提供了三个具体的子类。 -
NSURL: 这是一个表示要连接到的 URL 的对象。 -
NSMutableURLRequest: 这个类包含我们正在发出的请求的信息,并由NSURLSessionTask服务用于发出请求。 -
NSHTTPURLResponse: 这个类包含对我们请求的响应。
现在,让我们更深入地看看这些类中的每一个,以便我们对每个类的基本功能有一个基本的了解。
NSURLSession
在 iOS 7 和 OS X 10.9 之前,当开发者想要从 URL 检索内容时,他们使用 NSURLConnection API。从 iOS 7 和 OS X 10.9 开始,NSURLSession 成为首选 API。NSURLSession API 可以被视为对较旧的 NSURLConnection API 的改进。
NSURLSession 对象提供了一个与各种协议(如 HTTP 和 HTTPS)交互的 API。会话对象,即 NSURLSession 的实例,管理这种交互。这些会话对象高度可配置,这使我们能够控制我们的请求方式以及我们如何处理返回的数据。
与大多数网络 API 一样,NSURLSession 是异步的。这意味着我们必须提供一种方式将服务的响应返回给需要它的代码。从会话返回结果的最流行方式是将完成处理程序块(闭包)传递给会话。然后,当服务成功响应或我们收到错误时,将调用此完成处理程序。本章中的所有示例都使用完成处理程序来处理从服务返回的数据。
NSURLSessionConfiguration
NSURLSessionConfiguration 类定义了在使用 NSURLSession 对象连接到 URL 时使用的行为和策略。当使用 NSURLSession 对象时,我们通常首先创建一个 NSURLSessionConfiguration 实例,因为创建 NSURLSession 类的实例时需要这个类的实例。
NSURLSessionConfiguration 类定义了三种会话类型:
-
默认会话配置: 这种配置的行为类似于
NSURLConnectionAPI -
临时会话配置: 这种配置的行为类似于默认会话配置,不同之处在于它不会将任何内容缓存在磁盘上
-
后台会话配置: 这种会话允许在应用程序在后台运行时执行上传和下载操作
重要的一点是,在使用它来创建 NSURLSession 类的实例之前,我们应该确保适当地配置 NSURLSessionConfiguration 对象。当会话对象被创建时,它会创建我们提供的配置对象的副本。一旦创建会话对象,对配置对象所做的任何更改都会被会话忽略。如果我们需要更改配置,我们必须创建 NSURLSession 类的另一个实例。
NSURLSessionTask
NSURLSession 服务使用 NSURLSessionTask 类的实例来调用我们正在连接的服务。NSURLSessionTask 类是一个基类,苹果提供了三个具体的子类供我们使用:
-
NSURLSessionDataTask: 这会将响应,以内存中的形式,直接作为一个或多个NSData对象返回给应用程序。这是我们通常最常使用的任务。 -
NSURLSessionDownloadTask: 这会将响应直接写入一个临时文件。 -
NSURLSessionUploadTask: 这用于制作需要请求体(如 POST 或 PUT 请求)的请求。
重要的一点是,任务不会向服务发送请求,直到我们调用 resume() 方法。
使用 NSURL 类
NSURL对象代表我们将要连接到的 URL。NSURL类不仅限于表示远程服务器的 URL,它也可以用来表示磁盘上的本地文件。在本章中,我们将专门使用NSURL类来表示我们连接到的远程服务的 URL。
NSMutableURLRequest
NSMutableURLRequest类是NSURLRequest类的可变子类,它表示一个 URL 加载请求。我们使用NSMutableRequest类来封装我们的 URL 和请求属性。
重要的是要理解,NSMutableURLRequest类用于封装我们请求所需的信息,但它并不实际发出请求。为了发出请求,我们使用NSURLSession和NSURLSessionTask类的实例。
NSURLHTTPResponse
NSURLHTTPResponse类是NSURLResponse类的子类,它封装了与 URL 请求响应相关的元数据。NSURLHTTPResponse类提供了访问与 HTTP 响应相关特定信息的方法。具体来说,这个类允许我们访问 HTTP 头字段和响应状态码。
我们在本节中简要介绍了多个类,它们如何实际结合在一起可能并不清楚;然而,一旦你看到本章稍后的一些示例,就会变得非常清晰。在我们进入示例之前,让我们快速了解一下我们将要连接到的服务类型。
REST 网络服务
REST 已成为设备之间无状态通信最重要的技术之一。由于基于 REST 的服务轻量级和无状态的特性,随着更多设备连接到互联网,其重要性可能会继续增长。
REST 是一种用于设计网络应用程序的架构风格。REST 背后的理念是,我们不是使用复杂的机制,如 SOAP 或 CORBA 在设备之间进行通信,而是使用简单的 HTTP 请求进行通信。虽然从理论上讲,REST 不依赖于互联网协议,但它几乎总是使用它们来实现。因此,当我们访问 REST 服务时,我们几乎总是以与我们的网络浏览器与这些服务器交互相同的方式与 Web 服务器进行交互。
REST 网络服务使用 HTTP POST、GET、PUT 或 DELETE 方法。如果我们考虑一个标准的 CRUD(创建/读取/更新/删除)应用程序,我们会使用 POST 请求来创建或更新数据,使用 GET 请求来读取数据,使用 DELETE 请求来删除数据。
当我们在浏览器的地址栏中输入一个 URL 并按Enter键时,我们通常是在向服务器发送一个 GET 请求,并要求它发送与该 URL 相关的网页。当我们填写一个网页表单并点击提交按钮时,我们通常是在向服务器发送一个 POST 请求。然后我们在 POST 请求的主体中包含网页表单的参数。
现在,让我们看看如何使用苹果的网络 API 发起 HTTP GET 请求。
发起 HTTP GET 请求
在本例中,我们将向苹果的 iTunes 搜索 API 发起一个 GET 请求,以获取与搜索词 Jimmy Buffett 相关的项目列表。由于我们从服务中检索数据,根据 REST 标准,我们应该使用 GET 请求来检索数据。
虽然 REST 标准是使用 GET 请求从服务中检索数据,但没有任何阻止 Web 服务的开发者使用 GET 请求来创建或更新数据对象。不推荐以这种方式使用 GET 请求,但请记住,有些服务并不遵循 REST 标准。
以下代码向苹果的 iTunes 搜索 API 发起请求,并将结果打印到控制台:
public typealias dataFromURLCompletionClosure = (NSURLResponse!, NSData!) -> Void
public func sendGetRequest(handler: public func getConnect(
handler: dataFromURLCompletionClosure) {
let sessionConfiguration =
NSURLSessionConfiguration.defaultSessionConfiguration();
let urlString =
"https://itunes.apple.com/search?term=jimmy+buffett"
if let encodeString =
urlString.stringByAddingPercentEncodingWithAllowedCharacters(
NSCharacterSet.URLQueryAllowedCharacterSet()),
url = NSURL(string: encodeString) {
let request = NSMutableURLRequest(URL:url)
request.HTTPMethod = "GET"
let urlSession = NSURLSession(
configuration:sessionConfiguration, delegate: nil,
delegateQueue: nil)
let sessionTask =
urlSession.dataTaskWithRequest(request) {
(data, response, error) in
handler(response, data)
}
sessionTask.resume()
}
}
我们首先创建一个名为DataFromURLCompletionClosure的类型别名。DataFromURLCompletionClosure类型将用于本章的 GET 和 POST 示例。如果您不熟悉使用typealias对象来定义闭包类型,请参阅第十二章,使用闭包,以获取更多信息。
然后我们创建一个名为sendGetRequest()的函数,该函数将用于向苹果的 iTunes API 发起 GET 请求。这个函数接受一个名为 handler 的参数,它是一个符合DataFromURLCompletionClosure类型的闭包。handler 闭包将用于返回请求的结果。
在我们的sendGetRequest()方法中,我们首先使用defaultSessionConfiguration()方法创建一个NSURLSessionConfiguration类的实例,这会创建一个默认会话配置实例。如果我们需要,我们可以在创建后修改会话配置属性,但在这个例子中,我们想要的是默认配置。
在创建我们的会话配置之后,我们创建 URL 字符串。这是我们要连接到的服务的 URL。在 GET 请求中,我们将参数放在 URL 本身中。在这个特定的例子中,https://itunes.apple.com/search是 Web 服务的 URL。然后我们用问号(?)跟随 Web 服务 URL,这表示 URL 字符串的其余部分由 Web 服务的参数组成。
参数的形式是键/值对,这意味着每个参数都有一个键和一个值。在 URL 中,参数的键和值由等号(=)分隔。在我们的例子中,键是term,值是jimmy+buffett。接下来,我们通过stringByAddingPercentEncodingWithAllowedCharacters()方法运行我们刚刚创建的 URL 字符串,以确保我们的 URL 字符串已正确编码。我们使用URLQueryAllowedCharacterSet字符集与此方法一起使用,以确保我们有一个有效的 URL 字符串。
接下来,我们使用我们刚刚构建的 URL 字符串来创建一个名为 url 的 NSURL 实例。由于我们正在执行 GET 请求,这个 NSURL 实例将代表网络服务的位置和我们发送给它的参数。
我们使用我们刚刚创建的 NSURL 实例创建一个 NSMutableURLRequest 类的实例。我们使用 NSMutableURLRequest 类而不是 NSURLRequest 类,这样我们就可以设置我们请求所需的属性。在这个例子中,我们设置了 HTTPMethod 属性;然而,我们也可以设置其他属性,例如超时间隔或向我们的 HTTP 标头添加项。
现在,我们使用在 sendGetRequest() 函数开头创建的 sessionConfiguration 变量(NSURLSessionConfiguration 类的实例)来创建 NSURLSession 类的实例。NSURLSession 类提供了我们将用于连接到苹果的 iTunes 搜索 API 的 API。在这个例子中,我们使用 NSURLSession 实例的 dataTaskWithRequest() 方法来返回名为 sessionTask 的 NSURLSessionDataTask 实例。
sessionTask 实例是向 iTunes 搜索 API 发送请求的部分。当我们从服务收到响应时,我们使用处理程序回调来返回 NSURLResponse 对象和 NSData 对象。NSURLResponse 包含有关响应的信息,而 NSData 实例包含响应的主体。
最后,我们调用 NSURLSessionDataTask 实例的 resume() 方法来向网络服务发送请求。记住,正如我们之前提到的,一个 NSURLSessionTask 实例将不会向服务发送请求,直到我们调用 resume() 方法。
现在,让我们看看我们如何调用 sendGetRequest() 函数。首先,我们需要做的是创建一个闭包,该闭包将被传递给 sendGetRequest() 函数并在收到网络服务的响应时被调用。在这个例子中,我们将简单地打印响应到控制台。由于响应是 JSON 格式,我们可以使用在 第八章 中描述的 NSJSONSerialization 类来解析响应;然而,由于本章是关于网络编程,我们只需将响应打印到控制台。以下是代码:
var printResultsClosure: HttpConnect.DataFromURLCompletionClosure = {
if let data = $1 {
let sString = NSString(data: data, encoding: NSUTF8StringEncoding)
print(sString)
}
}
我们定义这个闭包,命名为 printResultsClosure,为 DataFromURLCompletionClosure 类型的实例。在闭包内部,我们解包第一个参数并将值设置为名为 data 的常量。如果第一个参数不是 nil,我们将数据常量转换为 NSString 类的实例,然后将其打印到控制台。
现在,让我们用以下代码调用 sendGetRequest() 方法:
let aConnect = HttpConnect()
aConnect.sendGetRequest(printResultsClosure)
此代码创建了一个HttpConnect类的实例,然后调用sendGetRequest()方法,将printResultsClosure闭包作为唯一参数传递。如果我们连接到互联网时运行此代码,我们将收到一个包含与 iTunes 上吉米·巴菲特相关项目的 JSON 响应。
现在我们已经看到了如何发起一个简单的 HTTP GET 请求,让我们看看如何向一个网络服务发起 HTTP POST 请求。
发起 HTTP POST 请求
由于苹果的 iTunes,API 使用 GET 请求来检索数据。在本节中,我们将使用免费的httpbin.org服务向您展示如何发起 POST 请求。httpbin.org提供的 POST 服务可以在httpbin.org/post找到。此服务将回显它接收到的参数,以便我们可以验证我们的请求是否正确发起。
当我们发起 POST 请求时,我们通常有一些想要发送或提交给服务器的数据。这些数据以键/值对的形式存在。这些对之间由一个和号(&)符号分隔,每个键与其值之间由一个等号(=)分隔。作为一个例子,假设我们想要提交以下数据到我们的服务:
firstname: Jon
lastname: Hoffman
age: 47 years
POST 请求的正文将采用以下格式:
firstname=Jon&lastname=Hoffman&age=47
一旦我们有了合适格式的数据,我们就会使用dataUsingEncoding()方法,就像我们在对 GET 请求进行正确编码 POST 数据时所做的那样。
由于发送到服务器的数据是键/值格式,因此在发送到服务之前,最合适的方式是使用Dictionary对象来存储这些数据。考虑到这一点,我们需要创建一个方法,该方法将接受一个Dictionary对象并返回一个字符串对象,该对象可以用于 POST 请求。以下代码将实现这一点:
func dictionaryToQueryString(dict: [String : String]) -> String {
var parts = [String]()
for (key, value) in dict {
let part: String = key + "=" + value
parts.append(part);
}
return parts.joinWithSeparator("&")
}
此函数遍历Dictionary对象中的每个键/值对,并创建一个包含键和值(由等号=分隔)的String对象。然后我们使用joinWithSeperator()函数将数组中的每个项目连接起来,每个项目由指定的字符串分隔。在我们的例子中,我们想要用和号符号(&)分隔每个字符串。然后我们将这个新创建的字符串返回给调用它的代码。
现在,让我们创建我们的sendPostRequest()函数,该函数将发送 POST 请求到httpbin.org的 POST 服务。我们将看到这个sendPostRequest()函数和我们在本章的制作 HTTP GET 请求部分中展示的sendGetRequest()函数之间有很多相似之处。让我们看一下以下代码:
public func sendPostRequest(handler: dataFromURLCompletionClosure) {
let sessionConfiguration =
NSURLSessionConfiguration.defaultSessionConfiguration()
let urlString = "http://httpbin.org/post"
if let encodeString =
urlString.stringByAddingPercentEncodingWithAllowedCharacters(
NSCharacterSet.URLQueryAllowedCharacterSet()),
url = NSURL(string: encodeString) {
let request = NSMutableURLRequest(URL:url)
request.HTTPMethod = "POST"
let params = dictionaryToQueryString(["One":"1 and 1", "Two":"2 and 2"])
request.HTTPBody = params.dataUsingEncoding(
NSUTF8StringEncoding, allowLossyConversion: true)
let urlSession = NSURLSession(
configuration:sessionConfiguration, delegate: nil, delegateQueue: nil)
let sessionTask = urlSession.dataTaskWithRequest(request) {
(data, response, error) in
handler(response, data)
}
sessionTask.resume()
}
}
现在,让我们逐步分析这段代码。注意,我们正在使用与 sendGetRequest() 函数相同的类型别名,名为 DataFromURLCompletionClosure。如果你不熟悉使用 typealias 对象来定义闭包类型,请参阅 第十二章,使用闭包,以获取更多信息。
sendPostRequest() 函数接受一个名为 handler 的参数,它是一个符合 DataFromURLCompletionClosure 类型的闭包。当服务对我们的请求做出响应后,handler 闭包将被用来处理来自 httpbin.org 服务的数据。
在我们的 sendPostRequest() 方法中,我们首先使用 defaultSessionConfiguration() 方法创建一个 NSURLSessionConfiguration 类的实例,该方法创建一个默认会话配置实例。我们可以在创建后会话配置属性,但在这个例子中,我们想要的是默认配置。
在我们创建了会话配置之后,我们创建我们的 URL 字符串。这是我们要连接到的服务的 URL。在这个例子中,URL 是 httpbin.org/post。接下来,我们将我们刚刚创建的 URL 字符串通过 stringByAddingPercentEncodingWithAllowedCharacters() 方法运行,以确保我们的 URL 字符串被正确编码。我们使用 URLQueryAllowedCharacterSet 字符集与这个方法一起使用,以确保我们有一个有效的 URL 字符串。
接下来,我们使用我们刚刚构建的 URL 字符串来创建一个名为 url 的 NSURL 类实例。由于这是一个 POST 请求,这个 NSURL 实例将代表我们要连接到的网络服务的位置。
我们现在使用我们刚刚创建的 NSURL 实例来创建一个 NSMutableURLRequest 类的实例。我们使用 NSMutableURLRequest 类而不是 NSURLRequest 类,这样我们就可以设置我们请求所需的属性。在这个例子中,我们设置了 HTTPMethod 属性;然而,我们也可以设置其他属性,例如超时间隔或向我们的 HTTP 头中添加项。
现在,我们使用我们在本节开头展示的 dictionaryToQueryString() 函数来构建我们要发送到服务器的数据。我们使用 dataUsingEncoding() 函数确保我们的数据在发送到服务器之前被正确编码,最后,数据被添加到 NSMutableURLRequest 实例的 HTTPBody 属性中。
我们使用在函数开头创建的 sessionConfiguration 变量(NSURLSessionConfiguration 类的实例)来创建 NSURLSession 类的实例。NSURLSession 类提供了我们将用于连接到 httpbin.org POST 服务的 API。在这个例子中,我们使用 NSURLSession 实例的 dataTaskWithRequest() 方法来返回一个名为 sessionTask 的 NSURLSessionDataTask 类的实例。
sessionTask 实例是用来向 httpbin.org POST 服务发起请求的。当我们从服务收到响应时,我们使用处理程序回调来返回 NSURLResponse 对象和 NSData 对象。NSURLResponse 包含有关响应的信息,而 NSData 实例包含响应的主体。
最后,我们调用 NSURLSessionDataTask 实例的 resume() 方法来向网络服务发起请求。记住,正如我们之前提到的,NSURLSessionTask 类不会向服务发送请求,直到我们调用 resume() 方法。
我们可以像调用 sendGetRequest() 方法一样,以完全相同的方式调用 sendPostRequest() 方法。
在开发通过互联网与其他设备和服务通信的应用程序时,验证我们是否有一个网络连接是一个好的做法。在开发移动应用程序时,验证我们不是在使用移动连接(3G、4G 等等)传输大量数据也是一个好的做法。
让我们看看如何验证我们是否有一个网络连接以及我们有什么类型的连接。
检查网络连接
随着我们创建通过互联网与其他设备和服务通信的应用程序,我们最终将想要验证在发起网络调用之前我们已经建立了网络连接。当我们编写移动应用程序时,还需要考虑用户所拥有的网络连接类型。作为移动应用程序开发者,我们需要记住,我们的用户可能有一个限制每月可以发送/接收的数据量的移动数据计划。如果他们超过了这个限制,他们可能需要支付额外的费用。如果我们的应用程序发送大量数据,那么在用户处于蜂窝网络的情况下,在发送这些数据之前提醒用户可能是合适的。
下一个示例将展示我们如何验证我们是否有一个网络连接,它还告诉我们我们有什么类型的连接。我们将首先导入系统配置 API,并定义一个包含不同连接类型的枚举。我们将以这种方式导入系统配置 API:
import SystemConfiguration
我们创建一个 ConnectionType 枚举。这个枚举将被用作 networkConnectionType() 的返回类型:
public enum ConnectionType {
case NONETWORK
case MOBILE3GNETWORK
case WIFINETWORK
}
现在,让我们看看检查网络连接类型的代码:
public func networkConnectionType(hostname: NSString) -> ConnectionType {
let reachabilityRef = SCNetworkReachabilityCreateWithName(nil,hostname.UTF8String)
var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags()
SCNetworkReachabilityGetFlags(reachabilityRef!, &flags)
let reachable: Bool = (flags.rawValue & SCNetworkReachabilityFlags.Reachable.rawValue) != 0
let needsConnection: Bool = (flags.rawValue & SCNetworkReachabilityFlags.ConnectionRequired.rawValue) != 0
if reachable && !needsConnection {
// what type of connection is available
let isCellularConnection = (flags.rawValue & SCNetworkReachabilityFlags.IsWWAN.rawValue) != 0
if isCellularConnection {
// cellular connection available
return ConnectionType.MOBILE3GNETWORK
} else {
// wifi connection available
return ConnectionType.WIFINETWORK
}
}
return ConnectionType.NONETWORK // no connection at all
}
networkConnectionType()函数首先创建一个SCNetworkReachability引用。为了创建SCNetworkRechabilityRef引用,我们使用SCNetworkReachabilityCreateWithName()函数,该函数创建了一个指向提供的宿主的可达性引用。
在我们获取到SCNetworkReachabilityRef引用之后,我们需要从引用中检索SCNetworkReachabilityFlags枚举。这是通过SCNetworkReachabilityGetFlags()函数完成的。
一旦我们有了网络可达性标志,我们就可以开始测试我们的连接。我们使用位与(&)运算符来查看主机是否可达,以及在我们能够连接到主机之前是否需要建立连接(needsConnection)。如果可达标志为假(我们目前无法连接到主机),或者如果needsConnection为真(我们需要在连接之前建立连接),我们返回NONETWORK,这意味着主机当前不可达。
如果我们能够连接到主机,然后我们会检查是否有蜂窝连接,通过再次检查网络可达性标志。如果有蜂窝连接,我们返回MOBILE3GNETWORK,否则,我们假设我们有一个 Wi-Fi 连接,并返回WIFINETWORK。
注意
如果你正在编写连接到互联网上其他设备或服务的应用程序,我建议将此功能放入标准库中使用,因为你会经常需要检查网络连接性,以及你所拥有的连接类型。
现在我们已经看到了如何使用苹果的网络 API 连接到远程服务,我想展示一个你可以用在你的应用程序中的网络库。这个网络库使得连接到互联网上各种类型的服务变得非常简单和容易。这是一个我创建和维护的库,但我肯定会欢迎任何愿意为代码库做出贡献的人。这个库叫做RSNetworking。
Swift 2 的 RSNetworking2
你可以在 GitHub 上找到RSNetworking2,链接为github.com/hoffmanjon/RSNetworking2
RSNetworking2库是一个完全用 Swift 编程语言编写的网络库。RSNetworking2是使用苹果强大的 URL 加载系统(developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html)构建的,该系统具有我们在此章中较早使用的NSURLSession类。RSNetworking2的主要设计目标是使开发者能够轻松快速地将强大的异步网络请求添加到他们用 Swift 编写的应用程序中。
我们可以使用RSNetworking2的以下三种方式:
-
RSURLRequest:此 API 提供了一个非常简单且易于使用的接口,用于向服务发送单个 GET 请求。 -
RSTransaction和RSTransactionRequest:这些 API 提供了一种非常强大且灵活的方式来对服务发起 GET 和 POST 请求。此 API 还使得对服务发起多个请求变得非常容易。 -
Extensions:RSNetworking2为UIImageView和UIButton类提供了扩展,以便从 URL 动态加载图像,并在加载后将其插入到UIImageView或UIButton类中。
让我们更详细地查看这些 API,并提供一些使用示例。
RSURLRequest
使用RSURLRequest API,我们可以向服务发起 GET 请求,我们只需要提供 URL 和我们希望发送给服务的参数。RSURLRequest API 公开了四个函数。这些函数如下:
-
dataFromURL(url: NSURL, completionHandler handler: RSNetworking.dataFromURLCompletionClosure): 这个方法从 URL 中检索一个NSData对象。这是主要函数,并被其他三个函数用来在转换成所需格式之前检索NSData对象。 -
stringFromURL(url: NSURL, completionHandler handler: RSNetworking.stringFromURLCompletionClosure): 这个方法从 URL 中检索一个NSString对象。此函数使用dataFromURL()函数来检索一个NSData对象,并将其转换为NSString对象。 -
dictionaryFromJsonURL(url: NSURL, completionHandler handler: RSNetworking.dictionaryFromURLCompletionClosure): 这个方法从 URL 中检索一个NSDictionary对象。此函数使用dataFromURL()函数来检索一个NSData对象,并将其转换为NSDictionary对象。从 URL 返回的数据应该是 JSON 格式,以便此函数能正常工作。 -
imageFromURL(url: NSURL, completionHandler handler: RSNetworking.imageFromURLCompletionClosure): 这个方法从 URL 中检索一个UIImage对象。此函数使用dataFromURL()函数来检索一个NSData对象,并将其转换为UIImage对象。
现在,让我们看看如何使用RSURLRequest API 的示例。在这个例子中,我们将向苹果的 iTunes 搜索 API 发起请求,就像我们在本章的制作 HTTP GET 请求部分所做的那样:
func rsURLRequestExample() {
var client = RSURLRequest()
if let testURL = NSURL(string:"https://itunes.apple.com/search?term=jimmy+buffett&m edia=music") {
client.dictionaryFromJsonURL(testURL, completionHandler: resultsHandler)
}
}
让我们逐步分析这段代码。我们首先创建一个RSURLRequest类的实例和一个NSURL类的实例。NSURL实例代表我们希望连接到的服务的 URL,由于我们正在发起 GET 请求,它还包含我们发送给服务的参数。如果我们回想一下之前的制作 HTTP GET 请求部分,当我们发起 HTTP GET 请求时,我们发送给服务的参数包含在 URL 本身中。
苹果的 iTunes 搜索 API 以 JSON 格式返回搜索结果。我们可以在 API 文档中看到这一点,也可以通过将搜索结果打印到控制台来验证;因此,我们将使用 RSURLRequest 类的 dictionaryFromJsonURL() 方法来向服务发出请求。如果我们想获取数据,我们也可以使用 dataFromURL() 或 stringFromURL() 方法,但此方法专门编写来处理从基于 REST 的网络服务返回的 JSON 数据。
dictionaryFromJsonURL() 方法将 NSURLSession 请求返回的数据转换为 NSDictionary 对象。我们在这里使用 NSDictionary 对象而不是 Swift 的 Dictionary 对象,因为网络服务可能返回多种类型(字符串、数组、数字等),并且如果我们回想起来,Swift 的 Dictionary 对象只能有一个键类型和一个值类型。
当我们调用 dictionaryFromJsonURL() 方法时,我们传递我们想要连接的 URL 以及一个完成处理程序,该处理程序将在服务信息返回并转换为 NSDicationary 对象后调用。
现在,让我们看看我们的完成处理程序:
var resultsHandler:RSURLRequestRSURLRequestRSURLRequestRSURLRequest.dictionaryFromURLCompletionClosure = {
var response = $0
var responseDictionary = $1
var error = $2
if error == nil {
let res = "results"
if let results = responseDictionary[res] as? NSArray {
print(results[0])
}
else {
print("Problem with data")
}
}
else {
//If there was an error, log it
print("Error : \(error)")
}
}
我们的完成处理程序是 RSURLRequest.dictionaryFromURLCompletionClosure 类型。这种类型与 RSTransactionRequest.dictionaryFromRSTransactionCompletionClosure 类型的定义方式相同,这使我们能够为 RSURLRequests 和 RSTransactionRequest 请求使用相同的闭包。
我们通过检索传递的三个参数并将它们分配给 response、responseDictionary 和 error 变量来开始完成处理程序。然后我们检查 error 变量以查看它是否为 nil。如果是 nil,则表示我们收到了有效的响应,可以检索 NSDictionary 对象的值。
在这个例子中,我们从服务返回的 NSDictionary 对象中检索与 results 键关联的 NSArray 值。这个 NSArray 值将包含与我们的搜索词关联的 iTunes 商店中的项目列表。一旦我们有了 NSArray 值,我们就将数组的第一个元素打印到控制台。
RSURLRequest API 非常适合向服务发出单个 GET 请求。现在,让我们看看 RSTransaction 和 RSTransactionRequest API,这些 API 可以用于 POST 和 GET 请求,并且在我们需要向同一服务发出多个请求时应使用。
RSTransaction 和 RSTransactionRequest
RSTransaction 和 RSTransactionRequest 类允许我们配置一个事务(RSTransaction),然后使用该事务向服务发出请求(RSTransactionRequest)。使这个 API 如此强大的一个因素是,我们通过简单地更新事务并重新提交它来执行后续请求是多么容易。让我们看看这两个类公开的 API。
RSTransaction
RSTransaction 类定义了我们希望进行的交易。它公开了四个属性和一个初始化器。
属性如下:
-
TransactionType: 这定义了 HTTP 请求方法。目前定义了三种类型——GET、POST 和 UNKNOWN。只有 GET 和 POST 实际上发送请求。 -
baseURL: 这是用于请求的基本 URL。这通常看起来像itunes.apple.com。如果我们使用非标准端口,我们会在服务器 URL 后面跟一个冒号和端口号,例如http://mytestserver:8080。 -
path: 这是将被添加到基本 URL 的路径。这可能是search这样的内容。它也可以包括一个更长的路径字符串,例如path/to/my/service。 -
parameters: 这是一个包含要发送到服务的参数的Dictionary对象。
初始化器如下:
init(transactionType: RSTransactionType, baseURL: String, path: String, parameters: [String: String]): 这将使用所有必需的属性初始化RSTransaction类。
RSTransactionRequest
RSTransactionRequest 类构建并发送由以下四个函数定义的请求:
-
dataFromRSTransaction(transaction: RSTransaction, completionHandler handler: RSNetworking.dataFromRSTransactionCompletionCompletionClosure): 这个函数从由RSTransaction类定义的服务中检索一个NSData对象。这是主函数,并被其他三个函数用来在将其转换为请求的格式之前检索NSData对象。 -
stringFromRSTransaction(transaction: RSTransaction, completionHandler handler: RSNetworking.stringFromRSTransactionCompletionCompletionClosure): 这个函数从由RSTransaction类定义的服务中检索一个NSString对象。这个函数使用dataFromRSTransaction()函数来检索NSData对象,并将其转换为NSString对象。 -
dictionaryFromRSTransaction(transaction: RSTransaction, completionHandler handler: RSNetworking.dictionaryFromRSTransactionCompletionCompletionClosure): 这个函数从由RSTransaction类定义的服务中检索一个NSDictionary对象。这个函数使用dataFromRSTransaction()函数来检索NSData对象,并将其转换为NSDictionary对象。从 URL 返回的数据应该是 JSON 格式,这样这个函数才能正常工作。 -
imageFromRSTransaction(transaction: RSTransaction, completionHandler handler: RSNetworking.imageFromRSTransactionCompletionCompletionClosure): 这个函数从由RSTransaction类定义的服务中检索一个UIImage对象。这个函数使用dataFromRSTransaction()函数来检索NSData对象,并将其转换为UIImage对象。
现在,让我们看看如何使用 RSTransaction 和 RSTransactionRequest 类向苹果的 iTunes 搜索 API 发出 GET 请求的示例。在这个例子中,我们将使用本章 RSURLRequest 部分中定义的相同的 resultsHandler 闭包。让我们看一下以下代码:
func rsTransactionExample() {
let rsRequest = RSTransactionRequest()
//First request
let rsTransGet = RSTransaction(transactionType: RSTransactionType.GET, baseURL: "https://itunes.apple.com", path: "search", parameters: ["term":"jimmy+buffett", "media":"music"])
rsRequest.dictionaryFromRSTransaction(rsTransGet, completionHandler: resultsHandler)
//Second request
rsTransGet.parameters = ["term":"jim", "media":"music"]
rsRequest.dictionaryFromRSTransaction(rsTransGet, completionHandler: resultsHandler)
}
在这个例子中,我们首先创建一个名为 rsRequest 的 RSTransactionRequest 类实例。这个 RSTransactionRequest 实例将被用来向我们的 RSTransaction 实例中定义的服务发送请求。
在创建 RSTransactionRequest 实例后,我们使用 RSTransaction 初始化器创建一个名为 rsTransGet 的 RSTransction 类实例。在这个初始化器中,我们按以下方式定义以下属性:
-
transactionType:transactionType被设置为RSTransactionType.GET (这也可以是RSTransactionType.POST 或RSTransactionType. UNKNOWN) -
baseURL:baseURL被设置为itunes.apple.com -
path:path被设置为搜索 -
parameters: 参数被设置为["term":"jimmy+buffett","media":"music"]
最后,我们使用 RSTransactionRequest 实例的 dictionaryFromRSTransaction() 方法。此方法接受两个参数;第一个是定义要发送的事务的 RSTransaction 实例,第二个是数据从服务返回后将被调用的完成处理程序。
正如我们之前提到的,使 RSTransaction 和 RSTransactionRequest 类易于使用的一个原因是,向同一服务进行后续请求非常简单。在我们的例子中,在发出初始请求后,我们更改参数并向同一服务发出第二个请求。需要注意的一点是,由于这些是异步请求,如果我们连续发出两个这样的请求,我们无法保证哪个请求会先返回。
现在,让我们看看 RSNetworking2 库的最后部分——扩展。
扩展
在 Swift 中,扩展向现有类添加新功能。RSNetworking2 为 UIImageView 和 UIbutton 类提供了扩展。这些扩展允许我们从 URL 加载图像,然后将其添加到 UIImageView 或 UIButton 中,一旦图像下载完成。我们还可以放置一个占位符图像,在最终图像下载之前将在 UIImageView 或 UIButton 中显示。一旦图像下载完成,占位符图像将被下载的图像替换。
UIImageView 和 UIButton 扩展都公开了四个新方法:
-
setImageForURL(url: NSString, placeHolder: UIImage): 此方法将UIImageView或UIButton扩展的图像设置为占位符图像,然后从提供的 URL 异步下载图像。一旦图像下载完成,它将用下载的图像替换占位符图像。 -
setImageForURL(url: NSString): 这个方法异步从 URL 下载图像。一旦图像下载完成,它将UIImageView或UIButton扩展的图像设置为下载的图像。 -
setImageForRSTransaction(transaction:RSTransaction, placeHolder: UIImage): 这个方法将UIImageView或UIButton中的图像设置为占位符图像,然后从提供的RSTransaction对象异步下载图像。一旦图像下载完成,它将用下载的图像替换占位符图像。 -
setImageForRSTransaction(transaction:RSTransaction): 这个方法异步从提供的RSTransaction对象下载图像。一旦图像下载完成,它将UIImageView或UIButton扩展的图像设置为下载的图像。
UIButton和UIImageView扩展的使用方式完全相同。为了了解如何使用这些扩展,让我们看看如何使用UIImageView扩展来查看从互联网下载的图像:
let url = "http://is4.mzstatic.com/image/pf/us/r30/Features/2a/b7/da/dj.kkir mfzh.100x100-75.jpg"
if let iView: UIImageView = imageView, image = UIImage(named: "loading") {
iView.setImageForURL(url, placeHolder: image)
}
在这个例子中,我们首先定义了我们图像的 URL。然后我们验证imageView变量是否包含UIImageView类的实例。请注意,我们通常不会在if-let语句中定义常量类型(UIImageView类型),但我在这个例子中定义了类型,以展示imageView常量应该是UIImageView类的实例。接下来,我们创建了一个名为loading的UIImage类的实例。这个图像将被用作占位符图像,在我们从 URL 下载最终图像的过程中显示。
现在我们已经拥有了图像的 URL 和占位符图像,我们使用setImageForURL()扩展方法。这个方法接受两个参数——下载图像的 URL 和占位符图像。一旦我们调用这个方法,RSNetworking2将UIImageView类的图像设置为提供的占位符图像,并从提供的 URL 下载图像。一旦图像下载完成,RSNetworking2将用下载的图像替换占位符图像。
在本章中,我们简要介绍了RSNetworking2的一些示例。更多示例可以在RSNetworking2的 GitHub 网站上找到:github.com/hoffmanjon/RSNetworking2。
摘要
在当今世界,一个开发者必须具备良好的网络开发知识。在本章中,我们看到了如何使用 Apple 的NSURLSession API,结合其他类,连接到基于 HTTP REST 的 Web 服务。NSURLSession API 被编写为旧版NSURLConnection API 的替代品,并且现在是进行网络请求时推荐使用的 API。
我们还看到了如何使用苹果的系统配置 API 来确定我们有什么类型的网络连接。如果我们正在为移动设备(iPhone、iPod 或 iPad)开发应用程序,了解我们是否有网络连接以及连接类型是至关重要的。
我们在本章中讨论了RSNetworking2,这是一个开源网络库,完全用 Swift 编写,由我维护。RSNetworking2使我们能够非常快速和容易地将网络功能添加到我们的应用程序中。它还向UIImageView和UIButton类添加了一个扩展,用于从互联网动态加载图像,并在下载完成后显示它们。我鼓励任何希望参与 RSNetworking 开发的人。
第十七章. 在 Swift 中采用设计模式
尽管四人帮的《设计模式:可复用面向对象软件元素》一书首次出版于 1994 年 10 月,但直到最近 6 或 7 年,我才开始关注设计模式。像大多数经验丰富的开发者一样,当我最初开始阅读有关设计模式的内容时,我认识到了很多模式,因为我已经在没有意识到它们是什么的情况下使用了它们。我必须说,自从我开始阅读有关设计模式以来,过去 6 或 7 年,我没有在不使用四人帮的至少一种设计模式的情况下编写过任何严肃的应用程序。我要告诉你,我绝对不是设计模式的狂热者,实际上,如果我陷入关于设计模式的对话,通常只有一两样我可以不用查找就能说出名字,但有一件事我记得很清楚,那就是主要模式的概念以及它们旨在解决的问题。这样,当我遇到这些问题之一时,我可以查找适当的模式并应用它。
在本章中,你将了解以下主题:
-
引用类型和值类型之间的区别
-
什么是设计模式
-
构成设计模式创建、结构和行为类别的模式类型
-
如何在 Swift 中实现建造者、工厂方法和单例创建模式
-
如何在 Swift 中实现桥接、外观和代理结构模式
-
如何在 Swift 中实现策略和命令行为模式
值类型与引用类型
在第五章中,我们讨论了值类型和引用类型之间的区别。了解这两种类型的基本区别非常重要,尤其是在我们构建代码架构时。某些设计模式与引用类型配合得最好,而其他则与值类型配合得最好;因此,知道何时使用每种类型在设计模式中很重要。考虑到这一点,让我们回顾一下引用类型和值类型之间的区别。
类是一个引用类型。这意味着当我们传递一个类的实例在我们的代码中时,我们正在传递原始实例的引用。由于我们正在传递原始实例的引用,因此对这个实例所做的任何更改都会反映到原始实例上。
结构体、枚举和元组都是值类型。当我们传递一个值类型的实例时,我们正在传递该类型的副本。这意味着对这个副本所做的任何更改都不会反映到原始实例上。让我们通过查看一些代码来了解值类型和引用类型之间的区别。我们将首先创建一个名为MyClass的类和一个名为MyStruct的结构体。这两种类型都包含一个名为number的单个属性,其类型为 Int:
class MyClass {
var number = 0
}
struct MyStruct {
var number = 0
}
现在,让我们创建一个MyClass类的实例。我们还将创建一个MyClass类型的第二个常量,它是由第一个实例创建的。然后我们将改变其中一个实例的number属性,并查看两个实例中的值:
let myClass1 = MyClass()
let myClass2 = myClass1
myClass2.number = 5
print("myClass1 = \(myClass1.number)")
print("myClass2 = \(myClass2.number)")
如果我们运行这段代码,我们会看到以下输出:
myClass1 = 5
myClass2 = 5
如我们所见,当我们在一个实例中改变了number属性时,它在两个实例中都改变了值。这也意味着在内存中只有一个MyClass类的实例。
现在,让我们看看这个相同的例子,但这次,我们将使用MyStruct结构(值类型)而不是MyClass类(引用类型):
var myStruct1 = MyStruct()
var myStruct2 = myStruct1
myStruct2.number = 5
print("myStruct1 = \(myStruct1.number)")
print("myStruct2 = \(myStruct2.number)")
如果我们运行这段代码,我们会看到以下输出:
myStruct1 = 0
myStruct2 = 5
注意,在这个例子中,当我们改变一个实例的number属性时,它并没有改变另一个实例的属性。由于myStruct2结构是通过myStruct结构的副本创建的,我们现在在内存中有两个MyStruct结构的实例。
还要注意,我们使用let关键字将MyClass类的实例定义为常量;然而,我们使用var关键字将MyStruct结构的实例定义为变量。
当一个常量引用一个引用类型的实例时,我们无法改变常量所引用的实例;然而,我们可以改变该实例的属性值,就像上一个例子中所示。当一个常量引用一个值类型的实例时,我们不仅无法改变常量所引用的实例,而且也无法改变任何属性值。Swift 的数组和字典是值类型,这就是为什么当它们被声明为常量时是不可变的。这意味着,在我们的上一个例子中,为了改变number属性的值,我们需要将MyStruct结构的实例作为变量而不是常量来创建。
注意
在本章的一些设计模式中,我们使用了结构体,而在其他一些中,我们使用了类。选择使用结构体或类来举例是基于作者的经验。对于大多数模式,选择使用结构体或类应该基于单个应用程序的需求。在每个部分中,我们将解释为什么选择结构体或类,以帮助您理解为什么做出这样的选择。
什么是设计模式
每个经验丰富的开发者都有一套非正式的策略,这些策略塑造了他/她设计和编写应用程序的方式。这些策略是由他们过去的经验和他们在以前的项目中必须克服的障碍所塑造的。尽管这些开发者可能发誓支持他们的策略,但这并不意味着他们的策略已经完全经过检验和证明。使用这些策略也引入了不同开发者之间不一致的实现。
设计模式识别一个常见的软件开发问题,并提供了解决该问题的策略。多年来,这些设计模式背后的策略已被证明能够有效地解决它们旨在解决的问题。
虽然设计模式有很多优点,并且对开发人员和架构师来说非常有益,但它们并不是一些开发者所说的解决世界饥饿问题的方案。在你的开发生涯中,你可能会遇到一些认为设计模式是不可变法则的开发人员或架构师。这些开发者通常会试图强制使用设计模式,即使它们不是必要的。一个好的经验法则是,在尝试解决问题之前,确保你有一个问题要解决。
请记住,设计模式是避免和解决常见编程问题的起点。我们可以将每个设计模式视为一道菜品的食谱,就像一个好的食谱一样,我们可以对其进行调整以满足我们的特定口味,但我们通常不想偏离原始食谱太远,因为我们可能会把它搞砸。
也有时候我们没有某个菜品的食谱,就像有时候没有设计模式来解决我们面临的问题一样。在这些情况下,我们可以利用我们对设计模式和它们背后的哲学的了解,来提出一个有效的解决方案来解决问题。
设计模式可以分为三类:
-
创建型模式:这些模式支持对象的创建
-
结构模式:这些模式关注类和对象的组合
-
行为模式:这些模式关注类之间的通信
虽然“四人帮”定义了超过 20 种设计模式,但我们将在本章中仅给出一些最流行模式的示例。让我们从创建型模式开始。
创建型模式
创建型模式是处理对象创建的设计模式。这些模式以适合特定情况的方式创建对象。创建型模式背后有两个基本思想。第一个是封装应该创建哪些具体类的知识,第二个是隐藏这些类的实例是如何创建的。在创建型模式类别中,有五种知名的模式:
-
抽象工厂模式:提供一个接口来创建相关对象,而不指定具体的类
-
建造者模式:将复杂对象的构建与其表示分离,以便可以使用相同的流程创建类似类型的对象
-
工厂方法模式:创建对象而不暴露对象或其类型创建的底层逻辑
-
原型模式:通过克隆现有对象来创建对象
-
单例模式:这允许一个类在应用程序的生命周期内只有一个实例
在本章中,我们将向您展示如何在 Swift 中使用构建器、工厂方法和单例模式的示例。让我们从最具有争议性且可能被过度使用的模式之一——单例模式开始。
单例设计模式
单例模式在开发社区的一些角落里是一个相当有争议的话题。其中一个主要原因是单例模式可能是最被过度使用和误用的模式。这个模式引起争议的另一个原因是,单例模式将全局状态引入了应用程序中,这使得在应用程序的任何点上都可以更改对象,从而忽略了作用域。我个人认为,如果单例模式被正确使用,使用它是没有问题的;然而,我们确实需要小心不要误用它。
单例模式限制了类在应用程序生命周期内的实例化,使其只有一个实例。当我们需要恰好一个对象来协调应用程序内的操作时,这个模式非常有效。单例的一个良好用途是,如果我们的应用程序通过蓝牙与远程设备通信,并且我们还想在整个应用程序中保持这个连接。虽然有些人可能会说我们可以从一页传递连接类的实例到另一页,但这本质上就是单例。
在我看来,在这个例子中,单例模式要干净得多,因为有了单例模式,任何需要连接的页面都可以获取它,而不必强迫每个页面都维护实例。这也允许我们在不每次切换到另一页时重新连接的情况下维护连接。
注意
在 Swift 中实现单例模式有几种方法。这里介绍的方法使用了类常量,这是在 Swift 1.2 版本中引入的。
让我们看看如何使用 Swift 实现单例模式。以下代码示例展示了如何创建一个单例类:
class MySingleton {
static let sharedInstance = MySingleton()
var number = 0
private init() {}
}
我们可以看到,在 MySingleton 类中,我们创建了一个名为 sharedInstance 的静态常量,它包含了一个 MySingleton 类的实例。静态常量可以在不实例化类的情况下被调用。由于我们声明了 sharedInstance 常量是静态的,因此在整个应用程序的生命周期中只有一个实例存在,从而创建了单例模式。
我们还创建了一个私有的初始化器,这将限制其他代码创建 MySingleton 类的另一个实例。
现在,让我们看看这个模式是如何工作的。MySingleton 模式还有一个名为 number 的属性,其类型为 Int。我们将监控这个属性在我们使用 sharedInstance 属性创建多个 MySingleton 类型的变量时的变化,如下面的代码所示:
var singleA = MySingleton.sharedInstance
var singleB = MySingleton.sharedInstance
var singleC = MySingleton.sharedInstance
singleB.number = 2
print(singleA.number)
print(singleB.number)
print(singleC.number)
singleC.number = 3
print(singleA.number)
print(singleB.number)
print(singleC.number)
在这个例子中,我们使用sharedInstance属性创建了三个MySingleton类型的变量。我们最初将第二个MySingleton变量(singleB)的number属性设置为数字2。当我们打印出singleA、singleB和singleC的number属性值时,我们看到这三个的number属性都等于2。然后我们改变第三个MySingleton变量(singleC)的number属性值为数字3。当我们再次打印出number属性值时,我们看到这次,所有三个的值现在都是3。因此,当我们改变任何实例的number属性值时,它将改变所有三个的值,因为每个变量都指向同一个实例。
当我们需要在整个应用程序中维护一个对象的状态时,单例模式非常有用,但请注意不要过度使用它。单例模式只有在有具体要求(关键字是需求)在整个应用程序的生命周期中只有一个实例时才应该使用。如果我们仅仅为了方便而使用单例模式,那么我们就是在误用它。
注意
对于单例模式,我们创建了MySingleton类型作为一个类(引用类型),因为我们希望确保在整个应用程序中只存在该类型的一个实例。如果我们把MySingleton类型作为一个结构(值类型),我们就会面临存在多个实例的风险,因为结构是值类型。
现在,让我们看看构建者设计模式。
构建者设计模式
构建者模式帮助我们创建复杂对象,并强制执行这些对象的创建过程。使用这个模式,我们通常将创建逻辑从复杂的类中分离出来,并将其放入另一个类中。这允许我们使用相同的构建过程来创建类的不同表示形式。
在本节中,我们将通过创建一个Burger类并使用各种不同的汉堡构建者来创建不同类型的汉堡来了解如何使用构建者模式。在我们了解如何使用构建者模式之前,让我们看看如何在不使用构建者模式的情况下创建一个Burger类以及我们可能会遇到的问题。
以下代码创建了一个名为BurgerOld的类,并且没有使用构建者模式:
class BurgerOld {
var name: String
var patties: Int
var bacon: Bool
var cheese: Bool
var pickles: Bool
var ketchup: Bool
var mustard: Bool
var lettuce: Bool
var tomato: Bool
init(name: String, patties: Int, bacon: Bool, cheese: Bool, pickles: Bool,ketchup: Bool,mustard: Bool,lettuce: Bool,tomato: Bool) {
self.name = name
self.patties = patties
self.bacon = bacon
self.cheese = cheese
self.pickles = pickles
self.ketchup = ketchup
self.mustard = mustard
self.lettuce = lettuce
self.tomato = tomato
}
}
在BurgerOld类中,有几个属性定义了汉堡上有什么以及汉堡的名称。由于我们需要知道哪些项目在汉堡上,哪些不在,当我们创建BurgerOld类的实例时,初始化器要求我们定义每个项目。这可能导致我们应用程序中的一些复杂初始化,更不用说如果我们有多个标准汉堡(培根芝士汉堡、芝士汉堡、汉堡等),我们需要确保我们正确地定义了每个汉堡。让我们看看如何创建BurgerOld类的实例:
// Create Hamburger
var burgerOld = BurgerOld(name: "Hamburger", patties: 1, bacon: false, cheese: false, pickles: false, ketchup: false, mustard: false, lettuce: false, tomato: false)
// Create Cheeseburger
var burgerOld = BurgerOld(name: "Cheeseburger", patties: 1, bacon: false, cheese: false, pickles: false, ketchup: false, mustard: false, lettuce: false, tomato: false)
现在,让我们看看更好的做法。我们将首先创建一个BurgerBuilder协议,其中将包含以下代码:
protocol BurgerBuilder {
var name: String {get}
var patties: Int {get}
var bacon: Bool {get}
var cheese: Bool {get}
var pickles: Bool {get}
var ketchup: Bool {get}
var mustard: Bool {get}
var lettuce: Bool {get}
var tomato: Bool {get}
}
此协议简单地定义了任何实现此协议的类所需的九个属性。现在,让我们创建两个实现此协议的类——HamburgerBuilder和CheeseBurgerBuilder类:
class HamBurgerBuilder: BurgerBuilder {
let name = "Burger"
let patties = 1
let bacon = false
let cheese = false
let pickles = true
let ketchup = true
let mustard = true
let lettuce = false
let tomato = false
}
class CheeseBurgerBuilder: BurgerBuilder {
let name = "CheeseBurger"
let patties = 1
let bacon = false
let cheese = true
let pickles = true
let ketchup = true
let mustard = true
let lettuce = false
let tomato = false
}
在HamburgerBuilder和CheeseBurgerBuilder类中,我们所做的只是为每个必需的属性定义值。在更复杂的类中,我们可能需要初始化由该实例需要的其他对象。
现在,让我们看看我们的Burger类,它将使用BugerBuilder协议的实现来创建自身的实例。让我们看看以下代码:
class Burger {
var name: String
var patties: Int
var bacon: Bool
var cheese: Bool
var pickles: Bool
var ketchup: Bool
var mustard: Bool
var lettuce: Bool
var tomato: Bool
init(builder: BurgerBuilder) {
self.name = builder.name
self.patties = builder.patties
self.bacon = builder.bacon
self.cheese = builder.cheese
self.pickles = builder.pickles
self.ketchup = builder.ketchup
self.mustard = builder.mustard
self.lettuce = builder.lettuce
self.tomato = builder.tomato
}
func showBurger() {
print("Name: \(name)")
print("Patties: \(patties)")
print("Bacon: \(bacon)")
print("Cheese: \(cheese)")
print("Pickles: \(pickles)")
print("Ketchup: \(ketchup)")
print("Mustard: \(mustard)")
print("Lettuce: \(lettuce)")
print("Tomato: \(tomato)")
}
}
与本节前面展示的BurgerOld类相比,这个Burger类的主要区别在于初始化器。在先前的BurgerOld类中,初始化器接受九个参数——每个类中定义的一个常量。在新的Burger类中,初始化器接受一个参数,即符合BurgerBuilder协议的类的实例。这个新的初始化器允许我们像这样创建Burger类的实例:
// Create Hamburger
var myBurger = Burger(builder: HamBurgerBuilder())
myBurger.showBurger()
// Create Cheeseburger with tomatoes
var myCheeseBurgerBuilder = CheeseBurgerBuilder()
var myCheeseBurger = Burger(builder: myCheeseBurgerBuilder)
myCheeseBurger.tomato = false
myCheeseBurger.showBurger()
如果我们将创建新Burger类实例的方式与早期的BurgerOld类进行比较,我们可以看到创建Burger类的实例要容易得多。我们还知道我们为每种汉堡设置了正确的值,因为这些值是在构建类中直接设置的。
如我们所见,建造者模式帮助我们简化了复杂对象的创建。它还确保我们的对象被完全创建。
注意
在这个例子中,对于我们的构建类型,我们选择使用类(引用类型)。实际上,使用引用类型或值类型并没有太大的优势;因此,选择了引用类型,因为它没有理由复制我们的构建类型。
在我们最后一个创建型模式的例子中,我们将看看工厂方法模式。
工厂方法模式
工厂方法模式使用工厂方法来创建对象的实例,而不指定将要创建的确切类。这允许我们在运行时选择要创建的确切类。
让我们通过创建一个允许我们从多个型号中选择计算机的计算机商店类来查看如何使用工厂方法模式。我们将首先创建一个名为Computer的协议。每个代表不同计算机模型的类都将实现Computer协议。以下是Computer协议的代码:
protocol Computer {
func getType() -> String
}
Computer协议中只有一个方法,它返回一个表示计算机型号的字符串类型。现在,让我们创建三个具体类来实现Computer协议:
class MacbookPro: Computer {
func getType() -> String {
return "Macbook Pro"
}
}
class IMac: Computer {
func getType() -> String {
return "iMac"
}
}
class MacMini: Computer {
func getType() -> String {
return "MacMini"
}
}
实现Computer协议的三个类中的每一个都在getType()方法中返回一个独特的字符串类型。这将标识创建了哪个类。现在,让我们看看我们的ComputerStore类,它将根据我们寻找的计算机类型创建这三个类中的一个实例。让我们看看以下代码:
class ComputerStore {
enum ComputerType {
case Laptop
case Desktop
case Headless
}
func getModel(type: ComputerType) -> Computer {
switch(type) {
case ComputerType.Laptop:
return MacbookPro()
case ComputerType.Desktop:
return IMac()
case ComputerType.Headless:
return MacMini()
}
}
}
在ComputerStore类中,我们首先创建一个名为ComputerType的枚举,它定义了我们销售的计算机类型。这些类型是Laptop、Desktop和Headless。
ComputerStore类有一个方法,那就是getModel()方法。该方法接受一个参数,该参数是ComputerType类型,并返回一个符合Computer协议的类型的实例,具体取决于传入的ComputerType枚举。在这个方法中,我们创建了一个switch语句,该语句将创建并返回一个符合Computer协议的类的实例。
现在,让我们看看如何使用ComputerStore类:
var laptop = store.getModel(.Laptop)
print(laptop.getType())
在这个例子中,我们首先创建ComputerStore类的一个实例。然后,我们调用getModel()方法,通过传递一个ComputerType值来检索符合Computer协议的类的实例。调用getModel()方法的代码不需要知道后端代码如何选择要创建哪种类型的类;它只知道它应该得到一个符合Computer协议或nil的有效实例。
我发现自己经常使用这个模式。每当有多个类型符合相同的协议时,我们可能需要考虑使用工厂方法模式来集中创建这些对象;否则,我们可能会发现我们在应用程序的多个部分中重复了对象创建代码。
注意
与构建者模式类似,我们选择使用类来表示不同的计算机类型,主要是因为创建多个计算机类型的副本没有意义。
关于设计模式,尤其是创建模式的关键思想之一是,我们将有关如何以及创建什么的逻辑从我们的通用代码中提取出来,并将其放入特定的类或函数中。然后,当我们未来需要修改我们的代码时,逻辑嵌入在单个位置,可以轻松更改,而不是在代码的多个位置中都有逻辑。
现在,让我们来看看结构化设计模式。
结构型设计模式
结构型设计模式描述了如何将类组合成更大的结构。这些更大的结构通常更容易处理,并隐藏了大量单个类的复杂性。结构型模式类别中的大多数模式都涉及对象之间的连接。
有七个著名的模式属于结构型设计模式类型:
-
适配器(Adapter):这允许具有不兼容接口的类一起工作
-
桥接(Bridge):这用于将类的抽象元素与实现分离,以便两者可以独立变化
-
组合(Composite):这允许我们将一组对象视为单个对象
-
装饰器(Decorator):这让我们可以在现有对象的方法中添加或覆盖行为
-
外观(Façade):这为更大的更复杂的代码库提供了一个简化的接口
-
享元(Flyweight):这允许我们减少创建和使用大量相似对象所需的资源
-
代理(Proxy):这是一个充当其他类或类的接口的类
在本章中,我们将给出如何在 Swift 中使用桥接、外观和代理模式的示例。让我们首先看看桥接模式。
桥接模式
桥接模式将抽象与实现解耦,以便它们可以独立变化。桥接模式也可以被视为一种双层抽象。
在本节中,我们将通过创建一个简单的通用遥控器类来展示如何使用桥接模式,该类可以控制多个电视对象。我们将首先创建遥控器和电视的协议,如下面的代码所示:
protocol TV {
var currentChannel: Int {get set}
func turnOn()
func turnOff()
}
protocol RemoteControl {
var tv: TV {get set}
init(tv: TV)
}
TV 协议定义了一个属性和两个函数。currentChannel 属性用于跟踪电视当前所在的频道。函数 turnOn() 和 turnOff() 用于打开或关闭电视。
RemoteControl 协议定义了一个属性和一个初始化器。tv 属性持有我们想要控制的电视实例。初始化器将使用符合 TV 协议的类型启动遥控器。
现在,我们将扩展 TV 和 RemoteControl 协议,为符合协议的类型添加常用功能。请注意,这里添加的功能可以在符合协议的类型中重写:
extension TV {
mutating func changeChannel(channel: Int) {
self.currentChannel = channel
}
}
extension RemoteControl {
func turnOn() {
tv.turnOn()
}
func turnOff() {
tv.turnOff()
}
mutating func setChannel(channel: Int) {
tv.changeChannel(channel)
}
mutating func nextChannel() {
tv.changeChannel(tv.currentChannel + 1)
}
mutating func prevChannel() {
tv.changeChannel(tv.currentChannel - 1)
}
}
在 TV 扩展中,我们添加了一个方法来更改电视的频道。在 RemoteControl 扩展中,我们添加了五个方法,这些方法可以打开/关闭电视或更改电视的频道。
现在,让我们看看如何创建符合 TV 协议的结构。为此,我们将定义两个协议的具体实现,如下所示:
struct VizioTV: TV {
var currentChannel = 1
func turnOn() {
print("Vizio On")
}
func turnOff() {
print("Vizio Off")
}
}
struct SonyTV: TV {
var currentChannel = 1
func turnOn() {
print("Sony On")
}
func turnOff() {
print("Sony Off")
}
}
使用这段代码,我们定义了SonyTV和VizioTV对TV协议的实现。在这些结构体中,我们实现了TV协议的所有要求。我们将使用这些实现来告诉通用遥控器控制哪个电视。现在,让我们看看如何实现RemoteControl协议,如下所示:
class MyUniversalRemote: RemoteControl {
var tv: TV
required init(tv: TV) {
self.tv = tv
}
}
在MyUniversalRemote类中,我们实现了Remote协议所需的初始化器。
要使用此模式,我们首先创建一个我们想要控制的TV类型的实例。然后,我们会使用该实例来启动我们的遥控器类型,如下面的代码所示:
var myTv = VizioTV()
var remote = MyUniversalRemote(tv: myTv)
remote.turnOn()
remote.nextChannel()
print("Channel on: \(myTv.currentChannel)")
remote.nextChannel()
print("Channel on: \(myTv.currentChannel)")
remote.turnOff()
桥接模式可以看作是两层抽象,其中抽象和实现不应该在编译时绑定。这允许我们在运行时定义要使用哪些对象。这也允许我们通过创建实现TV协议的新类来简单地给我们的myUniversalRemote类添加更多的电视。
注意
对于这个模式,我们使用结构体实现了符合TV协议的类型。选择结构体是因为创建一个TV类型的实例非常容易,然后可以使用它来创建多个遥控器类型的实例,如下面的示例所示:
var myTv = VizioTV()
var remoteForTV1 = MyUniversalRemote(tv: myTv)
var remoteForTV2 = MyUniversalRemote (tv: myTv)
在这个例子中,如果VizioTV类型是用类实现的,那么两个MyUniversalRemote实例将引用同一个电视而不是不同的电视。因此,尽管我们有两个电视,每个电视都有单独的遥控器,但这两个遥控器实际上都在操作同一个电视。
有时候我们想要这种行为,对于那些时候,我们应该使用类;然而,根据我的经验,这通常不是我们想要的行为。
现在,让我们看看结构类别中的下一个模式——外观模式。
外观模式
外观模式提供了一个简化的大规模和更复杂代码库的接口。这允许我们通过隐藏一些复杂性来使我们的库更容易使用和理解。它还允许我们将多个 API 组合成一个单一、更易于使用的 API,这正是我们将在示例中看到的。
在这个例子中,我们将创建一个简化的旅行 API,将酒店、航班和租车 API 组合成一个单一、易于使用的接口。我们将从定义酒店、航班和租车类开始,如下所示:
struct HotelBooking {
static func getHotelNameForDates(to: NSDate, from: NSDate) -> [String]? {
let hotels = [String]()
//logic to get hotels
return hotels
}
}
struct FlightBooking {
static func getFlightNameForDates(to: NSDate, from: NSDate) -> [String]? {
let flights = [String]()
//logic to get flights
return flights
}
}
struct RentalCarBooking {
static func getRentalCarNameForDates(to: NSDate, from: NSDate) -> [String]? {
let cars = [String]()
//logic to get flights
return cars
}
}
在这些 API 中的每一个,我们定义一个单一的静态方法,该方法将返回一个列表,其中包含(酒店、航班或租车)在请求日期可用的项目。实际上,我们在这里没有实现任何逻辑,因为我们需要定义一个数据源,而我更喜欢保持示例简单,以便集中精力了解模式的工作原理。
现在,让我们看看我们的TravelFacade类,它将这三个 API 组合成一个单一、更易于使用的 API,如下面的代码所示:
class TravelFacade {
var hotels: [String]?
var flights: [String]?
var cars: [String]?
init(to: NSDate, from: NSDate) {
hotels = HotelBooking.getHotelNameForDates(to, from: from)
flights = FlightBooking.getFlightNameForDates(to, from: from)
cars = RentalCarBooking.getRentalCarNameForDates(to, from: from)
}
}
在 TravelFacade 类内部,我们创建了一个单例初始化器,它接受两个 NSDate 对象作为参数。然后我们使用这两个 NSDate 对象来检索在由日期定义的时间段内可用的酒店、航班和租赁汽车。
当我们有一个复杂的 API 结构想要简化时,外观模式非常有用。它同样非常有用,当我们有一系列多个相关的 API,就像我们在示例中看到的那样,将它们合并到一个 API 中。
注意
对于这个模式,我们在实现三种预订类型时选择了使用结构体;然而,使用哪种类型(类或结构体)实际上取决于应用程序的个体设计。在本章的 桥接模式 部分,我们能够说在大多数情况下结构体会被优先选择;然而,在这个模式中,我们真的不能说哪种类型在大多数情况下会被优先选择。
现在,让我们看看我们的最后一个结构化模式,即代理设计模式。
代理设计模式
在代理设计模式中,有一个对象充当其他对象的接口。这个包装类,即代理,可以给对象添加功能,使对象可以通过网络访问,或者限制对对象的访问。
在本节中,我们将通过创建一个可以添加多个楼层平面图(每个楼层平面图代表房子的不同楼层)的房屋类来演示代理模式。让我们首先创建一个 FloorPlanProtocol 协议:
protocol FloorPlanProtocol {
var bedRooms: Int {get set}
var utilityRooms: Int {get set}
var bathRooms: Int {get set}
var kitchen: Int {get set}
var livingRooms: Int {get set}
}
在 FloorPlanProtocol 中,我们定义了五个属性,这些属性将代表每个楼层平面图中包含的房间数量。现在,让我们创建一个名为 FloorPlan 的 FloorPlanProtocol 协议的实现,如下所示:
struct FloorPlan: FloorPlanProtocol {
var bedRooms = 0
var utilityRooms = 0
var bathRooms = 0
var kitchen = 0
var livingRooms = 0
}
FloorPlan 类实现了从 FloorPlanProtocol 所需的所有五个属性,并为它们分配了默认值。接下来,我们将创建一个名为 House 的类,它将代表一栋房子:
class House {
private var stories = [FloorPlanProtocol]()
func addStory(floorPlan: FloorPlanProtocol) {
stories.append(floorPlan)
}
}
在我们的 House 类中,我们有一个 FloorPlanProtocols 对象的数组,其中每个楼层平面图将代表房子的一个楼层。我们还有一个名为 addStory() 的函数,它接受一个符合 FloorPlanProtocol 协议的对象实例。这个函数将楼层平面图添加到 FloorPlanProtocols 协议的数组中。
如果我们考虑这个类的逻辑,可能会遇到一个问题。问题是我们可以添加任意多的楼层平面图,这可能会导致房子有 60 或 70 层高。如果我们是在建造摩天大楼,那将很棒,但我们只是想建造基本的单户家庭住宅。如果我们想限制楼层平面图的数量而不改变 House 类(我们无法改变它,或者我们只是不想改变它),我们可以实现代理模式。以下示例展示了如何实现 HouseProxy 类,其中我们限制了可以添加到房子中的楼层平面图数量,如下所示;
class HouseProxy {
var house = House()
func addStory(floorPlan: FloorPlanProtocol) -> Bool {
if house.stories.count < 3 {
house.addStory(floorPlan)
return true
}
else {
return false
}
}
}
我们从创建HouseProxy类的一个实例开始,然后创建一个名为addStory()的方法,允许我们向房子添加一个新的楼层平面图。在addStory()方法中,我们检查房子的楼层数是否少于三,如果是这样,我们就将楼层平面图添加到房子中并返回true。如果楼层数等于或大于三,则我们不将楼层平面图添加到房子中并返回false。让我们看看我们如何使用这个代理:
var ourHouse = HouseProxy()
var basement = FloorPlan(bedRooms: 0, utilityRooms: 1, bathRooms: 1, kitchen: 0, livingRooms: 1)
var firstStory = FloorPlan(bedRooms: 1, utilityRooms: 0, bathRooms: 2, kitchen: 1, livingRooms: 1)
var secondStory = FloorPlan(bedRooms: 2, utilityRooms: 0, bathRooms: 1, kitchen: 0, livingRooms: 1)
var additionalStory = FloorPlan(bedRooms: 1, utilityRooms: 0, bathRooms: 1, kitchen: 1, livingRooms: 1)
print(ourHouse.addStory(basement))
print(ourHouse.addStory(firstStory))
print(ourHouse.addStory(secondStory))
print(ourHouse.addStory(additionalStory))
在我们的示例代码中,我们首先创建了一个名为ourHouse的HouseProxy类实例。然后,我们创建了四个FloorPlan类实例,每个实例都有不同数量的房间。最后,我们尝试将每个楼层平面图添加到ourHouse实例中。如果我们运行代码,我们会看到前三个FloorPlan类实例成功添加到了房子中,但最后一个没有添加,因为我们只能添加三层楼。
当我们想要向一个类添加一些额外的功能或错误检查,但又不想改变实际的类本身时,代理模式非常有用。
注意
对于代理模式,我们选择使用一个类来实现这个模式,因为通常我们不会想要复制我们代理的类型。相反,我们通常希望保持对实例所做的更改。这在某种程度上是桥接模式的反义词,在我的经验中,结构在大多数情况下会被优先考虑。
现在,让我们看看行为设计模式。
行为设计模式
行为设计模式解释了对象之间如何交互。这些模式描述了不同的对象如何发送消息给彼此以使事情发生。
有九种众所周知的设计模式属于结构设计模式类型:
-
责任链:这个用于处理各种请求,每个请求都可能被委派给不同的处理者。
-
命令:这创建可以封装动作或参数的对象,以便稍后或由不同的组件调用。
-
迭代器:这允许我们按顺序访问对象中的元素,而不暴露底层结构。
-
中介者:这个用于减少相互通信的类之间的耦合。
-
备忘录:这个用于捕获对象的当前状态,并以可以稍后恢复的方式存储它。
-
观察者:这允许一个对象发布其状态的变化。其他对象可以订阅,以便在发生任何变化时得到通知。
-
状态:这个用于在对象的内部状态改变时改变对象的行为。
-
策略:这允许在运行时从一组算法中选择一个。
-
访问者:这是一种将算法与对象结构分离的方法。
在本节中,我们将给出如何在 Swift 中使用策略和命令模式的示例。让我们首先看看命令模式。
命令设计模式
命令设计模式允许我们定义可以在以后执行的操作。此模式通常封装了在以后时间调用或触发操作所需的所有信息。
在本节中,我们将通过创建一个 Light 类来演示如何使用命令模式。在这个例子中,我们将定义两个命令——lightOnCommand 和 lightOffCommand。然后我们将使用 turnOnLight() 和 turnOffLight() 方法来调用这些命令。
我们将首先创建一个名为 Command 的协议,所有我们的命令都需要遵守。以下是 Command 协议:
protocol Command {
func execute()
}
此协议包含一个名为 execute 的方法,它将用于执行命令。现在,让我们看看 Light 类将用于打开和关闭灯光的 LightOneCommand 和 LightOffCommand 类。它们如下所示:
struct RockerSwitchLightOnCommand: Command {
func execute() {
print("Rocker Switch: Turning Light On")
}
}
struct RockerSwitchLightOffCommand: Command {
func execute() {
print("Rocker Switch: Turning Light Off")
}
}
struct PullSwitchLightOnCommand: Command {
func execute() {
print("Pull Switch: Turning Light On")
}
}
struct PullSwitchLightOffCommand: Command {
func execute() {
print("Pull Switch: Turning Light Off")
}
}
RockerSwitchLightOffCommand、RockerSwitchLightOnCommand、PullSwitchLightOnCommand 和 PullSwitchLightOffCommand 命令通过实现 execute() 方法符合 Command 协议,因此我们可以在我们的 Light 类中使用它们。现在,让我们看看如何实现 Light 类:
class Light {
var lightOnCommand: Command
var lightOffCommand: Command
init(lightOnCommand: Command, lightOffCommand: Command) {
self.lightOnCommand = lightOnCommand
self.lightOffCommand = lightOffCommand
}
func turnOnLight() {
self.lightOnCommand.execute()
}
func turnOffLight() {
self.lightOffCommand.execute()
}
}
在 Light 类中,我们首先创建两个名为 lightOnCommand 和 lightOffCommand 的变量,它们持有符合 Command 协议的类的实例。然后我们创建一个初始化器,它允许我们在初始化类时设置这两个命令。最后,我们创建 turnOnLight() 和 turnOffLight() 方法,我们将使用这些方法来打开和关闭灯光。在这些方法中,我们调用适当的命令来打开或关闭灯光。
我们将像这样使用 Light 类:
var on = PullSwitchLightOnCommand()
var off = PullSwitchLightOffCommand()
var light = Light(lightOnCommand: on, lightOffCommand: off)
light.turnOnLight()
light.turnOffLight()
light.lightOnCommand = RockerSwitchLightOnCommand()
light.turnOnLight()
在这个例子中,我们首先创建了一个名为 on 的 PullSwitchLightOnCommand 类的实例和一个名为 off 的 PullSwitchLightOffCommand 类的实例。然后我们使用我们刚刚创建的两个命令创建了一个 Light 类的实例,并调用 Light 实例的 turnOnLight() 和 turnOffLight() 方法来打开和关闭我们的灯光。在最后两行中,我们将 lightOnCommand 方法从最初设置为 PullSwitchLightOnCommand 类的实例更改为 RockerSwitchLightOnCommand 类的实例。当我们将灯光打开时,light 实例将使用 RockerSwitchLightOnCommand 类。这允许我们在运行时更改 Light 类的功能。
使用命令模式有许多好处。其中一个主要的好处是我们能够在运行时设置命令的实现,这也允许我们根据需要在整个应用程序的生命周期中用符合Command协议的不同实现替换命令。命令模式的另一个优点是将命令实现的细节封装在命令类本身中,而不是在容器类中。
注意
对于命令模式,我们使用结构体来实现我们的命令类型,因为创建一个命令类型的实例并将其用于创建Light类的多个实例非常容易。在这种情况下,如果Light类中的任何内容在命令实例中发生变化,那么它将反映在所有使用该命令实例的Light类的实例中。通常,我们不想这种行为;然而,如果您的应用程序需要这种行为,那么您应该使用类而不是结构体。
现在,让我们看看我们的最后一个设计模式,即策略模式。
策略模式
策略模式在某种程度上与命令模式相似,因为它们都允许我们将实现细节从我们的调用类中解耦,并允许我们在运行时切换实现。最大的区别是,策略模式旨在封装算法。通过替换算法,我们期望对象执行相同的功能,但以不同的方式。在命令模式中,当我们替换命令时,我们期望对象以不同的方式工作。
在本节中,我们将通过展示如何在运行时替换压缩策略来演示策略模式。让我们从这个示例开始,创建一个CompressionStrategy协议,我们的每个压缩类都将遵循这个协议。让我们看一下以下代码:
protocol CompressionStrategy {
func compressFiles(filePaths: [String])
}
此协议定义了一个名为compressFiles()的方法,它接受一个参数,即包含要压缩的文件路径的字符串数组。现在,我们将创建两个符合CompressionStrategy协议的结构。这些类是ZipCompressionStrategy和RarCompressionStrategy类,如下所示:
struct ZipCompressionStrategy: CompressionStrategy {
func compressFiles(filePaths: [String]) {
print("Using Zip Compression")
}
}
struct RarCompressionStrategy: CompressionStrategy {
func compressFiles(filePaths: [String]) {
print("Using RAR Compression")
}
}
这两个结构体通过具有名为compressFiles()的方法来实现CompressionStrategy协议,该方法接受一个字符串数组。在这些方法中,我们只是简单地打印出我们正在使用的压缩名称。通常,我们会在这些方法中实现压缩逻辑。
现在,让我们看看将被调用来压缩文件的CompressContent类:
class CompressContent {
var strategy: CompressionStrategy
init(strategy: CompressionStrategy) {
self.strategy = strategy
}
func compressFiles(filePaths: [String]) {
self.strategy.compressFiles(filePaths)
}
}
在这个课程中,我们首先定义了一个名为 strategy 的变量,它将包含一个符合 CompressStrategy 协议的类的实例。然后我们创建了一个初始化器,用于在类初始化时设置压缩类型。最后,我们创建了一个名为 compressFiles() 的方法,它接受一个包含我们希望压缩的文件路径的字符串数组。在这个方法中,我们使用在 strategy 变量中设置的压缩策略来压缩文件。
我们将像这样使用 CompressContent 类:
var filePaths = ["file1.txt", "file2.txt"]
var zip = ZipCompressionStrategy()
var rar = RarCompressionStrategy()
var compress = CompressContent(strategy: zip)
compress.compressFiles(filePaths)
compress.strategy = rar
compress.compressFiles(filePaths)
我们首先创建一个包含我们希望压缩的文件的字符串数组。我们还创建了 ZipCompressionStrategy 和 RarCompressionStrategy 类的实例。然后我们创建了一个 CompressContent 类的实例,将压缩策略设置为 ZipCompressionStrategy 实例,并调用 compressFiles() 方法,该方法将在控制台打印出 Using zip compression 消息。然后我们将压缩策略设置为 RarCompressionStrategy 实例,并再次调用 compressFiles() 方法,该方法将在控制台打印出 Using rar compression 消息。
策略模式非常适合在运行时设置要使用的算法,这也允许我们根据应用程序的需要交换不同的实现。策略模式的另一个优点是,我们将算法的细节封装在策略类本身中,而不是在主实现类中。
注意
就像命令模式一样,我们使用结构体来实现策略模式,因为它非常容易创建一个策略类型的实例,然后使用它来创建 CompressContent 类的多个实例。在这种情况下,如果策略实例中发生了任何变化,它将反映在所有的 CompressContent 类型中。通常,我们并不希望这种行为;然而,如果您的应用程序需要这种行为,那么您应该使用类而不是结构体。
这标志着我们对 Swift 中设计模式的探索结束。
摘要
设计模式是解决我们在现实世界的应用程序设计中反复看到的软件设计问题的解决方案。这些模式旨在帮助我们创建可重用和灵活的代码。设计模式还可以使我们的代码更容易被其他开发人员以及我们自己阅读和理解,当我们几个月/几年后回顾我们的代码时。
如果我们仔细查看本章中的示例,我们会注意到设计模式的一个关键组成部分是协议。几乎所有的设计模式(单例设计模式除外)都使用协议来帮助我们创建非常灵活和可重用的代码。
如果这是你第一次真正关注设计模式,你可能注意到了一些与你在过去自己的代码中可能使用过的策略的相似之处。当经验丰富的开发者第一次接触设计模式时,这种情况是预料之中的。我还会鼓励你更多地阅读有关设计模式的内容,因为它们肯定会帮助你创建更灵活和可重用的代码。







浙公网安备 33010602011771号