Swift3--新特性指南-全-
Swift3 新特性指南(全)
原文:
zh.annas-archive.org/md5/c0f6221d9eaed9c721bfc5570e07d639译者:飞龙
前言
随着 Swift 3 的发布,苹果公司正寻求增加 Swift 的采用率。本书的使命是让新和有经验的开发者能够快速掌握 Swift 3 并提高其生产力。我们将探讨引入到 Foundation 和标准库中的主要特性。我们还将提供如何将现有的 Swift 2.2 项目转换为 Swift 3 的评论,并检查 Swift 在 Linux 上运行和开发的支持。
目标和成就
我的目的是向您介绍随着 Swift 3 的发布而出现的新概念。我们共同的经历有望使您在以下领域有更深入的理解:
-
理解 大重命名 如何使您的代码更容易编写和理解
-
在 Mac 或 Linux 上编写 Swift 应用程序的舒适工具
-
将您的 Swift 2.2 项目转换为 Swift 3
-
使您了解 Swift 3 中新出现的语法变更
本书涵盖的内容
第一章, 他们是怎么想的? 介绍了 Swift 3。Swift 是苹果公司的一个重要语言,其采用率到目前为止已经非常惊人。我们将介绍语言变更的选择过程以及社区如何贡献。此外,我们还将介绍 Swift.org 和苹果的 Github 页面作为 Swift 中发生的一切的仓库。
第二章, 发现新领域 - Linux 终于来了! 讨论了,尽管直到最近,Mac 开发一直是您唯一支持的选项,但 Swift 3 支持在 Linux 机器上开发和运行 Swift 应用程序。我们的目标是使您在本章结束时在 Mac 和 Linux 机器上设置开发环境。我们将一起编写我们的第一个 Linux 应用程序。
第三章, 迁移到 Swift 3 以更 Swifty,将展示如何使用 Swift 迁移器升级我们的 Swift 2.2 项目。我们将使用一个示例项目来演示如何使用迁移器,并概述在迁移 Swift 项目时的一些有用策略。
第四章, Swift 核心变更让您欲罢不能,将快速突出编写良好 Swift API 的哲学。之后,我们将用剩余的章节来介绍在 Swift 3 中引用和使用 Objective-C 功能以及从 Objective-C 和 C 导入代码到 Swift 3 的语言改进。
第五章, 函数和运算符变更 – 以新方式完成任务,将探讨函数声明和使用的变更,以及这些变更如何转化为更好的 Swift 代码。我们还将解释运算符的变更,并突出一些已被从语言中移除的运算符。
第六章, 额外的、额外的集合和闭包变化,令人兴奋!,我们在这里关注 Swift 3 中的集合和闭包变化。有一些很好的新增功能将使使用集合变得更加有趣。我们还将探讨在 Swift 2.2 中创建闭包的一些令人困惑的副作用,以及这些副作用如何在 Swift 3 中得到修复。
第七章, 抓住你的椅子,高级类型变化来了!,我们将介绍一些您可能不会经常使用的语言改进。本章重点介绍UnsafePointer类型、typealiases和浮点运算。
第八章, 哦,天哪!看看在 Foundation 框架中的新内容,我们将讨论新的测量和单位 API。我们将使用几个示例来强化这些概念,以便您在未来的测量挑战中更好地准备。
第九章, 使用 Xcode Server 和 LLDB 调试改进您的代码,我们将介绍 Xcode Server 作为持续集成服务器的能力以及如何将自动化测试包含在内以改进您的测试工作流程。在下半部分,我们将描述如何使用 LLDB 在 Linux 上调试您的代码。
第十章, 探索服务器上的 Swift,展示了 Swift 在 Linux 上运行的重要性,尤其是 Linux 在托管和运行服务器方面的普及。Swift 3 为开发者打开了使用与他们在 iOS、macOS、tvOS 和 watchOS 上创建应用程序相同的 Swift 来创建服务器端应用程序的可能性。到本章结束时,您将有一个完全用 Swift 编写的运行在 Linux 服务器上的服务器。
您需要本书的内容
本书将指导您安装所有必要的工具,以便跟随示例。您需要安装 Webstorm 版本 10 才能有效地运行本书中的代码示例。
本书面向的对象
要在 Mac 上开发 Swift 3,您需要 Xcode 8 和 macOS Sierra 10.12。如果您想利用 Linux 上的 Swift,您需要访问一台能够运行 Ubuntu 14.04 的 Linux 机器或虚拟机。
习惯用法
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称显示如下:“下载toolchain文件”
代码块设置如下:
h5> oneMillion = "one million"
error: repl.swift:11:12: error: cannot assign to value: 'oneMillion' is a
'let' constant
oneMillion = "one million"
~~~~~~~~~~~^
repl.swift:2:1: note: change 'let' to 'var' to make it mutable`
let oneMillion = 1\_000\_000`
^~~~
var
任何命令行输入或输出都写作如下:
$ swift
新术语和重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,文本中显示如下:“接下来,您选择要针对其开发的工具链,这将仅更改 Xcode 设置”
注意
警告或重要注意事项以这种方式显示在框中。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们非常重要,因为它帮助我们开发出您真正能从中获得最大价值的书籍。要发送一般性反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从购买中获得最大价值。
下载示例代码
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持标签上。
-
点击代码下载与勘误。
-
在搜索框中输入书籍名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买这本书的地方。
-
点击代码下载。
一旦文件下载完成,请确保使用最新版本解压或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
本书代码包也托管在 GitHub 上github.com/PacktPublishing/Swift-3-New-Features。我们还有其他来自我们丰富图书和视频目录的代码包可供在github.com/PacktPublishing/找到。查看它们吧!
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。
侵权
互联网上版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过版权@packtpub.com 与我们联系,并提供疑似侵权材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您对本书的任何方面有问题,您可以联系 questions@packtpub.com,我们将尽力解决问题。
第一章。他们是怎么想的?
苹果公司发布的 Swift 从一开始就取得了巨大的成功。这种语言产生了大量的炒作,并且它实现了承诺。当然,随着任何新编程语言的引入,问题和问题也会随之而来。苹果公司已经精心培养了这种年轻的语言,并且一直在稳步改进其基础,引入新的功能、支持和与长期主导的 Objective-C 的兼容性。那么,为什么苹果要将语言开源?苹果的目标是什么,这又对我们关于即将发布的 Swift 3 的发布有什么启示?
本章的重点是讨论苹果公司对 Swift 3 的目标,向您展示您可以在哪里找到关于语言新和当前发展的官方信息来源,并解释开发者社区将如何塑造 Swift 作为一门语言的未来。
苹果公司对 Swift 3 的目标
在 2016 年苹果公司全球开发者大会(WWDC)的“Swift 新特性”讲座中,苹果工程师概述了即将发布的 Swift 3 的几个目标:
-
建立一个开放的社区
-
可移植性到新平台
-
确保基础正确
-
优化以实现卓越
注意
如果您错过了会议,您可以在苹果开发者门户上观看讲座的重播。这里是 Swift 新特性 的链接:developer.apple.com/videos/play/wwdc2016/42。
我想在这里简要地提及几个主题,因为它将为你在剩余章节中找到的材料提供一个基础:
-
苹果公司认为,为了 Swift 的广泛采用,它需要一个强大的社区。快速采用 Swift 的途径是将社区的呼声纳入其开发中。
-
Swift 是一种通用语言,可以在任何平台上运行。想象一下在 Linux 上或在您创建或需要控制的 物联网(IoT)上运行 Swift。苹果公司相信 Swift 语言的能力如此之强,以至于他们移除了将 Swift 与在 Mac 上运行绑定的障碍,为平台可移植性打开了无限可能。苹果公司希望社区找到让 Swift 在其他平台上运行的方法。今天,Swift 团队正在支持 Linux 的移植。明天,Swift 团队可能会对广泛的平台提供官方支持。
-
为了使前两个主题成为可能,Swift 团队需要在 Swift 3 的开发初期就确保一切正确。不幸的是,这意味着 Swift 3 中的新变化与 Swift 的先前版本不兼容。Swift 3 将修复和删除 Swift 2(及其前身)中不协调的部分。Swift 3 还重新构想了自己与 Cocoa 和 Objective-C 的交互方式,使连接它们的 API 感觉更加 Swift。
Swift 对苹果来说意义重大,对语言的期望也很高。苹果已经制定了实现目标的路线图。实际上,您可以通过订阅这里找到的邮件列表之一 swift.org/community/#mailing-lists 来保持 Swift 相关内容的最新状态。与 Swift 社区沟通的主要方式是通过邮件列表。您可以找到针对一般信息的邮件列表,以及针对语言日常更新的列表。Swift 邮件列表可以是一个非常有价值的工具,您不应忽视。
在下一节中,我们将讨论开源社区对您作为 Swift 开发者意味着什么。
开源 Swift
2015 年 12 月 3 日,苹果在 Apache 2.0 许可下开源了 Swift(包括语言、支持库、调试器和包管理器),并在同一天启动了 Swift.org/ 网站。Swift.org/ 是查找构成 Swift 的各种项目的官方网站。它是您阅读有关语言所有开发工作的公告的主要来源,从新特性的提案到您可以下载和测试的 Swift 代码开发分支的链接。
Swift.org/ 是一个您希望将其添加到书签以供将来参考的网站。整个 Swift 代码库托管在 GitHub 上,任何人都可以访问。花点时间想想这一点。任何人都可以下载 Swift,玩玩二进制文件,构建一个项目,或者查看内部结构以了解事物实际上是如何工作的。对于一个规模和声誉如此之大的公司来说,将 Swift(他们押注以驱动所有应用程序的语言)开源给社区是惊人的,不应被轻视。这是一件大事!
自然,苹果并没有只是将语言交付后就走开了。相反,苹果成立了一个内部团队来监督开发过程,并负责日常项目管理。Swift 语言的开源版本由一组项目组成,每个项目都在 GitHub 上作为独立的仓库托管。今天,您可以找到六个活跃项目的链接:
-
Swift 编译器:命令行工具
-
标准库:作为核心语言的一部分分发
-
核心库:提供更高级的功能
-
LLDB 调试器:包括 Swift REPL
-
Swift 包管理器:构建项目和分发它们
每个项目在 Swift.org/ 上都有一个专门的章节,解释项目目标和如何使用 Swift 以及作为社区成员贡献的方法。我鼓励您查看 Swift.org/ 以更好地了解苹果视角下的所有 Swift 相关内容。在我们结束关于 Swift 资源查找的章节之前,我确实想简要讨论一下成为社区贡献者意味着什么。
社区结构已经经过深思熟虑,旨在为社区成员提供强有力的领导。这个结构将指导语言的持续发展,并有望确保随着社区规模的扩大,许多新的社区贡献者都能发出被听到和尊重的声音。以下列出的是构成社区成员角色的内容:
-
项目负责人: 苹果公司是项目的负责人,并将从社区中选出其他人担任各种技术领导职位
-
核心团队: 这个由工程师组成的小团队负责战略方向
-
代码负责人: 这个头衔属于负责 Swift 项目代码库特定领域的任何人
-
提交者: 这个角色授予任何拥有 Swift 仓库提交访问权限的人
-
贡献者: 这个角色保留给任何为补丁做出贡献或帮助进行代码审查的人
您可以在Swift.org/的社区部分了解更多关于社区中个人角色的信息。Swift 社区正在增长,并且不出所料,许多开发者都对如何做出贡献感到好奇。考虑到这一点,让我们来探讨一下事情是如何完成的。
为 Swift 做出贡献
有几种方式可以帮助使社区和 Swift 变得更好。令人惊讶的是,这不仅仅是通过编写代码。社区需要支持在邮件列表上回答问题。您的回答可能从帮助新手更好地理解新概念到,走向极端,帮助经验丰富的开发者解决微妙的错误。无论如何,贡献您的知识对他人来说可能很有价值,并且会受到极大的欢迎!
为 Swift 项目做出贡献的下一个选项是通过报告或分类错误。Swift 团队使用 Jira 进行缺陷跟踪,您可以在位于bugs.swift.org的项目 Jira 实例上提交错误。
作为开发者,您作为贡献者有最后一个选择,那就是贡献代码。提交代码有一个正式的过程,我们将简要介绍。Swift 项目更喜欢小规模的增量更改,而不是大型提交或长期断开的特性分支。Swift 团队还鼓励,但不强制要求,提交消息详细描述提交的代码更改内容。代码质量非常重要,并通过强制代码审查和拉取请求来强调,以确保至少有另一组眼睛审查了所有代码更改。这样想吧;您的更改最终将进入生产环境,并有可能影响数百万使用 Swift 语言的开发者。您真的想冒险引入可能影响数百万开发者的缺陷吗?
Swift 进化过程
虽然 Apple 和 Swift 团队各自有很多关于 Swift 发展方向的优秀想法,但重要的是要记住,他们并不是唯一有想法的人。事实上,Swift 团队完全意识到这一点,并为此创建了一个流程,让你可以提交你的大或小想法,以帮助塑造 Swift 语言。
Swift 进化流程涵盖了从构思到讨论和对话,以及希望最终以被接受的提案结束的所有相关事项,该提案可供开发者用于生产发布。该流程的目标是在社区中保持活跃参与,以引导语言的方向,同时保持 Swift 的愿景。在实践中,这可能意味着添加使语言更容易使用的新功能,或删除不再符合 Swift 愿景的功能。你可以通过提出新想法或讨论和审查其他社区成员的提案来参与其中。

Swift 进化提案步骤
这里是将新想法转变为被接受提案所需的步骤:
-
检查类似提案:做你的作业并确保你的想法尚未被提出和/或拒绝。花时间审查提案及其状态。你可以通过查看“常见拒绝提案”列表来完成这项任务。
-
告诉他人你的想法:关于新想法的大部分讨论都发生在 swift-evolution 邮件列表上。这是你应该创建你的想法草稿的地方,包括它解决的问题以及一些关于解决方案的背景信息。
-
创建你的提案:使用此处找到的提案模板
github.com/apple/swift-evolution/blob/master/0000-template.md,你详细阐述你的想法,并在进化邮件列表上继续推广它。 -
请求审查:当你认为你的提案已经准备好由核心团队进行正式审查时,你向 swift-evolution 仓库
github.com/apple/swift-evolution提交一个拉取请求。当你的拉取请求被接受时,你的提案将被分配一个提案编号,并指派一位核心团队成员来协助审查。 -
回应反馈:你的任务是回应邮件列表上关于你的提案的问题和反馈。在审查期间这尤为重要。
如果一切顺利,你的提案将在审查过程中通过以下提案状态,并被接受。
-
等待审查:直到提案被分配一个审查日期,你的提案将保持此状态。
-
审查中:你的提案正在 swift-evolution 邮件列表上进行公开审查。
-
修订中:如果你在审查状态期间收到反馈,你将有机会处理和修改你的提案。
-
延迟:由于不符合即将到来的主要 Swift 版本的准则,决策被推迟。在此状态下,您的提案将在为下一个主要 Swift 版本规划时重新考虑。
-
接受:已接受,并且可以开始或正在积极实施您接受的提案。同时,也会发布公告,让社区了解即将发布的新的接受提案。
-
拒绝:被核心团队考虑但被拒绝。
关于审查过程,以下是一些关键事项要记住。它不会开始,直到核心团队成员(审查经理)接受您的提案拉取请求。一旦接受,审查经理将与您和其他提案作者协调审查期,开始正式的公开审查。在大多数情况下,审查期为一周,但根据提交的提案中概述的范围和复杂性,可能会更长。最后,核心团队,而不仅仅是审查经理,将根据 swift-evolution 邮件列表上的评论来做出决策。
Swift 3 接受的提案概述
Swift 的每个主要版本都将有高层目标,接受的功能必须遵守这些目标。对于 Swift 3 版本,Swift 团队概述了该版本的主要目标是巩固和成熟 Swift 语言和开发体验。
用他们自己的话来说,Swift 团队继续表示,虽然 Swift 1 到 3 的语言源代码破坏性变更一直是常态,但我们希望 Swift 3.x(以及 Swift 4+)语言尽可能与 Swift 3.0 兼容。然而,这仍然是最努力做到的:如果确实有很好的理由在 Swift 3 之外进行破坏性变更,我们将考虑它,并找到最不具侵略性的方式来推出该变更(例如,通过有一个漫长的弃用周期)。
为了实现 Swift 3 的发布目标,以下各项在确保未来发布的基本正确性方面都被认为是重要的:
-
API 设计指南
-
自动应用命名规范到导入的 Objective-C API
-
在关键 API 中采用命名规范
-
导入的 Objective-C API 的 Swift 化
-
焦点和语言精炼
-
工具质量改进
您可以在 Swift Evolution 存储库页面上了解更多关于这些领域的信息,以及查看已实施、已接受但尚未实施以及被拒绝或撤回的提案的完整列表。
摘要
在本章中,我们讨论了苹果对 Swift 3 的目标以及社区参与和投入对语言发展的重要性。我还向您展示了如何找到关于语言新和当前发展的官方信息来源。最后,我们学习了社区如何为 Swift 语言的发展做出贡献。在第二章中,发现新领域 – Linux 终于来了! 我们探讨了在 Linux 上开发 Swift。
第二章. 探索新领域 – Linux 终于来了!
直到最近,为 Swift 开发意味着您需要一个装有 Xcode IDE 的 Mac。然而,所有这些都随着苹果公司在 2015 年 12 月开源 Swift 编程语言而发生了变化。作为开发者,我们进入了一个全新的世界,因为 Swift 现在可以在 Linux 上运行!此外,您现在可以访问预览版本,并可以直接访问开发主干,从中可以下载开发快照(例如,Swift 的非官方预构建二进制文件)。
这将是一个内容丰富的章节,我想强调我们将要涵盖的内容。我的目标是向您展示如何在 Mac 和 Linux 上找到最新的 Swift 源代码。我还会提供如何使用 工具链 的说明,并解释 Swift 包管理器的工作原理。最后,我们将在 Linux 上开发我们的第一个程序。
下载 Swift
为了开始使用 Swift 3,您需要下载预构建的二进制文件(也称为 工具链)或源代码来自己构建 Swift 库。Swift.org (swift.org) 网站托管了一个 下载 部分 swift.org/download/,其中维护了一个发布版本、预览版本和快照的列表:
-
发布版本:维护到当前发布版本和旧版官方 Swift 发布版本的链接。
-
预览版本:包含开发者预览版本,也称为 种子 或 测试版。这些二进制文件不被视为最终发布版本,但确实提供了到那时已完成工作的相当稳定的版本,用于即将发布的版本。
-
开发者快照 - 是开发分支的预构建二进制文件。这些构建包含最新的开发更改,并已通过自动化单元测试,但并不保证稳定性。快照构建没有经过完整的测试流程。
注意
由于学习构建二进制文件对于您学习 Swift 3 的知识不是关键,我们将把编译源代码作为一项练习留给您自己尝试,在某个时候自行完成。您可以在苹果的 GitHub (github.com/apple) 上找到源代码,以及构建说明。
Mac 上的 Swift 3
要在 Mac 上开始,您只需选择您想要开发的 Swift 工具链 类型。您可以从 下载 部分选择一个版本。Mac 上的 Swift 包含在 Xcode 中,这使得开始使用变得非常容易。Swift 3 要求您拥有 macOS 10.11.5(El Capitan)或更高版本和 Xcode 8。让我们一起来走一遍步骤,并在 Mac 上安装 Swift 3。
-
下载工具链 - 从
swift.org/的下载页面获取最新的 Swift 3 发布版本或预览候选版本。Xcode 由苹果公司创建和维护,从swift.org/选择下载的版本将带您到苹果开发者门户上的下载部分。注意
Xcode 工具链是一个特殊的二进制文件,具有工具链扩展,它包括 Xcode 以及构成 Swift 的所有工具和库(LLVM、LLDB、REPL 和其他工具),所有这些都针对特定的 Swift 版本。您可以将工具链视为一个捆绑的开发环境,您将其安装并提取到您的系统上,以便与特定版本一起工作。当您想要尝试不同的版本时,您必须下载并安装与目标版本相对应的工具链。
-
运行包安装器 - 这将安装 Xcode(工具链)。
注意
您为发布或开发者快照下载的 Xcode 包应该使用开源项目的开发者 ID(开发者 ID 安装器:Swift Open Source (V9AUD2URP3))进行数字签名,以防止恶意代码和篡改。Swift 安装器应该在标题栏的右侧显示一个锁。当您点击锁时,您应该看到开发者签名的详细信息。
-
选择工具链[可选] - 如果这是您机器上的唯一 Swift 版本,您可以跳过此步骤。但是,如果您有多个开发者快照或预览,您可以通过导航到首选项,选择组件 | 工具链或直接使用首选项...下列出的工具链菜单来告诉 Xcode 使用哪个版本:
![Swift 3 on Mac]()
在 Xcode 8 中选择工具链
-
接下来,您选择要开发的工具链,这将仅更改 Xcode 设置。如果您想更改命令行工具设置,您需要使用xcrun和xcodebuild进行配置。
![Swift 3 on Mac]()
Xcode 中的工具链菜单
-
每个命令都有一个选项,可以指向指定的 Swift
工具链。$ xcrun --toolchain swift $ xcodebuild --toolchain swift
一旦您在 Mac 上安装了工具链,您只需启动 Xcode,就可以开始开发了。由于我们将在接下来的几章中主要使用 Xcode 来处理新的 Swift 特性,我们将转换方向,在本章的剩余部分讨论 Linux 上的 Swift。
Linux 上的 Swift 3
Swift 团队目前支持在 Ubuntu 14.04 或 15.10(64 位)上安装 Linux。在 Linux 上,Swift 包以 tar 存档的形式分发。每个包都包括 Swift 编译器、LLDB 调试器和与在 Swift 中进行开发相关的工具。
注意
如果您无法访问 Linux 服务器,您可以使用 VirtualBox www.virtualbox.org和 Vagrant www.vagrantup.com创建虚拟机。
VirtualBox 是一个可以在多个平台上运行的虚拟化应用程序,允许您安装另一个操作系统。您可以从www.virtualbox.org/wiki/Downloads下载最新版本。
Vagrant 是一个配置和预配包,允许您安装和配置完整的发展环境。您可以在以下位置找到如何安装和配置 Linux 盒子的说明www.vagrantup.com/
-
我们需要安装我们的必需依赖项。运行以下命令:
$ sudo apt-get install clang libicu-dev-
clang:LLVM 编译器的 C 语言前端。 -
libicu-dev:一个用 C++和 C 编写的 Ubuntu 包,提供强大的全面 Unicode 和本地支持。
-
-
我们需要下载一个工具链以及一个作为工具链数字签名的.sig文件。工具链的格式为
swift-<VERSION>-<PLATFORM>.tar.gz,数字签名文件的格式相同,但扩展名为.sig。 -
我们将为 Ubuntu 14.04 安装一个预览工具链。复制工具链的链接并将其下载到您的 Linux 机器上。
-
下载
toolchain文件:$ wget https://swift.org/builds/swift-3.0-preview-2/ubuntu1404/ swift-3.0-PREVIEW-2/swift-3.0-PREVIEW-2-ubuntu14.04.tar.gz -
下载数字签名文件:
$ wget https://swift.org/builds/swift-3.0-preview-2/ubuntu1404 /swift-3.0- PREVIEW-2/swift-3.0-PREVIEW-2-ubuntu14.04.tar.gz.sig -
导入 PGP 密钥以验证工具链的完整性。您只需要下载一次密钥:
$ wget -q -O - https://swift.org/keys/all-keys.asc | gpg --import - -
我们使用导入的 PGP 密钥验证我们下载的工具链。
-
刷新您的密钥并下载任何新可用的证书:
$ gpg --keyserver hkp://pool.sks-keyservers.net --refresh-keys Swift -
然后我们验证我们下载的签名文件是否良好:
$gpg--verify swift-3.0-PREVIEW-2-ubuntu14.04.tar.gz.sig gpg: Signature made Thu 07 Jul 2016 11:12:12 PM UTC using RSA key ID 91D306C6 gpg: Good signature from "Swift 3.x Release Signing Key <swift- infrastructure@swift.org>" gpg: WARNING: This key is not certified with a trusted signature! gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: A3BA FD35 56A5 9079 C068 94BD 63BC 1CFE 91D3 06C6注意
如果我们的
*gpg*验证语句返回Bad signature,则不要打开工具链并向swift-infrastructure@swift.org报告问题。 -
从存档中提取工具链:
$ tar xzf swift-3.0-PREVIEW-2-ubuntu14.04.tar.gz -
我们需要将/usr目录添加到我们的路径中,这样我们就可以执行用于使用 REPL 环境的 swift 命令。将 Swift 工具链添加到您的路径:
$ export PATH=/home/vagrant/swift-3.0-PREVIEW-2- ubuntu14.04/usr/bin:"${PATH}"
使用 REPL
一旦我们安装了 Swift,我们就可以使用 Swift REPL(读取评估打印循环)环境,并在 Linux 上对 Swift 进行测试运行。Swift REPL 环境和 LLDB 调试器与工具链紧密相连,有助于 Swift 类型推断、语法和表达式评估。基本上,如果一次只关注一个版本的 Swift,它会使编译器、调试器和 REPL 环境的工作变得更简单。让我们启动 REPL 环境并执行一些命令,以熟悉 REPL 环境的功能。
要启动 Swift REPL,您输入 swift 命令:
$ swift
随着我们添加语句,REPL 环境足够智能,只有在您完全输入一个语句后才会执行。我们可以创建赋值语句、函数,甚至整个类。
在 REPL 提示符下,让我们分配:
1> let oneMillion = 1_000_000
oneMillion: Int = 1000000
2> let twoMillion: Int = 2_000_000
twoMillion: Int = 2000000
3> oneMillion + twoMillion
$R0: Int = 3000000
4> $R0 / 1_000_000
$R1: Int = 3
每次我们执行一个语句时,REPL 环境都会在下一行添加该语句的结果。在我们的例子中,我们已将数值赋给了两个不同的变量(oneMillion 和 twoMillion)。我们的第三个语句将这两个变量相加。注意,结果显示为 $R0 Int = 3000000。如果你没有将表达式赋给变量,Swift REPL 环境会为你创建一个变量名。我们可以在未来的表达式中使用分配的变量。在我们的第四个表达式中,我们使用存储在 $R0 中的值创建一个新的表达式,该表达式将值除以 1,000,000。
正如我在这部分前面提到的,调试器与 REPL 环境紧密耦合。如果我们添加了一个错误的表达式,REPL 环境会显示错误信息作为结果:
5> oneMillion = "one million"
error: repl.swift:11:12: error: cannot assign to value: 'oneMillion' is a 'let' constant
oneMillion = "one million"
~~~~~~~~~~~^
repl.swift:2:1: note: change 'let' to 'var' to make it mutable`
let oneMillion = 1\_000\_000`
^~~~
var
REPL 环境也支持多行语句,你可以使用它来创建函数或类。要创建多行语句,你只需箭头向下而不是按回车键。
如果你之前没有使用过 Swift 的 REPL 环境,你应该试一试。我相信 REPL 环境对于实验创建算法或快速开发测试函数非常有用。
Swift 包管理器
Swift 包管理器是 Swift 的瑞士军刀,它允许你管理你的代码依赖项,分享你自己的包,并使用其他人创建的库。这是一个极其重要的工具,了解它是用 Swift 进行任何有成效工作的必要条件。我的目标是为你提供一个快速概述,然后深入一些示例,这样我们就可以在示例中使用它来巩固核心概念。
与其他语言一样,Swift 允许你组织和分组你的 Swift 代码。Swift 将这些分组称为模块。Swift 中的模块允许开发者对公开(模块外部)的功能性和仅在模块内部可见的功能性进行控制。
作为开发者,我们使用我们创建的或其他开发者创建的模块来编写我们的软件。当我们使用其他开发者的模块时,我们就在他们的代码上创建了一个依赖。Swift 允许我们创建一个包,它由我们编写的 Swift 代码和一个清单文件组成,用于管理我们构建产品所需的一切。我们包含在包中的清单文件定义了我们正在构建的内容,因为它包括一个包名和包含内容的列表。一个 Swift 包可以有一个或多个目标,每个目标指定一个产品或一个或多个依赖项。
如果你曾经使用过 Node.js,你可以很快看到 node 的包管理器和 Swift 的包管理器之间的相似之处。两者都允许开发者定义描述应用程序所需依赖类型的清单文件。Swift 的包管理器要求你提供源代码的相对或绝对 URL 以及所需的版本。一旦提供,包管理器就会接管,为你下载和编译所需的依赖。实际上,包管理器会递归地检查每个依赖,评估依赖是否有任何依赖,并重复此过程,直到覆盖整个图。这可能会是一个很大的任务,取决于你的包的大小。
注意
更多资源
:
你可以在这里访问 Swift 包管理器的源代码:github.com/apple/swift-package-manager.
你可以在这里了解更多关于如何结构化你的清单文件的信息:github.com/apple/swift-package-manager/blob/master/Documentation/Package.swift.md
你可以在这里了解如何创建自己的包:github.com/apple/swift-package-manager/blob/master/Documentation/DevelopingPackages.md
我们的第一个 Swift 程序
让我们在 Linux 上使用 Swift 创建我们的第一个程序。我们的第一个项目将是一个包。创建一个名为 guesswho 的目录,然后进入该目录:
$ mkdir guesswho
$ cd guess who
接下来,我们需要初始化一个新的包,类型为可执行文件:
$ swift package init --type executable
Creating executable package: guesswho
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/main.swift
Creating Tests/
我想指出关于 swift package init 命令输出的一些事项。首先,使用 swift package init 命令是可选的,它仅作为一个生成你可能需要的文件和目录的工具机制。其次,包管理器期望你在 Sources 目录下放置你的源文件。你可以在 Sources 目录下进一步嵌套额外的目录,包管理器会将这些目录视为模块。最后,当你想要创建一个可执行文件时,你需要在该模块的子目录中包含一个 main.swift 文件,或者在只有一个目标的情况下,直接在 Sources 目录中包含该文件。让我们看看一个 Swift 包的示例。
创建一个包含多个模块的包:
-
mymodule/Sources/worker/workerbot.swift -
mymodule/Sources/manager/manager.swift
在上述包上运行 swift build 会创建以下内容:
-
mymodule/.build/debug/workerbot.a -
mymodule/.build/debug/manager.a
创建一个包含一个可执行文件和一个库模块的包:
-
mymodule/Sources/worker/main.swift -
mymodule/Sources/manager/manager.swift
在这个时间运行 swift build 将会产生以下结果:
-
mymodule/.build/debug/workerbot -
mymodule/.build/debug/manager.a
注意,我们的可执行文件没有扩展名;然而,我们的库文件是以 *.a* 扩展名创建的。
打开 main.swift 文件,并移除其中现有的代码。我们将用一些新的逻辑替换现有的代码。
让我们添加一个函数,该函数将递归地调用自身,并从其输入字符串的剩余字符中移除第一个字母,重复此任务,直到字符串中没有字母为止。一旦我们完成,我们将执行一个闭包来通知调用者我们已经完成:
func breakWord(combine result:String, input:String, done:(String?)->Void){
let characterArray = input.characters
let breakoutCharacter = characterArray.first
let remainingCharacters = characterArray.dropFirst()
if characterArray.count > 0{
let line = "\n\(breakoutCharacter!): \(String(remainingCharacters)) "
let newResult = "\(result) \(line)"
breakWord(combine: newResult, input: String(remainingCharacters),
done: done)
return
}
done(result)
}
接下来,我们需要处理在执行 Swift 程序时传递的参数。首先,在第 1 行,我们将参数列表存储为一个数组。然后,在第 2 行,我们检查是否至少有一个参数可以使用我们的 breakWord(combine:) 函数进行处理。第 3-8 行遍历我们的参数列表,并对每个参数调用我们的 breakWord(combine:) 函数。在第 4 行,我们使用闭包表达式来打印 breakWord(combine:) 过程的最终结果:
let arguments = Process.arguments
if arguments.count > 1{
for n in 1..<arguments.count{
breakWord(combine: "", input: arguments[n]){ (result) in
print(result!)
}
}
}
else{
print("no arguments passed")
}
注意
如果你希望将源代码全部放在一个地方查看,你可以通过以下链接下载本章的代码。
关闭并保存 main.swift 文件,然后使用 swift build 命令编译程序。你可以通过输入 guesswho 以及一个或多个参数来执行程序:
$ .build/debug/guesswho Swift 3 New Features
S: wift
w: ift
i: ft
f: t
t:
3:
N: ew
e: w
w:
F: eatures
e: atures
a: tures
t: ures
u: res
r: es
e: s
s:
摘要
在本章中,我们介绍了如何将你的开发环境配置为在 Mac 或 Linux 机器上开发 Swift。我们学习了 工具链、使用 REPL 环境、以及 Swift 包管理器。我们还创建了我们的第一个 Swift 包,我们能够在 Linux 上执行它。如果你还在这里,我们将在接下来的章节中介绍更多关于 Swift 的精彩内容!如果你足够细心,你可能已经注意到我们的示例包缺少一些东西。请放心,我们将在第九章 改进代码:使用 Xcode 服务器和 LLDB 调试中深入探讨创建和执行测试以及调试技术,改进代码:使用 Xcode 服务器和 LLDB 调试。我们还将回到 Linux,处理一个更复杂的使用案例,包括在我们的包中添加依赖项,在第十章 在服务器上探索 Swift,在服务器上探索 Swift。
第三章。迁移到 Swift 3 以更 Swifty
如果你像我一样是一位 Swift 开发者,你可能有现有的 Swift 2.2 代码,你还不准备放弃。幸运的是,Xcode 8 和内置的 Swift 迁移器 将帮助你将 Swift 2.2 项目转换为 Swift 3。我们将使用一个示例项目来演示如何使用迁移器。我们还将讨论一些有用的策略,当迁移器未能正确转换所有代码时,你可以采用这些策略。
你如何迁移你的项目…
当你第一次在 Xcode 8 中打开 Swift 2.2 项目时,你会得到选择将项目迁移到 Swift 2.3 或 Swift 3 的选项,以便利用新的 SDK。然而,如果你决定现在还不是转换的最佳时机,你总是可以在以后进行。Swift 迁移器工具在 Xcode 8 的 编辑 | 转换 | 转换为当前 Swift 语法… 菜单下可用。
如果你想要使用适用于 iOS 10、macOS 10.12、watchOS 3 或 tvOS 10 的新 SDK,你需要转换你的代码。以下列出了迁移项目的两种选项:
选项 1 - 迁移到 Swift 3
如果你想要针对最新的 Swift 构建并使用 Xcode 8 的所有新功能,请选择迁移到 Swift 3 的选项。迁移器将修改你的源文件以符合新的 Swift 3 语法。
选项 2 - 迁移到 Swift 2.3
如果你只想使用新的 SDK,但还没有准备好或能够迁移到 Swift 的最新版本,那么请选择 Swift 2.3 选项。Swift 2.3 是 Swift 2.2 加上新的 SDK。在这种迁移场景中,迁移器将修改构建设置以使用遗留 Swift(Swift 2.2),同时进行选择性的源代码更改,以便你的项目可以针对新的 SDK 进行构建。
提前规划
让我们面对现实,Xcode 正在要求你勇敢地使用它的黑盒工具对你的项目进行不可逆的更改。虽然我是一个狂热的苹果粉丝,但我怀疑我是否会不假思索地按下那个闪亮的迁移按钮,而不考虑过程中可能出错的地方。我不从事废弃项目从头开始的工作。说实话,谁真的会呢?为了避免迁移器可能带来的糟糕体验,你真的应该在迁移项目之前考虑以下所有作为预工作:
-
确保你的现有代码库正在使用版本控制系统,例如 Git (
git-scm.com) 或 Subversion (subversion.apache.org)。如果你运行迁移器,并且输出结果不符合预期(或者发生其他意外情况),你将可以安心地知道你总是可以回到原始版本。 -
确保您的项目在最新版本的 Xcode(7.3 或 7.3.1)上可以编译。您想要确保包括测试在内的一切都在 Xcode 7 下按预期运行。如果您的项目不能作为 Swift 2.2 项目编译和运行,那么在此时使用迁移器只会给您带来麻烦。在迁移到 Swift 2.3 或 Swift 3 之前,请确保一切正常工作。您的目标是现在和迁移后测试都能通过。
-
确保您想要转换的每个目标都使用活动方案进行构建。Swift 迁移器使用 Xcode 中的活动方案来确定它应该检查哪些源文件进行迁移。您可以通过使用“编辑方案”菜单查看方案设置来验证迁移器将考虑的目标。一旦进入,切换到方案的构建选项卡,并确保所有您想要迁移的目标都已勾选。
小贴士
您可以为项目添加一个新的方案,并包括所有目标。使用单独的方案进行迁移将确保您不会修改主方案中的设置。迁移完成后,您可以删除该方案。
如果您的项目使用 Carthage 或 CocoaPods,或者另一个不是与主项目一起构建的项目,那么您有两个选择:
-
将外部项目整合到主项目中:选择此选项意味着您打算将其他项目的源文件复制到现有的项目中。除非您真的非常了解项目的结构并打算继续维护它,否则我可能会犹豫这样做。一旦您复制了文件,您将无法连接到项目中正在进行的更新和持续工作。未来的更新将需要您每次都执行相同的复制、粘贴和配置操作。
-
不采取任何行动,即您现在不进行转换:选择此选项,您只需迁移您的代码,并继续链接到任何第三方项目而不进行迁移。这对于外部项目的所有者来说可能是一个值得考虑的好选项,因为所有者可能对如何迁移项目有更好的见解,但尚未这样做。如果不是您的项目,您可能最好等待项目所有者和维护者提供更新。有很大可能性,您可以继续使用第三方项目。开发者正在实施的一个最后趋势是创建 Swift 2.3 和 Swift 3 分支,以简化在 Xcode 8 测试版中开发时的后续转换。
使用 Xcode 的 Swift 迁移工具进行迁移
在完成预工作后,是时候迁移您的代码了。为了我们的目的,我们将使用一个简单的井字棋项目,您可以从 Packt 网站下载:
-
启动 Xcode 8 并打开井字棋项目:在首次启动时,Xcode 将询问您是否想要将您的 Swift 2.2 项目迁移到使用最新 SDK。选择“转换”以开始迁移。
![使用 Xcode 的 Swift 迁移工具进行迁移]()
-
选择 Swift 2.3 或 Swift 3:在选择转换后,您将看到一个另一个屏幕,基本上告诉您 Xcode 将修改您的文件。您还被告知,一旦迁移完成,您将有一个选择接受更改或取消更改而不永久更改项目文件的选项。模态对话框还提供了一份免责声明,说明 Swift 迁移工具并不完美,迁移完成后您可能需要做出额外的更改。按下一步,然后选择 Swift 3。
![使用 Xcode 的 Swift 迁移工具进行迁移]()
-
选择要转换的目标:如果您有一个具有多个目标的项目,您需要确保您已选择一个方案,该方案将构建您想要迁移到最新 Swift(或 Swift 2.3)的所有目标。在我们的例子中,只有一个目标需要转换,并且它应该已经勾选。如果您想跳过一个目标,只需取消选中其对应的复选框即可。
![使用 Xcode 的 Swift 迁移工具进行迁移]()
小贴士
您可以在项目上多次运行迁移。如果您不想修改您的方案,您只需为要转换的每个方案运行迁移。只需将选定的方案设置为活动方案,然后开始迁移。
-
在预览屏幕上审查更改:在选择目标并按下一步后,迁移工具将开始工作。一旦过程完成,您将看到一个预览屏幕,其中包含您要审查的更改前后的内容。Xcode 修改的每个源文件都将可在预览窗口中查看。强烈建议您检查每个文件,以确保您理解代码更改并同意推荐的更改,然后再接受它们。
-
在审查更改时,您可以选择永久修改的内容。每个修改过的文件都有一个编号的更改列表,您可以选择丢弃或进一步修改。您可以通过展开单个突出显示的更改按钮并选择忽略操作来丢弃文件中的特定更改。在文件级别,您可以通过在左侧侧边栏视图中取消选中文件来丢弃对文件的更改。
![使用 Xcode 的 Swift 迁移工具进行迁移]()
-
保存您的更改:在审查完更改后,按Save按钮。您将看到一个确认对话框,警告您更改将是最终的。一旦您允许 Swift 迁移工具应用所有更改,您将无法使用迁移工具撤回更改。点击Continue**按钮以确认您确实想要接受更改。

保存更改后,迁移完成。Xcode 将尝试编译您的项目,并在存在任何构建问题时通知您。在我们的例子中,我们有一个警告和两个构建错误。警告告诉我们我们没有使用所有推荐的项目设置。继续双击警告,让 Xcode 将我们的项目设置更改为推荐设置。在这种情况下,Xcode 建议我们使用整个模块优化。
注意
整个模块优化是一个构建设置,允许编译器在执行代码的高级优化时考虑整个模块。当编译器对您的代码具有模块级别的可见性时,优化决策将基于更多关于受影响例程在整个模块中如何使用的信息,而不仅仅是单个文件内。更好的优化会导致代码运行更快。您可以通过观看 WWDC 2015 上的“优化 Swift 性能”讲座了解更多关于整个模块优化和优化 Swift 性能选项的信息 developer.apple.com/videos/play/wwdc2015/409/。

剩余的构建错误可能或可能不会出现在您的机器上。在我的情况下,错误是由于我之前使用 Xcode 7.3.1 构建了项目。每次您构建项目时,Xcode 都会缓存编译的中间产品以提高未来的构建效率。如果您的代码的一部分没有更改,Xcode 将使用缓存的副产品来简化重新编译。这些缓存数据存储在 Xcode 用于未来构建的文件夹中。在我的情况下,衍生数据文件夹包含了一些对于项目迁移到 Swift 3 和 Xcode 8.0 后不再相关的代码。使用 Product > Clean 命令清理您的项目。现在,您的项目应该可以成功编译。
迁移过程中出现问题时进行故障排除
不幸的是,并非所有项目都能完美转换。swift.org 网站维护了一个已知迁移器问题的列表,您可以参考(swift.org/migration-guide/)。例如,迁移器可能会在编辑器边缘建议如何通过 fixit 修复问题,而无需自动为您执行。您在这个例子中看到 fixit 的原因可能是您的项目可能具有多个共享某些形式依赖的目标,这可能会使迁移器困惑。这是一个已知问题,但在迁移后很容易处理。您只需点击 fixit 选择推荐的操作,Xcode 将完成剩余的工作。我们没有足够的时间涵盖所有已知的迁移问题,但我们将介绍一些更重要的问题。
我们可以将已知的迁移器问题分为三个一般领域:
-
标准库问题:通常,这些问题涉及集合例程和类型
-
新的 SDK 问题:这些问题涉及迁移器未能将旧类型和协议关联到新 SDK 中创建的新类型。你也可能会遇到与 Foundation 框架重整相关的问题。我们在第八章,哇!看看 Foundation 框架的新功能中涵盖了 Foundation 框架的主要变更。
-
Swift 3 语言变更:这些迁移器问题与 Swift 3 语言变更有关。在函数或构造在 Swift 3 中不再可用的情况下,迁移器不会采取任何行动,你需要手动更改代码。
你需要结合使用在swift.org网站上列出的已知问题页面以及生成的警告/错误消息来确定如何在迁移后修复出现的构建错误。如果编辑器边缘没有提供修复提示,你将需要手动纠正问题。
快速解决问题的策略
-
修复建议:迁移完成后,检查警告/错误部分以查找修复操作。这些操作中的每一个都会为你提供有关如何修复相关代码的建议。只需选择操作,Xcode 就会应用代码更改。
-
迁移注释:即使你的项目编译成功,仍然有可能在转换过程中,迁移器遗漏了一些无法处理的点。在这种情况下,迁移器会在你的代码中留下
/*Migrator FIXME: ...*/注释。你将需要搜索这些注释,并在评估了代码块后决定是否需要手动进行更改。 -
使用新的 Foundation 框架值类型:在检查你的项目代码时,你可能会看到 Swift 正在将类型转换为以"NS"为前缀的类型。在使用 Swift 3 时,你可能不希望使用遗留的 Foundation 类型。同样,迁移器在查找和纠正这些问题方面做得相当不错;然而,你仍然建议手动搜索"NS"前缀。如果你发现任何'NS'前缀的出现,你将有机会确定每个是否正确,或者是否应该使用不带"NS"前缀的新 Foundation 类型。在第八章,哇!看看 Foundation 框架的新功能中,我们将介绍 Foundation 框架的变更,包括新的值类型。
-
用户定义的集合类型可能会产生迁移问题:在 Swift 3 中,集合需要处理在其项目集合中向前和向后移动。你需要采用新的 Collection 协议(
developer.apple.com/reference/swift/collection)函数来定义如何递增索引。如果你的集合支持递减,也有一个针对该功能的新的协议函数。最后,有一个协议函数允许你的集合支持随机访问一个项目。如果你看到与你的自定义集合不遵守 Collection 协议相关的错误,那么很可能你没有添加一个或多个以下的新协议函数:-
func index(after: Index) -> Index -
func index(before: Index) -> Index -
func index(_: Index, offsetBy: Int) -> Index -
func distance(from: Index, to: Index) -> IndexDistance
-
-
Swift 3 中移除的功能:例如,C 风格的 for 循环已被从 Swift 3 中移除。你必须手动将其重写为
for…in语句。
希望到这一点,你已经明白了迁移工具将处理你大多数日常使用场景。对于这些问题,你需要使用警告和错误消息来解析正在发生的事情。你应该首先检查 Swift.org 上的已知问题部分。
摘要
Swift 迁移工具是一个节省时间的优秀工具,当你需要将 Swift 2.2 项目迁移到 Swift 3(或 Swift 2.3)时,它会非常有用。我们了解到,为了充分利用 Xcode 8 提供的所有功能,我们必须将现有的 Swift 2.2 项目迁移到 Swift 3。我们还了解到,通过选择迁移到 Swift 2.3(Swift 2.2 加上新 SDKs),我们可以在不迁移到 Swift 3 的情况下使用新的 SDKs。在未来任何时候,我们都可以使用 编辑 | 转换 | 转换为当前 Swift 语法... 菜单来启动迁移工具。最后,我们了解到迁移工具并不完美,它可能无法转换所有内容。迁移完成后,我们可能需要手动做一些更改才能使一切正常工作。在下一章中,我们将开始介绍 Swift 3 的核心语言变化。
第四章:Swift 核心变更将让你欲罢不能
许多库都受到了影响以完成这项工作,包括 Swift 标准库、所有 Cocoa 和 Cocoa Touch 框架、Core Graphics 和 Grand Central Dispatch。随着 Swift 3 的发布,我们可以期待一些减少语言与 Objective-C 链接尴尬性的变化,展现出更多的 Swifty-ness。Swift 团队引入了新的 API 指南,目的是赋予语言其自身的特色。结果是整个语言范围内的大规模重命名和重构工作。Swift 3 在与 Objective-C 和 C API 的交互方面经历了一次巨大的改头换面。Swift 团队的目标是让你的开发体验感觉更像 Swift,而不是直接将 Objective-C 倒入你的代码中。Swift 是它自己的语言,应该有自己的感觉,就像任何其他编程语言一样。然而,Swift 的早期版本受到了与 Objective-C 和 C API 交互需求的强烈影响。
在本章中,我们将快速概述编写良好 Swift API 的哲学。之后,我们将用剩余的章节来探讨 Swift 3 中对 Objective-C 功能的引用和使用以及从 Objective-C 和 C 导入代码到 Swift 3 的语言改进。Swift 3 的每一次语言变化都由一个或多个 Swift Evolution 提案所涵盖。当我们介绍一个新特性时,我还会提供记录该变更背后理由的提案编号。虽然了解新特性的实际理由对于理解如何实现其代码不是至关重要的,但我认为了解背后所付出的努力和有时发生的辩论是有趣的。Swift Evolution 仓库包含了大量关于已接受和被拒绝提案的信息。如果你是一个细心的观察者,你还会看到一些被接受用于 Swift 3 发布但未能及时在发布中实现提案。
大规模重命名
让我们从 Swift API 设计指南的提案开始。大规模重命名提案共同代表了一个非常大的任务,包括 SE-0005、SE-0006、SE-0086 和 SE-0088。实现 API 指南代表了 Swift 3 语言的最大变化。我无法在这本简短的书中涵盖所有导致大规模重命名提案的 API 变化。幸运的是,你不需要理解库中每行更改的内容就能在 Swift 3 中高效工作。你有两个极有价值的资源,只需付出很少的努力就能带来回报。第一个资源是 Swift 迁移工具,它可以将现有的 Swift 项目转换为最新的语法。当你使用 Swift 迁移器时,你可以将你的 Swift 2.2 项目转换为 Swift 3,并免费获得大多数更改。第二个极其宝贵的资源是 Swift API 指南,它旨在帮助你编写更“Swift”的代码。Swift API 指南基于以下原则,如 swift.org/documentation/api-design-guidelines/ 上引用:
-
使用点的清晰度:这是你的最重要的目标。例如方法和属性等实体只声明一次但被反复使用。设计 API 以使这些使用清晰简洁。在评估设计时,阅读声明通常是不够的,总是要检查使用案例以确保它在上下文中看起来清晰。
-
清晰度比简洁性更重要:虽然 Swift 代码可以很紧凑,但使用尽可能少的字符来实现最小化代码并不是目标。在 Swift 代码中,简洁性(如果存在)是强大类型系统和自然减少模板代码的特性的副作用。
-
编写文档注释:这是针对每个声明。通过编写文档获得的见解可能会对你的设计产生深远的影响,所以不要推迟。
有关采用这些指南的更多详细信息,请参阅 WWDC 2016 关于 Swift API 指南的讲座 developer.apple.com/videos/play/wwdc2016/403/。
在 Swift 3 中引用 Objective-C 代码
使用 Swift 3,我们得到了一系列使使用 Objective-C 和 C API 更愉快的变化。我们将介绍这些重要变化,这些变化将使在 Swift 3 中编写代码更加高效。
引用属性获取器和设置器的 Objective-C 选择器 - SE-0064
在 Objective-C 中,我们可以使用一个称为 选择器 的类型来引用方法的名称。Swift 2.2 引入了 #selector 表达式,以消除提供字符串字面量作为选择器名称时的易错性。在 Swift 3 中,语言通过允许你引用获取器和设置器方法来构建在 #selector 表达式之上。这个特性允许我们引用对象的获取器和设置器属性。让我们通过一个例子来看看我们如何访问 ClassRoom 类上某个属性的设置器:
class ClassRoom: NSObject{
var roomNum: String
init(roomNum: String){
self.roomNum = roomNum
}
}
let classRoom = ClassRoom(roomNum: "1-D1")
classRoom.perform(#selector(setter: ClassRoom.roomNum), with: "2-D3")
我们现在可以使用 #selector(setter: ClassRoom.roomNum) 或 #selector(getter: ClassRoom.roomNum) 来访问 roomNum 的设置器。一旦我们有了引用,我们就可以使用 Objective-C 中可用的任何 NSObject 执行方法。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0064-property-selectors.md 阅读提案
引用 Objective-C key paths [SE-0062]
与选择器类似,在 Swift 2.2 中使用 Objective-C keypaths 需要我们使用字符串字面量。Swift 3 引入了 #keyPath 表达式,通过用可以在编译时检查的对象替换易错的字符串字面量来提高准确性。下面的例子展示了 Swift 2.2 中是如何处理 keypaths 的,以及 Swift 3 中引用 keypaths 的改进。
对于 Objective-C:
class Employee: NSObject{
var firstName: String
var lastName: String
var boss: Employee?
init(firstName: String, lastName: String, boss: Employee? = nil){
self.firstName = firstName
self.lastName = lastName
self.boss = boss
}
}
let bossMan = Employee(firstName: "Jack", lastName: "Spark")
let rocko = Employee(firstName: "Rocky", lastName: "Jones", boss: bossMan)
对于 Swift 2.2:
rocko.value(forKeyPath: "Employee.boss.firstName")
对于 Swift 3:
#keyPath(Employee.boss) // => boss
rocko.value(forKeyPath: #keyPath(Employee.boss.firstName)) // => Jack
在我们上面的 Swift 3 示例中,我们使用了编译时检查的 #keyPath 表达式,这允许我们安全地访问值。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0062-objc-keypaths.md 阅读提案
从 Objective-C 和 C API 导入到 Swift 3
如果你维护 Objective-C 或 C 库,并希望为 Swift 提供更干净的语法,本节是为你准备的!我们现在有能力提供更多控制,以确定如何从 Objective-C 和 C 导入常量、全局函数和泛型到 Swift。
将 Objective-C 常量作为 Swift 类型导入 [SE-0033]
在头文件中定义的全局常量,在 Swift 中以相同的全局作用域导入。在许多情况下,将相关的常量分组在一起会更有帮助。在 Swift 3 中,你现在可以使用 NS_STRING_ENUM 或 NS_EXTENSIBLE_STRING_ENUM 注释你的类型声明,以便将这些声明导入为公共类型的成员。
将其作为结构体导入
如果你希望你的常量作为结构体的成员导入,那么请将 NS_EXTENSIBLE_STRING_ENUM 添加到你的常量类型声明的末尾。为了更一致地导入到 Swift,请使用与类型相同的名称作为前缀。在我们的例子中,我们创建了一个 MPPlatformIdentifier 类型,我们将其用作前缀来命名我们的常量:MPPlatformIdentifierIOS、MPPlatformIdentifierMacOS。
对于 Objective-C:
typedef NSString * MPPlatformIdentifier NS_EXTENSIBLE_STRING_ENUM;
MPPlatformIdentifier const MPPlatformIdentifierIOS;
MPPlatformIdentifier const MPPlatformIdentifierMacOS;
这将导入到 Swift 中作为:
struct MPPlatformIdentifier : RawRepresentable {
typealias RawValue = String
init(rawValue: RawValue)
var rawValue: RawValue { get }
static var IOS: MPPlatformIdentifier { get }
static var macOS: MPPlatformIdentifier { get }
}
以枚举方式导入
您的另一个选择是将常量导入为枚举类型。当您在类型末尾添加 NS_STRING_ENUM 时,您的常量将作为枚举导入。您使用此新类型定义的每个常量都将添加到 Swift 中的枚举中。
对于 Objective-C:
typedef NSString * MPVersionEnum NS_STRING_ENUM;
MPVersionEnum const MPVersionEnumV1;
MPVersionEnum const MPVersionEnumV2;
MPVersionEnum const MPVersionEnumV3;
以以下方式导入到 Swift 中:
enum MPVersionEnum: String{
case V1
case V2
case V3
}
注意
您可以在以下位置阅读提案:github.com/apple/swift-evolution/blob/master/proposals/0033-import-objc-constants.md
导入 Objective-C 轻量级泛型 [SE-0057]
在 Swift 2 中,您可以导入并交互 Objective-C 轻量级泛型。虽然您可以将任何形式的轻量级泛型导入到 Swift 2 中,但只有 Foundation 类型(NSArray、NSSet 和 NSDictionary)在导入后保留了它们的类型信息。
注意
您可以通过阅读有关此主题的 Swift 文档来了解更多关于轻量级泛型的信息:developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html
在 Swift 3 中,您可以导入自己的 Objective-C 泛型而不会丢失类型信息。在我们的示例中,我们使用 Foundation 集合类型和自定义泛型类定义了几个属性类型。请注意,在 Swift 2 和 Swift 3 中,泛型都正确地导入了 Foundation 集合类型的类型信息。
对于 Objective-C:
@property NSArray<MyClass *> * myClasses;
@property NSDictionary<NSString *, MyClass *> * myClassDictionary;
@property NSSet<MyClass *> *mySet;
@property MyCollection<MyClass *> *myCollection;
@interface MyCollection<__covariant ObjectType> : NSObject
-(void) addItem:(ObjectType)item;
@end
以以下方式导入到 Swift 2 中:
var myClasses: [MyClass]
var myClassDictionary: [String : MyClass]
var mySet: Set<MyClass>
var myCollection: MyCollectionMyCollection Classfunc addItem(item: AnyObject!)
很遗憾,当我们导入到 Swift 2 时,我们自定义泛型属性 myCollection 的类型信息丢失了。Swift 正确地确定 myCollection 属性的类型为 MyCollection,但参数化数据丢失了。由于我们在导入时丢失了类型信息,访问 myCollection 类的 addItem() 方法将导致使用 AnyObject 作为参数类型的签名。这不是我们期望的类型 MyClass,与 Objective-C 签名匹配。
在 Swift 3 中,这个导入问题得到了解决。我们的自定义 Objective-C 泛型按预期导入。检查我们的 Swift 3 导入,我们可以看到所有的泛型都保留了它们的类型信息,并且我们的类方法将完全匹配自定义泛型类的 Objective-C 对应者。
以以下方式导入到 Swift 3 中:
var myClasses: [MyClass]
var myClassDictionary: [String : MyClass]
var mySet: Set<MyClass>
var myCollection: MyCollection<MyClass>
MyCollection Class
func addItem(_ item: MyClass)
注意
您可以在以下位置阅读提案:github.com/apple/swift-evolution/blob/master/proposals/0057-importing-objc-generics.md
以成员方式导入 [SE-0044]
许多 C API 提供了允许你创建、访问和修改 C 结构的函数。以原样导入这些库将把这些函数添加到你的 Swift 全局命名空间中。虽然这在许多情况下是可行的,但可能更倾向于将这些导入分组在 Swift 的常见类型下。Swift 3 允许我们使用 CF_SWIFT_NAME 宏来控制初始化器、属性和方法在 Swift 中的显示方式。让我们看看我们如何完成这些任务。
定义一个初始化器
我们可以通过在头文件中函数定义后附加 CF_SWIFT_NAME 宏来修改 C 函数以创建 Swift 初始化器。为了告诉 Swift 我们想要将函数添加为特定类型的初始化器,我们需要提供类型(MyPlatform)后跟一个点和我们想要的 init 格式(即 C 函数定义中的参数)。Swift 将我们的新初始化器作为 MyPlatform 的扩展导入,代表我们的常见类型。
小贴士
Swift 添加任何扩展都需要存在一个常见类型。Swift 不会为你创建这个类型,如果不存在类型,Swift 将会静默失败。
对于 C:
MyPlatform* MyPlatformWithIdentifier(MPPlatformIdentifier identifier) CF_SWIFT_NAME(MyPlatform.init(identifier:));
这将导入到 Swift 中为:
extension MyPlatform { init(identifier: MPPlatformIdentifier) }
创建获取器和设置器
除了初始化器之外,我们还可以创建计算属性。我们可以定义将被导入到 Swift 中的获取器和设置器。对于属性的获取器,我们需要在作为获取器的 C 函数末尾添加 CF_SWIFT_NAME 宏,并提供一个常见类型和属性名称。我们的设置器将与获取器类似,我们将在作为设置属性服务的 C 函数中添加宏。下面是语法在实际中的应用。在 Swift 3 中,获取器和设置器将通过扩展添加到常见类型 MyPlatform。
对于 C:
//getter
MPPlatformIdentifier MyPlatformGetIdentifier(MyPlatform *platform) CF_SWIFT_NAME(getter:MyPlatform.platformId(self:));
//setter
Void MyPlatformSetIdentifier(MyPlatform *platform, MPPlatformIdentifier identifier) CF_SWIFT_NAME(setter:MyPlatform.platformId(self:newValue:));
这将导入到 Swift 中为:
extension MyPlatform {
var platformId: MPPlatformIdentifier { get set }
}
添加方法
在我们将内容导入到 Swift 3 的过程中,我们可以将方法分组在常见类型下。这是通过在我们的头文件中 C 函数定义后附加 CF_SWIFT_NAME 宏来实现的。你需要为宏提供一个常见类型和一个与 C 函数使用相同数量变量的方法签名。Swift 将处理确定参数类型。你只需要提供你想要在 Swift 中用作签名的名称。
对于 C:
MPPlatformIdentifier MyPlatformRetreiveId(MyPlatform *platform, MPPlatformIdentifier identifier) CF_SWIFT_NAME(MyPlatform.retreiveId(self:identifier:));
这将导入到 Swift 中为:
extension MyPlatform {
func retreiveId(identifier: MPPlatformIdentifier) -> MPPlatformIdentifier
}
创建静态变量
最后,我们可以在 Swift 3 中将全局变量作为与常见类型关联的静态变量导入。我们只需要为我们的 CF_SWIFT_NAME 宏提供一个常见类型和变量名称。我们的静态变量将通过扩展添加到 Swift 中的常见类型。
对于 C:
extern const MPPlatformIdentifier *MyPlatformTVOS CF_SWIFT_NAME(MyPlatform.tvOS);
这将导入到 Swift 中为:
extension MyPlatform { static var tvOS: MPPlatformIdentifier }
注意
你可以在以下位置阅读提案:github.com/apple/swift-evolution/blob/master/proposals/0044-import-as-member.md
摘要
我们刚刚完成了 Swift API 指南原则的讲解。此外,我还提供了如何查找记录每个变更动机的 Swift Evolution 提案的资源。我们还涵盖了在 Swift 中使用 Objective-C API 时处理#selector和#keyPath表达式的全新功能。最后,我们探讨了如何与 C API 一起工作并控制它们如何导入到 Swift 3 中。在下一章中,我们将介绍更多的语言变更。请保持关注,因为我们还有很多新内容要介绍!
第五章. 函数和运算符变化 – 以新的方式完成任务
如果你想在 Swift 中编写有用的代码,或者在任何编程语言中,你至少需要创建函数和使用运算符。在本章中,我们将探讨函数声明和使用方面的变化,以及这些变化如何转化为更好的 Swift 代码。我们还将解释运算符的变化,并突出一些已被从语言中移除的运算符。
继续上一章的主题,我将提供 Swift 进化提案编号。让我们开始吧!
函数声明变化
Swift 提供了一套非常灵活的规则来定义函数。你可以创建没有参数、有参数,甚至有参数标签的函数。每个 Swift 函数都有一个类型、参数(或没有参数)以及返回类型。对于 Swift 3,语言经过调整以使事物更加一致和简单。
一致的参数命名 [SE-0046]
参数命名用于在函数定义中为每个参数命名。在 Swift 2.2 及更早版本中,函数参数可以同时定义本地和外部标签。本地参数标签是必需的,因为这个标签用于在函数体中引用参数。当提供外部参数标签时,它用于实际的函数调用中。你可以将外部标签视为在调用位置的闪亮的描述性名称,以提供对参数表示的深入了解。内部标签,正如其名称所暗示的,是函数在逻辑实现中使用的名称。由于没有人会看到这个本地参数,你可以将其缩短以节省一些按键。
这里事情变得有趣了。默认情况下,Swift 2.2 在调用函数时会丢弃你的外部名称。进一步增加混乱的是,当外部名称不存在时,Swift 使用你的本地名称作为任何剩余参数的外部名称。真正奇怪的是,Swift 只对函数这样做。当你创建一个类、结构体或枚举初始化器(一种设置初始值的特殊类型的函数)时,Swift 将为每个参数创建一个外部名称。
注意
Swift 枚举可以使用原始值类型定义。当你使用原始值创建 Swift 枚举时,你将得到一个初始化的枚举或 nil。你可以在 developer.apple.com/library/prerelease/content/documentation/Swift/Conceptual/Swift_Programming_Language/Enumerations.html#//apple_ref/doc/uid/TP40014097-CH12-ID145 上了解更多关于 Swift 枚举的信息。
删除第一个参数名称的主要原因似乎是为了与 Objective-C 保持历史正确性。Objective-C 开发者被指示将第一个参数名称纳入函数名称中。Swift 也采用了这种行为,这可能是通过 migrator 将 Objective-C 库最初翻译成 Swift 的副作用。
Swift 是一种不断发展的语言,并且随着每个新版本的发布而变得越来越好。为了与新的 API 命名指南保持一致,Swift 3 现在默认使用局部名称作为所有参数的外部名称,包括第一个参数。
在 Swift 2.2 中:
func gettingSwiftyUsingPeopleNamed(names: [String],
descriptionNames: [String],
shouldCapitalize: Bool){
// our function only has local names so those are the ones used
}
gettingSwiftyUsingPeopleNamed(["Joe", "Mark", "Roy", "Jessica"],
descriptionNames: ["Awesome", "Silly", "Tall", "Short"],
shouldCapitalize: true)
在 Swift 3 中:
func gettingSwiftyUsing(names: [String],
descriptions:[String],
shouldCapitalize: Bool){
// our function only has local names so those are the ones used
}
gettingSwiftyUsing(names: ["Joe", "Mark", "Roy", "Jessica"],
descriptions: ["Awesome", "Silly", "Tall", "Short"],
shouldCapitalize: true)
注意
最后的想法——如果您在函数调用中不喜欢使用参数标签,您可以使用下划线作为外部名称来抑制它们。这对于任何参数位置都适用。
func boxit(_ width: Double, _ height: Double){
// argument label will be omitted in function call
}
boxit(23, 14)
注意
您可以在以下链接中阅读提案 github.com/apple/swift-evolution/blob/master/proposals/0046-first-label.md
删除声明中的柯里化函数语法 [SE0002]
在 Swift 2 中,您有创建柯里化函数的能力;这些函数相当令人困惑,在 Swift 中似乎也没有多少价值。许多开发者在使用它们时产生的困惑主要集中在柯里化参数是否是主参数列表的一部分,或者柯里化参数是否表示新函数参数列表的开始。让我们考虑以下使用柯里化参数的示例:
func curried(cosx: Int)(siny: Int) -> Float {
return (Float(cosx) * Float(siny)) / Float(cosx)
}
let result = curried(2)(siny:3)
判断 (siny: Int)-> Float 是否是参数的一部分非常令人困惑。Swift 团队最终决定我们根本不需要这种语法。因此,Swift 3 删除了这种语法,并建议您将函数重写为返回闭包:
func curriedV2(cosx: Int)->(Int)->Float{
return { siny in
(Float(cosx) * Float(siny)) / Float(cosx)
}
}
let intermediateFunctionReturn = curriedV2(2)
let result2 = intermediateFunctionReturn(3)
在我们修订的示例中,我们定义了一个返回闭包的函数。然后,我们将这个初始调用的结果赋值给名为 intermediateFunctionReturn 的变量。最后,我们调用 intermediateFunctionReturn 闭包,传递我们的 Int 参数,以获取最终结果。
注意
您可以在以下链接中阅读提案 github.com/apple/swift-evolution/blob/master/proposals/0002-remove-currying.md
默认情况下对未使用结果的警告 [SE-0047]
属性是应用于声明或类型的特殊构造。您使用 @ 符号后跟一个名称以及可选的任何属性参数(括号内)来指定属性。
@<attribute name>
@<attribute name>(attribute arguments)
在 Swift 2 中,@warn_unused_result 属性应用于函数或方法,以让编译器知道如果未使用结果就调用属性或方法,则应向用户发出警告。@warn_unused_result 属性还允许你提供消息或 mutable_variant 属性参数。当你想让开发者知道完成相同任务的 mutating 方法的名称时,使用 mutable_variant 选项。使用该属性的目的在于为使用你的方法的开发者提供指导,即返回值很重要,应该使用。
例如,Swift 2 为 Foundation 集合类提供了 sort() 方法和一个 sortInPlace() 方法(mutating)。如果你对一个数组调用 sort()(非 mutating)但没有使用结果,编译器会警告你可能真的需要 sortInPlace() 方法,该方法会修改数组且不返回任何内容。下面你可以看到 @warn_unused_result 的一个示例用法。
@warn_unused_result(mutable_variant="sortInPlace")
public func sort() -> [Self.Generator.Element]
开发者对 @warn_unused_result 的主要问题是,它只有在将属性应用于所有相关位置时才有所帮助(即这是你主动采取的策略)。如果你忘记添加属性,则不会发出警告。在 Swift 3 中,逻辑相反,现在默认情况下你会收到关于未使用结果的警告。如果你想明确地让编译器知道返回值可以安全地忽略,你可以使用 @discardableResult 属性。下面是一个演示其用法的示例。
@discardableResult
func complexFunctionNonEssentialResult()->Int{
// do complex logic
// return trivial status code
return 123
}
注意
在 Swift 3 中,mutating 方法 sortInPlace() 变成了 sort(),而非 mutating 版本则从 sort() 变成了 sorted()。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0047-nonvoid-warn.md 上阅读更多关于该提案的信息。
从函数参数列表中移除 var [SE-0003]
在 Swift 2 中,在函数参数列表中使用 var 是有效的。由于参数类型默认是不可变的,开发者尝试在参数列表中使用 var 来向函数实现传递一个可变参数。虽然你可以在 Swift 中这样做,但实际上这证明是一个相当无用的策略。让我来解释一下。是的,你可以使用 var 关键字将一个可变变体传递到函数体中。然而,你对变量所做的任何更改都不会传播回原始类型。因此,你使用的是一个作用域限于函数体的可变副本。让我们通过一个示例来查看这在实践中是如何工作的:
func booyah(howHigh: Int){
howHigh += 100 // -> illegal assignment
}
func booyahTake2(var yaFeelMe: Int){
yaFeelMe += 100 // -> legal but doesn't write back to caller
}
你同样可以轻松地通过在函数体内部将传入的参数赋值给一个局部副本来实现相同的目标。下面是等效的函数定义:
func booyahTake3(yaFeelMe: Int){
var yaFeelMe = yaFeelMe
yaFeelMe += 100
}
最后,许多开发者会将 var 参数与标记为 inout 的参数混淆。两种版本都会提供一个可变的局部副本,但只有 inout 变体才会将更改传播回函数调用者。鉴于整体上的混淆,Swift 3 中已经移除了 var 作为参数修饰符。在以下示例中,howManyTimes 变量通过将其值传播回函数调用者而更新,因为它被标记为 inout 参数:
func trifecta(inout howManyTimes: Int){
howManyTimes += 2000 // updates the actual passed in variable
}
注意
你可以在这里阅读提案:github.com/apple/swift-evolution/blob/master/proposals/0003-remove-var-parameters.md
移除 ++ 和 -- 运算符 [SE-0004]
增量(++)和减少(--)运算符被添加到 Swift 中,因为它们在 C 语言中存在。此外,许多转向 Swift 的开发者习惯于在其他语言中看到它们。让我们来检查这些运算符是如何工作的,然后我们可以讨论一些需要注意的问题。
var row = 0
let currentRow = ++row // pre - adds 1 to row than assigns new value
let nextRow = row++ // post - assigns than adds 1 from row
let previousRow = --row // pre - minus 1 from row than assigns to value
let backOneRow = row-- // post - assigns than subtracts 1 from row
不利之处/需要注意的问题:
-
很容易弄错增量/减量运算符的前置和后置部分,这会导致你得到错误的结果
-
语法是
+=或-=的简写,这并没有节省你多少按键,与+= 1或-= 1相比 -
这些运算符主要在 C 风格的
for…in循环中使用,其中返回值被忽略。由于我们已经有for…in循环、范围、映射以及enumerate/iterate函数,我们不需要这些运算符 -
你只能使用这些运算符与一组有限的类型(例如整数和浮点标量)一起使用
-
如果你正在学习你的第一门编程语言,这些运算符会增加你需要学习的内容量,而没有提供通过 Swift 提供的其他功能无法获得的有意义的价值
实际上,Swift 团队认为保留这些运算符的缺点超过了优点,因此决定在 Swift 3 中移除它们。
注意
你可以在这里阅读提案:github.com/apple/swift-evolution/blob/master/proposals/0004-remove-pre-post-inc-decrement.md
移除 C 风格的 for 循环 [SE-0007]
与我们刚才讨论的增量/减量运算符类似,C 风格的 for…in 循环似乎也是为了其 C 语言血统而被添加到 Swift 中的。Swift 提供了几个比 C 风格循环更好的 Swifty 迭代和循环约定。事实上,C 风格的循环并不常用。一旦开发者开始精通 Swift 概念,他们通常会选择不使用 C 风格的循环。在考虑集合的迭代时,for…in 循环比 for…in 语句更难实现。最后,如果 C 风格的 for 循环在 Swift 中原本就不存在,没有人会想念它们或请求将它们包含到语言中。在 Swift 3 中,C 风格的 for 循环正式从语言中移除。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0007-remove-c-style-for-loops.md 阅读更多关于该提案的信息。
从函数中移除隐式元组展开 [SE-0029]
在 Swift 的早期版本中,函数调用允许开发者以元组的形式传递参数列表,这通常被称为 元组展开。一个 元组展开 可以在某个地方定义,然后作为一个对象传递给函数,从而无需将单个参数传递给函数。让我们通过一个例子来更清楚地说明这个概念。
func fooTastic(members: [String], instruments:[String]){
// fantastic work happening here...
}
let foo = (["Jackson", "Carey", "Wonderland"], instruments:["drums", "bass", "keyboard"])
fooTastic(foo)
在我们的示例中,我们创建了 fooTastic() 函数来接受两个 String 数组参数。然后我们创建了一个封装我们想要传递给函数的参数的元组。最后,我们调用 fooTastic() 并传递我们的 foo 元组。这可以工作,但这里有一些需要考虑的缺点:
-
我们的
foo元组必须与函数中参数表示的方式相匹配:这意味着我们必须去掉成员参数标签。我们必须在我们的元组中包含乐器标签,否则当我们将元组作为参数传递时,编译器会报错。 -
只传递一个元组给函数会使我们的方法看起来有重载,这对负责维护此代码的人来说很困惑。
-
元组展开的当前实现不一致且存在错误。
考虑到所有这些因素,我们根本不需要在语言中添加这种额外的复杂性,因此这个特性已被从 Swift 3 中移除。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0029-remove-implicit-tuple-splat.md 阅读更多关于该提案的信息。
调整 inout 声明以进行类型装饰 [SE-0031]
这是对 Swift 3 的一个微小改动。inout 关键字已经被移动到冒号(:)的右边,并在函数定义中紧邻类型。关于 inout 变量在代码中的行为没有任何改变。你仍然在函数体内赋予带有 inout 装饰的参数修改值的能力。这个改动是为了让装饰名称更靠近它实际修改的类型。由于我们修改的是类型而不是标签,所以将关键字放在类型旁边更有意义。
在 Swift 2 中:
func trifecta(inout howManyTimes: Int){
howManyTimes += 2000
}
在 Swift 3 中:
func trifecta(howManyTimes: inout Int){
howManyTimes += 2000
}
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0031-adjusting-inout-declarations.md 阅读该提案。
将属性参数中的等号替换为冒号 [SE-0040]
正如我们在 [SE-0047] 中讨论的那样,属性是应用于声明或类型的特殊构造。你使用 @ 符号后跟一个名称来指定属性,并可选地使用括号中的任何属性参数:
@<attribute name>
@<attribute name>(attribute arguments)
与常规函数和参数标签不同,你使用 = 而不是冒号来分隔参数名和它的值。这与 Swift 中其他标准操作模式不一致。因此,在 Swift 3 中,属性参数将与其他 Swift 参数一样接受相同的处理,使用冒号而不是等于号。
在 Swift 2 中:
@warn_unused_result(mutable_variant="sortInPlace")
public func sort() -> [Self.Generator.Element]
在 Swift 3:
@available(*, deprecated, renamed: "NSUnderlyingErrorKey")
public static let underlyingErrorKey: ErrorUserInfoKey
注意
你可以在github.com/apple/swift-evolution/blob/master/proposals/0040-attributecolons.md阅读这个提议。
标准化函数类型参数语法以要求使用括号 [SE-0066]
在 Swift 中定义函数时,使用括号来包围它们的参数列表。其目的是使函数声明开始和结束的位置清晰。然而,Swift 2 允许你在某些条件下调用函数而不使用括号。让我们通过一个例子来使这个概念更清晰。在下面的例子中,我们使用括号和它们的等效方式(省略括号)来定义函数:
let a: (Int) -> Int
let b: (Int) -> (Int)-> Int
这也可以写成不带括号的形式:
let a1: Int -> Int
var b1: Int -> Int -> Int
当然,第二种形式由于省略了括号而略短。然而,这种权衡引入了与语言中其他函数类型定义方式不一致的代码。坦白说,省略括号并没有带来任何真正的价值或表达性组件。以这种方式构建函数类型只是语法糖,没有实质内容。在 Swift 3 中,你将无法在定义函数类型时使用这种快捷形式。
注意
你可以在github.com/apple/swift-evolution/blob/master/proposals/0066-standardize-function-type-syntax.md了解更多关于这个提议的信息。
强制默认参数的顺序 [SE-0060]
在 Swift 中调用函数时,顺序很重要,但对于包含默认参数的函数来说,有一个例外。在这种情况下,你可以只使用部分参数名称来调用这类函数。让我们看看一些如何调用包含默认参数的函数的例子。
在 Swift 2 中:
func shifty(arg1: String = "", arg2: String = "", arg3: Int = 1){}
调用 shifty() 函数的第一种方式是使用所有默认参数,这意味着在调用位置不传递任何内容。这在 Swift 2 中是有效的,并且通常是预期的行为。下面是一个仅使用具有所有默认参数值的函数的例子:
shifty()
另一种调用 shifty() 的方式是省略我们不关心的参数,只传递我们关心的参数。我们可以只传递一个参数,比如 arg2 或 arg3,而我们的函数仍然可以正常工作。在下面的例子中,我们展示了在省略一些参数的情况下调用我们的 shifty 函数:
shifty(arg2: "")
shifty(arg3: 3)
最后,我们可以使用多个参数来调用函数。以下是一个使用多个参数调用 shifty 函数的示例用法:
shifty(arg2: "", arg3: 3)
shifty("", arg3: 3)
shifty(arg2: "", arg3: 4)
允许这种行为实际上会让开发者感到有些困惑,并且与语言其他部分强制执行的严格顺序相矛盾。Swift 3 移除了这种行为,并强制您在使用默认参数时保持参数顺序。让我们看看我们的 shifty() 函数在 Swift 3 中的工作方式。
在 Swift 3 中:
func shifty(arg1: String = "arg1", arg2: String = "arg2", arg3: Int = 0){}
使用所有默认参数调用函数的方式没有变化:
shifty()
当我们用一个参数调用函数时,我们必须使用参数标签。另一个区别是我们不能随意选择调用参数的顺序。我们可以省略默认参数,但不能随意调用它们。让我们看看在 Swift 3 中我们可以如何调用我们的 shifty() 函数。
shifty(arg1: "") // valid
shifty(arg2: "") // valid
shifty(arg3: 3) //valid
shifty(arg3: 3, arg1: "test") //invalid!
如果您仔细思考一下,我敢打赌您能看出 Swift 团队做出这一改变的原因。我们牺牲了更短的语法以增强可读性。
注意
您可以在github.com/apple/swift-evolution/blob/master/proposals/0060-defaulted-parameter-order.md阅读该提案
将可选要求仅限于 Objective-C [SE-0070]
Objective-C 协议有一个功能,允许开发者标记一些行为作为可选的。虽然这对于 Objective-C 来说是合理的,但它不会作为 Swift 功能有意义。在 Swift 中,协议作者可以使用协议扩展和协议继承提供默认实现。同样,使用协议继承,作者可以将可选方法添加到单独的协议中,开发者可以选择采用该协议以实现可选行为,而不是将其作为所有协议用户的必需要求。主要收获是,在 Swift 中处理可选协议要求有更好的选择,因此,在协议上添加 Objective-C 可选功能对 Swift 来说不是必要的。
既然我们知道为什么 Swift 团队选择不将此作为 Swift 功能包含在内,让我们讨论如何在 Swift 3 中处理 Objective-C 可选参数。基本上,我们使用 @objc 属性来装饰我们想要区分的作为 Objective-C 仅需要求的协议部分。我们还为每个函数签名添加了可选关键字。在大多数情况下,您不需要在 Swift 中进行任何更改。您也不必修改您的 Objective-C 代码。迁移器会为您处理所有工作。
在 Objective-C 中:
@protocol UICollectionViewDataSource <NSObject>
@required
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section;
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
@optional
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView;
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath;
- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath;
- (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath;
@end
Swift
public protocol UICollectionViewDataSource : NSObjectProtocol {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
optional public func numberOfSections(in collectionView: UICollectionView) -> Int
optional public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
optional public func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool
optional public func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
}
我们上面的代码片段显示了 Objective-C 协议(带有可选方法)以及通过迁移器转换后的 Swift 版本。
注意
您可以在github.com/apple/swift-evolution/blob/master/proposals/0070-optional-requirements.md阅读该提案
摘要
在本章中,我们介绍了函数的创建和调用方法。我们提到了一些不太Swift的特性,这些特性在 Swift 3 中被移除了。我们还探讨了属性和属性参数,重点关注语法变化,以及语言中的新增和删除功能。在下一章中,我们将深入探讨闭包和集合。
第六章。额外,额外令人兴奋的集合和闭包变化!
在本章中,我们专注于 Swift 3 中的集合和闭包的变化。集合对于所有编程语言都很重要,因为它们允许你持有相关项的组。闭包对于 Swift 也同样重要,因为它们赋予你将功能传递到代码中不同位置的能力。有一些很好的新增功能将使使用集合变得更加有趣。我们还将探讨在 Swift 2.2 中创建闭包的一些令人困惑的副作用,以及这些副作用如何在 Swift 3 中得到修复。
集合和序列类型变化
让我们从 Swift 3 对集合和序列类型的变化开始讨论。其中一些变化很微妙,而另一些则可能需要对你自定义实现进行相当数量的重构。Swift 提供了三种主要的集合类型来存储你的值:数组、字典和集合。数组允许你按顺序列表存储值。字典为你提供无序的键值存储。最后,集合提供了一组无序的唯一值列表(即不允许重复)。
懒惰的 FlatMap 用于可选序列 [SE-0008]
数组、字典和集合在 Swift 中作为泛型类型实现。它们各自实现了新的 Collection 协议,该协议实现了 Sequence 协议。沿着从顶级类型到 Sequence 协议的路径,你将找到在这个继承链中也实现了的各种其他协议。对于我们对 flatMap 和懒 flatMap 变化的讨论,我想专注于序列。
序列包含一组值,允许用户逐个访问每个值。在 Swift 中,你可能会考虑使用 for-in 循环来遍历你的集合。Sequence 协议提供了许多操作的实施,这些操作可能是你想要在列表上使用顺序访问来执行的操作;所有这些你都可以在采用协议时在你的自定义集合中重写。其中一个操作是 flatMap 函数,它返回一个包含从序列的每个元素应用转换操作得到的扁平化或连接的值的数组。让我们考虑一下我们如何使用 flatMap 方法。
let scores = [0, 5, 6, 8, 9]
.flatMap{ [$0, $0 * 2] }
print(scores) // [0, 0, 5, 10, 6, 12, 8, 16, 9, 18]
在我们上面的示例中,我们取一个分数列表,并使用我们的转换闭包调用 flatMap。每个值都被转换成一个包含原始值和双倍值的序列。一旦转换操作完成,flatMap 方法将中间序列扁平化为单个序列。
我们还可以使用 flatMap 方法与包含可选值的 序列 来实现类似的结果。这次我们在扁平化的序列中省略了值,通过在转换中返回 nil。在下一个示例中,我们使用 flatMap 方法从我们的集合中移除所有 nil 值。
let oddSquared = [1, 2, 3, 4, 5, 10].flatMap { n in
n % 2 == 1 ? n*n : nil
}
print(oddSquared) // [1, 9, 25]
前两个示例是对小集合值的基本转换。在更复杂的情况下,你需要处理的集合可能非常大,且转换操作成本高昂。在这些参数下,你不会想在绝对需要之前执行 flatMap 操作或其他任何昂贵的操作。幸运的是,在 Swift 中我们有针对此用例的懒操作。序列包含一个 lazy 属性,它返回一个 LazySequence,可以对序列方法执行懒操作。使用我们上面的第一个示例,我们可以获得一个懒序列并调用 flatMap 来获取懒实现。只有在懒操作场景下,操作才会在代码中稍后使用 scores 时完成。为了演示懒操作,我们定义了一个使用 lazy 属性和我们的 flatMap 方法的集合。
let scores = [0, 5, 6, 8, 9]
.lazy
.flatMap{ [$0, $0 * 2] } // lazy assignment has not executed
for score in scores{
print(score)
}
lazy 操作在我们上面的测试中按预期工作。然而,当我们使用包含可选值的第二个示例中的 lazy 形式的 flatMap 时,Swift 2 中的 flatMap 会立即执行。使用 oddSquared 的懒版本应该会延迟 flatMap 操作的执行,直到我们使用该变量。然而,flatMap 方法立即执行,就像没有懒形式一样。
let oddSquared = [1, 2, 3, 4, 5, 10]
.lazy // lazy assignment but has not executed
.flatMap { n in
n % 2 == 1 ? n*n : nil
}
for odd in oddSquared{
print(odd)
}
本质上,这曾是 Swift 中的一项特性,在 Swift 3 中被修改,以使其行为类似于其他懒加载实现。
注意
你可以在以下链接中阅读提案 github.com/apple/swift-evolution/blob/master/proposals/0008-lazy-flatmap-for-optionals.md
将 first(where:) 方法添加到 Sequence [SE-0032]
使用集合的一个常见任务是找到符合某个条件的第一元素。例如,你可以要求在包含 100 分的测试分数的学生数组中找到第一个学生。你可以通过使用谓词来返回匹配条件的过滤序列,然后只需在序列中返回第一个学生。然而,直接调用一个可以返回项目的方法会更简单,而不需要两步操作。这个功能在 Swift 2 中缺失,但经过社区投票,已被添加到这次发布中。在 Swift 3 中,现在在 Sequence 协议上有一个方法来实现 first(where:).
["Jack", "Roger", "Rachel", "Joey"].first { (name) -> Bool in
name.contains("Ro")
} // =>returns Roger
这个 first(where:) 扩展是语言的一个很好的补充,因为它确保了一个简单且常见的任务在 Swift 中实际上很容易执行。
注意
你可以在以下链接中阅读提案 github.com/apple/swift-evolution/blob/master/proposals/0032-sequencetype-find.md
添加序列(first: next:)和序列(state: next:)[SE-0094]
Swift 3 引入了两个新的全局函数,用于操作序列:sequence(first:next:)
和 (state:next:)。
让我们看看下面的完整定义:
public func sequence<T>(first: T, next: @escaping (T) -> T?) -> UnfoldSequence<T, (T?, Bool)>
public func sequence<T, State>(state: State, next: @escaping (inout State) -> T?) -> UnfoldSequence<T, State>
public struct UnfoldSequence<Element, State> : Sequence, IteratorProtocol
这两个函数被添加作为对 Swift 3 中移除的 C 风格 for 循环的替代,并作为对 Swift 2 中已经存在的全局 reduce 函数的补充。这些添加有趣的地方在于,每个函数都有生成和处理无限大小序列的能力。让我们检查第一个序列函数,以更好地理解它是如何工作的:
/// - Parameter first: The first element to be returned from the sequence.
/// - Parameter next: A closure that accepts the previous sequence element and
/// returns the next element.
/// - Returns: A sequence that starts with `first` and continues with every
/// value returned by passing the previous element to `next`.
///
func sequence<T>(first: T, next: @escaping (T) -> T?) -> UnfoldSequence<T, (T?, Bool)>
第一个序列方法返回一个由重复调用 next 参数创建的序列,该参数包含一个将按需执行的闭包。返回值是一个 UnfoldSequence,它包含传递给序列方法的 first 参数以及将 next 闭包应用于前一个值的结果。如果 next 最终返回 nil,则序列是有限的;如果 next 永远不返回 nil,则序列是无限的。在下面的示例中,我们使用 sequence(first: next:) 的尾随闭包形式创建和分配我们的序列。
let mysequence = sequence(first: 1.1) { $0 < 2 ? $0 + 0.1 : nil }
for x in mysequence{
print (x)
} // 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0
我们的有限序列将从 1.1 开始,并重复调用 next,直到我们的下一个结果大于 2,此时 next 将返回 nil。我们可以很容易地将这个转换为无限序列,通过移除我们之前值不能大于 2 的条件。第二个序列函数维护一个可变状态,该状态传递给所有 next 的懒调用以创建和返回一个序列。让我们考虑一个使用第二个方法的示例:
/// - Parameter state: The initial state that will be passed to the closure.
/// - Parameter next: A closure that accepts an `inout` state and returns the
/// next element of the sequence.
/// - Returns: A sequence that yields each successive value from `next`.
///
public func sequence<T, State>(state: State, next: (inout State) -> T?) -> UnfoldSequence<T, State>
这个版本的序列函数使用一个传入的闭包,允许你在每次调用 next 时更新可变状态。正如我们第一个序列函数的情况一样,一个有限序列在 next 返回 nil 时结束。你可以通过在调用 next 时永远不返回 nil 来将有限序列转换为无限序列。
让我们创建一个示例,说明这个版本的序列方法可能如何使用。遍历具有嵌套视图或任何嵌套类型列表的视图层次结构是一个使用第二个版本序列函数的完美任务。让我们创建一个具有两个属性的 Item 类。一个名称属性和一个可选的父属性,以跟踪项的所有者。最终所有者将没有父项,这意味着父属性将是 nil。
让我们定义一个 Item 类来在我们的示例中使用,以展示这些新概念的使用。
class Item{
var parent: Item?
var name: String = ""
}
接下来,我们创建一个父项和两个嵌套的子项。child1 的父项将是父项本身,而 child2 的父项将是 child1。
let parent = Item()
parent.name = "parent"
let child1 = Item()
child1.name = "child1"
child1.parent = parent
let child2 = Item()
child2.name = "child2"
child2.parent = child1
现在是时候创建我们的序列了。序列需要我们提供两个参数:一个 state 参数和一个 next 闭包。我将状态设为一个 Item,初始值为 child2。这样做的原因是我希望从树的最低层叶子节点开始遍历到最终父节点。我们的示例只有三个层级,但在更复杂的示例中可能会有很多层级。至于 *next* 参数,我使用了一个期望可变 Item 作为其状态的闭包表达式。我的闭包还将返回一个可选的 Item。在我们的闭包体中,我使用当前的 Item(可变状态参数)来访问 Item 的父节点。我更新状态并返回父节点。
let itemSeq = sequence(state: child2, next: {
(next: inout Item)->Item? in
let parent = next.parent
next = parent != nil ? parent! : next
return parent
})
for item in itemSeq{
print("name: \(item.name)")
}
这里有一些需要注意的问题,我想指出,以便你更好地理解如何为这个序列方法定义自己的下一个闭包。
-
状态参数实际上可以是任何你想要的内容。它是为了帮助你确定序列的下一个元素,并为你提供有关你在序列中位置的相关信息。改进我们上述示例的一个想法是跟踪我们有多少层嵌套。我们可以将状态设为一个元组,其中包含一个表示嵌套层级的整数计数器以及当前项。
-
下一个闭包需要扩展以显示签名。由于 Swift 在闭包方面的表达性和简洁性,你可能会倾向于将
*next*闭包转换为更短的形式并省略签名。除非你的*next*闭包非常简单,并且你确信编译器能够推断出你的类型,否则不要这样做。使用短闭包格式会使你的代码更难维护,而且当其他人继承它时,你也不会因为风格而得到额外的分数。 -
不要忘记在闭包体中更新你的状态参数。这真的是你了解自己在序列中位置的最佳机会。忘记更新状态可能会在你尝试遍历序列时导致你得到意外的结果。
-
提前明确决定你是在创建有限序列还是无限序列。这一决定在你从下一个闭包返回时是显而易见的。当你期望无限序列时,它并不是坏事,然而,如果你使用
for…in循环遍历这个序列,你可能会得到比你预期的更多,前提是你假设这个循环会结束。
集合和索引的新模型 [SE-0065]
Swift 3 引入了一个新的集合模型,将索引遍历的责任从索引转移到集合本身。为了使这一变化适用于集合,Swift 团队引入了四个方面的变化:
-
集合的索引属性可以是实现 Comparable 协议的任何类型
-
Swift 移除了区间和范围之间的任何区别;只留下范围
-
私有索引遍历方法现在是公开的
-
Range 的变化使得闭包范围可以无错误地工作
注意
您可以在以下链接中阅读提案:Swift 3 迁移现有类型时的问题
介绍 Collection 协议
在 Swift 3 中,Foundation 集合类型如 Arrays、Dictionaries 和 Sets 是实现了新创建的 Collection 协议的泛型类型。这一变化是为了支持集合的遍历。如果您想创建自己的自定义集合,您需要了解 Collection 协议以及它在 Collection 协议层次结构中的位置。我们将介绍新集合模型的重要方面,以帮助您过渡并准备好创建自己的自定义集合类型。
Collection 协议建立在 Sequence 协议之上,提供在集合中使用时访问特定元素的方法。例如,您可以使用集合的index(_:offsetBy:)方法返回一个距离参考索引指定距离的索引。
let numbers = [10, 20, 30, 40, 50, 60]
let twoAheadIndex = numbers.index(numbers.startIndex, offsetBy: 2)
print(numbers[twoAheadIndex]) //=> 30
在我们上面的例子中,我们创建了twoAheadIndex常量来保存我们的数字集合中距离起始索引两个位置的索引。我们只需使用这个索引通过下标符号从我们的集合中检索值。
遵守 Collection 协议
如果您想创建自己的自定义集合,您需要通过声明startIndex和endIndex属性、一个支持访问您元素的索引和index(after:)方法来采用 Collection 协议,以方便遍历您的集合索引。
注意
当我们将现有类型迁移到 Swift 3 时,迁移器在转换自定义集合时存在一些已知问题。您很可能会通过检查导入的类型是否符合 Collection 协议来轻松解决编译器问题。
此外,您还需要遵守 Sequence 和 IndexableBase 协议,因为 Collection 协议采用了这两个协议。
public protocol Collection : Indexable, Sequence { ... }
一个简单的自定义集合可能看起来像以下示例。请注意,我已经将我的Index类型定义为Int。在 Swift 3 中,您可以将索引定义为任何实现了 Comparable 协议的类型:
struct MyCollection<T>: Collection{
typealias Index = Int
var startIndex: Index
var endIndex: Index
var _collection: [T]
subscript(position: Index) -> T{
return _collection[position]
}
func index(after i: Index) -> Index {
return i + 1
}
init(){
startIndex = 0
endIndex = 0
_collection = []
}
mutating func add(item: T){
_collection.append(item)
}
}
var myCollection: MyCollection<String> = MyCollection()
myCollection.add(item: "Harry")
myCollection.add(item: "William")
myCollection[0]
Collection 协议为其大多数方法、Sequence 协议的方法和 IndexableBase 协议的方法提供了默认实现。这意味着您只需要提供一些自己的东西。然而,您可以为您的集合实现尽可能多的其他方法。
新的 Range 和相关索引类型
Swift 2 的 Range<T>、ClosedInterval<T>和OpenInterval<T>在 Swift 3 中将被弃用。这些类型将被四种新类型所取代。其中两种新的范围类型支持具有实现Comparable协议边界的通用范围:Range<T>和ClosedRange<T>。其他两种范围类型符合RandomAccessCollection。这些类型支持具有实现Strideable协议边界的范围。
最后,由于范围现在表示为两个索引的配对,它们不再可迭代。为了保持旧代码的兼容性,Swift 团队引入了一个关联的Indices类型,它是可迭代的。此外,创建了三个泛型类型,为每种集合遍历类别提供默认的Indices类型。这些泛型是DefaultIndices<C>、DefaultBidirectionalIndices<C>和DefaultRandomAccessIndices<C>;每个都存储其底层集合以进行遍历。
快速总结
我在 Swift 3 的集合类型仅用几页纸就涵盖了大量内容。以下是关于集合和索引的要点,请记住。
-
收集类型(内置和自定义)实现了收集协议。
-
遍历集合已移动到 Collection - 索引不再具有该功能。
-
您可以通过采用收集协议来创建自己的集合。您需要实现:
-
startIndex和endIndex属性, -
下标方法以支持访问您的元素
-
并且提供了
index(after:)方法来方便遍历集合的索引。
-
Swift 3 的闭包变化
Swift 中的闭包是一段代码块,可以用作函数调用的参数或分配给变量,以便在稍后执行其功能。闭包是 Swift 的核心特性,对于新接触 Swift 的开发者来说很熟悉,因为它们可能会让他们想起其他编程语言中的 lambda 函数。对于 Swift 3,这里将突出两个显著的变化。第一个变化是关于 inout 捕获。第二个变化是将非逃逸闭包作为默认值。
限制@noescape 闭包的 inout 捕获 [SE-0035]
在 Swift 2 中,在逃逸闭包中捕获inout参数对开发者来说难以理解。一些闭包被分配给变量,然后作为参数传递给函数。如果包含闭包参数的函数从其调用中返回,并且传递的闭包稍后被使用,那么您有一个逃逸闭包。另一方面,如果闭包仅用于传递给它的函数中,并且以后不再使用,那么您有一个非逃逸闭包。这里的区别很重要,因为inout参数的修改性质。
当我们将inout参数传递给闭包时,可能会因为inout参数的存储方式而无法得到预期的结果。inout参数被捕获为阴影副本,并且只有在值发生变化时才会写回原始值。这大多数时候都工作得很好。然而,当闭包在稍后时间被调用(即,当它逃逸时),我们不会得到预期的结果。我们的阴影副本无法写回原始值。让我们看一个例子。
var seed = 10
let simpleAdderClosure = { (inout seed: Int)->Int in
seed += 1
return seed * 10
}
var result = simpleAdderClosure(&seed) //=> seed = 11; result = 110
print(seed) // => 11
在上面的例子中,我们得到了预期的结果。我们创建了一个闭包来增加传入的inout参数,然后返回乘以 10 的新参数。当我们调用闭包后检查seed的值,我们看到值已经增加到11。
在我们的第二个例子中,我们修改我们的闭包,使其返回一个函数而不是仅仅一个Int值。我们将我们的逻辑移动到我们定义的返回值闭包中。
let modifiedClosure = { (inout seed: Int)-> (Int)->Int in
return { (Int)-> Int in
seed += 1
return seed * 10
}
}
print(seed) //=> 11
var resultFn = modifiedClosure(&seed)
var result = resultFn(1)
print(seed) // => 11
这次当我们用seed值执行modifiedClosure时,我们得到一个函数作为结果。执行这个中间函数后,我们检查seed的值,发现值没有改变;尽管我们仍在增加seed值。
当使用inout参数时,这两个微小的语法差异会产生不同的结果。如果没有了解阴影副本的工作原理,就很难理解结果之间的差异。最终,这又是一个你因为允许这个特性保留在语言中而得到更多伤害而不是好处的例子。
注意
你可以在以下链接中阅读提案 github.com/apple/swift-evolution/blob/master/proposals/0035-limit-inout-capture.md
解决方案
在 Swift 3 中,编译器现在限制inout参数在闭包中的使用为非逃逸(@noescape)。如果编译器检测到你的闭包包含inout参数时逃逸,你会收到一个错误。
将非逃逸闭包设置为默认值 [SE-0103]
在 Swift 的早期版本中,函数参数的类型为闭包时的默认行为是允许逃逸。这很有道理,因为大多数导入到 Swift 中的 Objective-C 块(Swift 中的闭包)都是逃逸的。Objective-C 中的代理模式,作为块实现,由逃逸的代理块组成。那么 Swift 团队为什么要将默认值改为非逃逸呢?让我们通过 Swift 2.2 和 Swift 3 的例子来更好地理解这个变化为什么合理。
在 Swift 2.2 中:
var callbacks:[String : ()->String] = [:]
func myEscapingFunction(name:String, callback:()->String){
callbacks[name] = callback
}
myEscapingFunction("cb1", callback: {"just another cb"})
for cb in callbacks{
print("name: \(cb.0) value: \(cb.1())")
}
在 Swift 3 中:
var callbacks:[String : ()->String] = [:]
func myEscapingFunction(name:String, callback: @escaping ()->String){
callbacks[name] = callback
}
myEscapingFunction(name:"cb1", callback: {"just another cb"})
for cb in callbacks{
print("name: \(cb.0) value: \(cb.1())")
}
Swift 团队认为,您可以使用非逃逸闭包编写更好的函数式算法。另一个支持因素是,当使用闭包的inout参数时,要求使用非逃逸闭包[SE-0035]。综合考虑,这个变化可能对您的代码影响很小。当编译器检测到您正在尝试创建一个逃逸闭包时,您将收到一个警告,提示您可能正在创建一个逃逸闭包。您可以通过添加@escaping或通过伴随错误的fixit轻松纠正错误。
注意
您可以在以下链接中阅读提案github.com/apple/swift-evolution/blob/master/proposals/0103-make-noescape-default.md
摘要
在本章中,我们介绍了集合和闭包的变化。我们学习了新的集合协议,它是新集合模型的基础,以及如何在我们的自定义集合中采用该协议。新的集合模型通过将集合遍历从索引移动到集合本身,实现了重大变化。为了支持 Objective-C 的交互性并提供一种机制,使用集合本身来遍历集合项,新的集合模型变化是必要的。至于闭包,我们还探讨了语言转向非逃逸闭包作为默认值的动机。我们还学习了如何在 Swift 3 中正确使用inout参数与闭包。在下一章中,我们将介绍更多协议和协议扩展中的类型变化和类型别名。
第七章:抓紧你的椅子;高级类型变更即将到来!
Swift 是一种伟大的语言,并且随着每个版本的发布而变得越来越好。到目前为止,我们已经涵盖了你在日常项目中可能使用的大多数功能。我们将介绍一些你可能不会经常使用的语言改进。本章重点介绍UnsafePointer类型、typealiases和浮点运算。
Unmanaged 和 UnsafePointer 的变更
准备好,因为我们即将浏览一些你可能见得不多、名字听起来让人想回避的类型,比如“我宁愿不碰那些”这类对你们中的一些人来说可能会感到不适的类型。就 Swift 中类型的命名惯例而言,对于普通开发者来说似乎既友好又合理。然而,有一组类型甚至没有列在 Swift 编程语言文档的主要部分中。这些是语言的“黑羊”类型。它们的名称如Unmanaged、UnsafeMutableRawPointer和UnsafeBufferPointer等。这些类型使用起来感觉不安全。也许,这些名称本身就是一个大大的提示,表明作为开发者的你,在使用这些类型时需要采取一些预防措施。如果你在 Swift 中开发的时间足够长,你最终会遇到这些类型之一。我们不妨先了解一下 Swift 3 中这些类型的变更,这样当你需要使用新特性时,你就能掌握最新的知识。
将 Unmanaged 转换为使用 UnsafePointer [SE-0017]
Unmanaged是 Swift 中的一种类型,允许你与一个未管理的对象引用一起工作,这意味着你负责对象的内存,并保持其存活。UnsafePointer是一种表示指针类型数据的原始指针的类型。你完全负责使用此类型管理内存。这两种类型在处理 C API 时都很有用。接受void *或const void *等类型的 C 函数非常常见,但在 Swift 中可能会出现一些问题。
当 C API 传递void *或const void *类型(或其他不易转换为 Foundation 类型的类型)到 Swift 时,该类型会被转换为UnsafePointer。这是我们第一步,但不是我们最终的目的地,因为我们想要的是一个可以在 Swift 中高效使用的类型。我们最终想要的是一个Unmanaged类型,因为这个类型为我们提供的对象提供了一个类型安全的包装,即使它不参与自动引用计数(ARC)。使用Unmanaged类型,开发者可以手动做出内存决策。在 Swift 2 中,没有直接的转换允许你从UnsafePointer转换为Unmanaged类型。你必须先转换为一个桥接类型,然后再转换为你想要的类型,即UnsafePointer | COpaquePointer | Unmanaged。
你使用以下方法之一在Unmanaged类型上完成转换:
static func fromOpaque(value: COpaquePointer) -> Unmanaged<Instance>
func toOpaque() -> COpaquePointer
在 Swift 2 中:
let str0: CFString = "Test string" as CFString
let bits: Unmanaged<CFString> = Unmanaged.passRetained(str0)
let oPtr: COpaquePointer = bits.toOpaque()
let ptr: UnsafePointer<CFString> = UnsafePointer(oPtr)
let oPtr2 = COpaquePointer(ptr)
let str1: Unmanaged<CFString> = Unmanaged.fromOpaque(oPtr2)
str1.takeRetainedValue()
在 Swift 3 中,我们现在可以直接在 Unmanaged 和 UnsafePointer 之间进行转换。fromOpaque 和 toOpaque 方法用 UnsafeRawPointer 和 UnsafeMutableRawPointer 类型替换了 COpaquePointer。我们通过消除中间人简化了我们的代码。
static func fromOpaque(_ value: UnsafeRawPointer) -> Unmanaged<Instance>
func toOpaque() -> UnsafeMutableRawPointer
在 Swift 3 中:
let str0: CFString = "Test string" as CFString
let bits = Unmanaged.passUnretained(str0)
let ptr = bits.toOpaque()
let str1: Unmanaged<CFString> = Unmanaged.fromOpaque(ptr)
str1.takeRetainedValue()
注意
使用可选类型明确 UnsafePointer [SE-0055]
与 Objective-C 不同,在 Objective-C 中你可以将指针标记为 可空 或 非空,Swift 没有确定指针是否为空的方法。因此,当你获得一个 UnsafePointer<T> 的引用时,你可能会持有指向空的指针。这是一个问题,因为 UnsafePointer(以及类似类型)本质上是在引用 C 指针。如果开发者的代码没有期望或考虑到空值,那么程序可能会崩溃。这在 Swift 中尤其令人担忧,因为使用 UnsafePointer 可以执行的所有非平凡操作都依赖于一个有效的、非空的底层指针。
幸运的是,在 Swift 中,对于内置类型、类和结构体,我们不需要处理这个问题,因为我们有可选类型。众所周知,可选类型 允许我们处理类型可能包含或不包含值的情况。Swift 3 新增了将 可选类型 应用于 UnsafePointer 类型的功能。当你确信你的指针不能为空时,你使用常规形式:UnsafePointer<T>。当你想要表示一个 可空 版本时,你使用可选语法(UnsafePointer<T>?)。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0055-optional-unsafe-pointers.md 阅读提案。
添加 UnsafeRawPointer [SE-0107]
Swift 添加了 UnsafePointer 类型,以便与 C API 进行交互并简化高性能数据结构的构建(例如,低级图形编程或基于数学的建模)。在这方面,UnsafePointer 是 Swift 中的一个重要补充。不幸的是,UnsafePointer 的实现允许开发者绕过为确保类型安全的内存访问而设置的防护措施。可以使用 UnsafePointer 类型来违反编译器逻辑。违反通常会导致编译错误。然而,编译器为 UnsafePointer 类型内置了一个异常,允许编译过程继续进行。在许多情况下,运行使用类型化内存访问来引用不同类型内存位置的程序会导致程序崩溃。Swift 3 引入了 UnsafeRawPointer 来处理无类型内存。让我们用一个例子来说明类型内存访问的滥用。
在 Swift 2 中:
let msg: CFString = "just a few characters" as CFString
let unmgd: Unmanaged<CFString> = Unmanaged.passRetained(msg)
let ptr: UnsafeMutablePointer<CFString> = UnsafeMutablePointer(unmgd.toOpaque())
// reassign pointer address with new value
ptr[0] = "testing..." as CFString
// use typed access of Int to access CFSTring memory location
let u = UnsafePointer<Int>(ptr)[0]
在我们的类型内存访问示例中,我们创建了一个UnsafeMutablePointer<CFString>来引用一个类型化的内存块。然后,我们将第一个块中的值更改为一个新的CFString,测试我们的指针。到目前为止一切正常。接下来,我们从一个现有的内存位置创建了一个UnsafePointer<Int>。请注意,我们将类型绑定到了Int,而不是原始指针中使用的CFString。如果我们使用这个Int指针,我们的程序可能会崩溃。即使编译器认为这段代码可疑,它也允许编译,因为我们正在使用UnsafePointer类型,这些类型在编译器的异常列表中对于某些类型的操作是允许的。为了纠正我们示例中的问题,我们需要使用一个不需要知道类型的类型来访问内存。UnsafeRawPointer和UnsafeMutableRawPointer在 Swift 3 中被引入,就是为了做这件事。
要点:
-
原始类型允许对内存进行无类型访问;类型化版本使用它们的类型访问内存
-
原始访问基本上允许 C 类型的
memcopy操作,而类型化访问遵循类型别名规则 -
当类型不明确时(例如,
const void *或void *),C 类型现在被导入为UnsafeMutableRawPointer和UnsafeRawPointer,而当它们的类型可以确定时(例如,const T*),则导入为UnsafePointer<T>和UnsafeMutablePointer<T>。
注意
您可以在github.com/apple/swift-evolution/blob/master/proposals/0107-unsaferawpointer.md阅读另一份提案。
类型别名和协议更改
类型别名是命名类型,用于在 Swift 中替换现有类型。一旦定义,您就可以在代码的任何地方使用这些类型。Swift 3 现在支持基于泛型的类型别名。此外,现在也支持协议和协议扩展的类型别名。谈到协议,Swift 3 对协议的使用进行了更改,这使得事情变得更简单,并为该功能的预期未来更改铺平了道路。让我们更详细地添加新更改,并通过一些示例来探讨。
泛型类型别名 [SE-0048]
泛型类型别名是 Swift 3 的新增功能。提醒一下,类型别名是在语言中为现有类型声明一个命名别名的途径。创建您的命名别名后,您可以在代码中使用这些别名,就像使用任何其他类型一样。泛型类型别名允许您添加可以在定义泛型类型时使用的类型参数。让我们考虑几个示例,以展示在 Swift 3 中创建类型别名的新可能性:
typealias ScoreBag<T> = [T]
typealias TriplePointTuple<T> = (T,T,T)
typealias AddPlotter<X:Hashable,Y> = Dictionary<X, Y>
typealias UndoItem<T> = [Date:T]
注意
您可以在github.com/apple/swift-evolution/blob/master/proposals/0048-generic-typealias.md阅读另一份提案。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0095-any-as-existential.md 阅读这个提议。
在 Swift 的早期版本中,当你定义一个遵循多个协议的类型时,你需要使用 protocol<...> 语法。在 Swift 3 中,你现在在每个你类型采用的协议之间使用 &。这仅仅是语法糖,但将成为在 Swift 未来版本中定义更多泛型类型的基础。
在 Swift 2 中:
protocol Driving {}
protocol Parking {}
protocol Braking {}
struct Car: Driving, Parking, Braking {}
let zoomzoom: protocol<Driving, Braking, Parking> = Car()
在 Swift 3 中:
let zoomzoom: Driving & Braking & Parking = Car()
为了更好地传达由多个协议构建的复合类型的意图,Swift 团队现在更倾向于使用 & 而不是逗号来定义类型上的多个协议。
协议和协议扩展中的类型别名 [SE-0092]
Swift 2.2 引入了 associatedtype 关键字来处理协议中的关联类型。这个变化消除了使用 typealias 关键字时的混淆,因为它现在只负责定义类型。添加 associatedtype 关键字的另一个好处是,它允许我们在协议和协议扩展中使用基于关联类型的类型别名。查看标准库中的 Sequence 协议,我们可以看到 Iterator 被定义为继承自 IteratorProtocol 的关联类型。使用 Swift 3,我现在可以添加一个名为 Element 的类型别名,它间接引用了 IteratorProtocol 上的关联类型,从而使我的语法更简洁。此外,我还可以在我的协议扩展中使用我创建的任何类型别名。
在 Swift 3 中:
public protocol Sequence {
associatedtype Iterator : IteratorProtocol
typealias Element = Iterator.Element
public func makeIterator() -> Iterator
func map<T>(_ transform: (Element) throws -> T)
rethrows -> [T]
}
在先前的 Sequence 协议中,我可以在 map() 函数中使用我的类型别名 Element。在 Swift 的早期版本中,你将不得不使用 Iterator.Element。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0092-typealiases-in-protocols.md 阅读这个提议。
浮点类型变更
浮点类型用于存储分数数字。标准库中的主要浮点类型是 Float 和 Double。Swift 团队创建了一个 FloatingPoint 协议来存储常见的数学操作,这使得你更容易创建支持所有浮点类型的函数。在本节中,我们将介绍对 FloatingPoint 协议和舍入函数的扩展。
增强的浮点协议 [SE-0067]
当前的 FloatingPoint 协议并没有提供一套完整的特性来真正符合 IEEE 754 en.wikipedia.org/wiki/IEEE_floating_point#CITEREFIEEE_7542008 类型。对 FloatingPoint 协议的更改旨在扩展大多数预期应包含的操作的范围。Swift 还添加了一个名为 BinaryFloatingPoint 的第二个协议(符合 FloatingPoint),它对于泛型编程将非常有用。
现在的 FloatingPoint 协议包含了 IEEE 754 的基本操作的大部分。BinaryFloatingPoint 协议额外符合 FloatLiteralConvertible。你可以使用 FloatingPoint 协议执行正常的算术和比较操作,以及使用 BinaryFloatingPoint 协议执行更适合使用具有固定基数 2 的浮点类型的更复杂操作。
在 FloatingPoint 和 BinaryFloatingPoint 协议上定义了许多新的操作,我将把它们留给你作为未来的练习。
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0067-floating-point-protocols.md 阅读该提案。
FloatingPoint 协议上的新舍入函数 [SE-0113]
Swift 标准库没有内置方法来实现舍入函数,如 floor() 或 ceil()。当你需要这些方法时,你必须导入 Darwin 或 Glibc 来访问使用 C 实现的版本。在 Swift 3 中,Swift 团队为 FloatingPoint 协议添加了新的舍入方法。舍入以及/或将浮点类型转换为整数的操作是协议应该处理的操作。
FloatingPoint 协议的更改包括添加了 FloatingPointRoundingRule 枚举和两个舍入方法,round() 和 rounded():
public func rounded(_ rule: FloatingPointRoundingRule) -> Self
public mutating func round(_ rule: FloatingPointRoundingRule)
public mutating func round()
public enum FloatingPointRoundingRule {
case toNearestOrAwayFromZero
case toNearestOrEven
case up
case down
case towardZero
case awayFromZero
}
使用新的本地化实现的舍入方法,你将能够复制使用 ceil 和 floor 等方法得到的行为:
(10.5).rounded(.down) // -> 10.0
(5.2).rounded(.up) // -> 6.0
注意
你可以在 github.com/apple/swift-evolution/blob/master/proposals/0113-rounding-functions-on-floatingpoint.md 阅读该提案。
摘要
我们介绍了在使用 C APIs 时使用的 Unmanaged、UnsafePointer 和类似类型的更改。你了解了在使用这些类型时 Swift 2 的编译器怪癖以及 Swift 3 中这些改进。接下来,我们讨论了类型别名更改、它们与协议的使用以及协议扩展。最后,我们探讨了 FloatingPoint 协议的更改。在下一章中,我们将介绍 Foundation 框架的新增功能。
第八章。哦,太好了!看看在 Foundation 框架中的新内容
去年,苹果公司做了一场关于在代码中使用值类型和面向协议的设计模式的益处的精彩演讲。今年,随着 Swift 3 的发布,苹果团队将值类型添加到了 Foundation 框架中。我们许多喜爱且广泛使用的类现在都有了值类型的对应版本。在本章中,我们将介绍如何使用 Foundation 值类型,并让你思考为什么值类型从一开始就没有包含在 Swift 中!
新增于 Foundation 且由 Swift 3 使之成为可能的新测量和单位 API developer.apple.com/reference/foundation/nsmeasurement 解决了 Objective-C 和 Swift 开发者长期以来的痛点。苹果公司为我们的问题创造了一个极其周到的解决方案。我们将通过几个示例来强化这些概念,以便你在未来更好地应对测量挑战。
可变性和 Foundation 值类型
Swift 语言的一个关键概念是赋予开发者控制其对象可变性的能力。我们使用 let 来使一个值成为常量,使用 var 来使一个值成为变量。然而,某些类型,当从 Objective-C 导入时,并不提供易于使用的可变性功能。Swift 3 通过添加一组新的 Foundation 值类型来包裹引用类型,从而为开发者提供可变选项,旨在改变这一现状。实际上,这并不是什么新鲜事,因为 Foundation 已经在 Objective-C 和 Swift 中使用了许多值类型。Foundation 已经存在的类型包括原始类型、枚举、选项集和 C 结构类型,这些在 Swift 和 Objective-C 的早期版本中已经是值类型。
为了使从引用类型到值类型的转换成为可能,Swift 使用了 copy-on-write 技术来处理新值类型,其底层数据包含的不仅仅是简单数据。在 copy-on-write 中,值类型代表一个指向共享数据的指针,并且只有在有请求通过其引用之一更改数据时,才会复制数据。以下表格列出了添加到 Foundation 框架中的新值类型。
表 1:新的值类型:
| 值类型 | 类 Type |
|---|---|
AffineTransform |
NSAffineTransform |
CharacterSet |
NSCharacterSet, NSMutableCharacterSet |
Data |
NSData |
Date |
NSDate |
DateComponents |
NSDateComponents |
DateInterval (新) |
|
Decimal |
NSDecimal |
IndexPath |
NSIndexPath |
IndexSet |
NSIndexSet, NSMutableIndexSet |
Measurement (新) |
|
Notification |
NSNotification |
PersonNameComponents |
NSPersonNameComponents |
URL |
NSURL |
URLComponents |
NSURLComponents |
URLRequest |
NSURLRequest, NSMutableURLRequest |
URLQueryItem |
NSURLQueryItem |
UUID |
NSUUID |
这些新的值类型工作方式与它们的对应类型相同,额外的好处是你可以以更透明的方式控制可变性。一些新的值类型被实现为结构体类型,并将公开一个或多个可变方法。好消息是新的 Foundation 值类型对现有的 Objective-C 代码没有影响。然而,它将对 Swift 侧产生影响。任何现有的 Swift 代码迁移到 Swift 3,并使用引用类型,将转向调用新的值类型。虽然迁移器会做很多繁重的工作,但你仍有机会进一步优化你的代码。这是因为迁移器采用了一种尽可能少做更改的策略。例如,迁移器不会尝试更改任何 Swift 子类以用于引用类型。
值类型与引用类型
作为复习,让我们快速了解一下值类型和引用类型之间的区别。值类型持有值,在赋值或作为参数传递给方法时复制内容。
let avg1: Double = 98.1
var avg2 = avg1
avg2 += 1.2 // -> 99.3
在前面的例子中,avg2 复制了 avg1 的值,并且可以自由更改其值而不影响 avg1 的值。另一方面,引用类型默认共享内容。更改其中一个变量将改变每个引用共享的底层值。
let dateComponents1 = NSDateComponents()
dateComponents1.setValue(10, forComponent: .Day)
dateComponents1.day // => 10
var dateComponents2 = dateComponents1
dateComponents2.setValue(2, forComponent: .Day)
dateComponents1.day // => 2
dateComponents2.day // => 2
在这个例子中,我们使用引用类型 NSDateComponents 创建 dateComponents1 并将日历单位设置为 10。然后我们创建另一个变量并将第一个值赋给它。由于两个变量共享其底层数据,修改任何一个变量都会改变两个值。
嵌套枚举
Foundation 框架利用了 Swift 3 的新嵌套枚举功能。这个功能允许迁移器将相关的 Objective-C 枚举导入到 Swift 中,作为一个公共类型。例如,NSDateFormatterStyle 和 NSDateFormatterBehavior 枚举被导入到 Swift 的 DateFormatter 类中作为嵌套枚举。让我们看看如何在 Swift 3 中实现嵌套枚举,从 Objective-C 枚举开始。
在 Objective-C 中:
typedef NS_ENUM(NSUInteger, NSDateFormatterStyle),
{
NSDateFormatterNoStyle = kCFDateFormatterNoStyle,
NSDateFormatterShortStyle = kCFDateFormatterShortStyle,
NSDateFormatterMediumStyle = kCFDateFormatterMediumStyle,
NSDateFormatterLongStyle = kCFDateFormatterLongStyle,
NSDateFormatterFullStyle = kCFDateFormatterFullStyle
};
typedef NS_ENUM(NSUInteger, NSDateFormatterBehavior){
NSDateFormatterBehaviorDefault = 0,
NSDateFormatterBehavior10_0 = 1_000,
NSDateFormatterBehavior10_4 = 1_040,
};
在 Swift 2.2-更好但尚未完成:
enum NSDateFormatterStyle : UInt {
case NoStyle
case ShortStyle
case MediumStyle
case LongStyle
case FullStyle
}
enum NSDateFormatterBehavior : UInt {
case BehaviorDefault
case Behavior10_0
case Behavior10_4
}
在 Swift 3 中:
DateFormatter {
public enum Style : UInt {
case none
case short
case medium
case long
case full
}
public enum Behavior : UInt {
case `default`
case behavior10_4
}
}
在 Swift 3 中使用嵌套枚举比 Swift 2.2 有所改进,因为我们不再需要担心基于 C 的枚举污染全局命名空间。
强类型字符串枚举
Foundation 框架有很多基于字符串的常量。例如,UIKit 使用 NSNotifications 发布 iOS 应用生命周期的通知。
NSString *const UIApplicationDidEnterBackgroundNotification
NSString *const UIApplicationWillEnterForegroundNotification NSString *const UIApplicationDidFinishLaunchingNotification;
对于 Foundation 来说,Objective-C 现在有了使用强类型字符串枚举的能力。这个新特性允许 Foundation 团队更新 Objective-C 中的枚举。我们之前列出的通知现在可以使用 NSNotificationName 类型,并将先前的常量转换为:
在 Objective-C 中:
typedef NSString *NSNotificationName NS_EXTENSIBLE_STRING_ENUM;
NSNotificationName const UIApplicationDidEnterBackgroundNotification
NSNotificationName const UIApplicationWillEnterForegroundNotification NSNotificationName const UIApplicationDidFinishLaunchingNotification;
在 Swift 中,NSNotificationName 类型作为 NSNotification.Name 的扩展导入。
extension NSNotification.Name {
public static let UIApplicationDidEnterBackground: NSNotification.Name
public static let UIApplicationWillEnterForeground: NSNotification.Name
public static let UIApplicationDidFinishLaunching: NSNotification.Name
}
类属性
由于历史原因,Foundation 框架中包含许多属性状态与类型紧密关联的案例。因此,你会在 Objective-C 类型上找到许多类属性。对于 Swift 3 的发布,我们得到了对 Objective-C 的一个调整,允许我们使用新的语法来创建类属性。这些 Objective-C 中的类属性也将作为类属性导入 Swift 中。
在 Objective-C(旧方法):
@interface NSCharacterSet
+ (NSCharacterSet *)controlCharacterSet;
+ (NSCharacterSet *)whitespaceCharacterSet;
+ (NSCharacterSet *)whitespaceAndNewlineCharacterSet;
@end
在 Objective-C(新方法):
@interface NSCharacterSet
@property(class, readonly, strong) controlCharacterSet;
@property(class, readonly, strong) whitespaceCharacterSet;
@property(class, readonly, strong) whitespaceAndNewlineCharacterSet;
@end
在 Swift 2.2 中:
class NSCharacterSet{
class func controlCharacters() -> NSCharacterSet
class func whitespaces() -> NSCharacterSet
class func whitespacesAndNewlines() -> NSCharacterSet
}
在 Swift 3 中:
class NSCharacterSet{
class var controlCharacters: CharacterSet { get }
class var whitespaces: CharacterSet { get }
class var whitespacesAndNewlines: CharacterSet { get }
}
值类型的类型安全访问
Swift 3 的新特性,你可以使用 Foundation 值类型来在编译时进行类型安全检查,以消除使用基于引用的 Foundation 类型时在运行时无法发现的许多错误。让我们通过一个示例来演示 Swift 2.2 中的运行时检查。
在 Swift 2.2 中:
if let filePath = NSBundle.mainBundle().pathForResource("testFile", ofType: "txt"){
let fileURL = NSURL.fileURLWithPath(filePath)
let keys = [NSURLCreationDateKey, NSURLPathKey, NSURLFileSizeKey,NSURLTagNamesKey]
var values = try fileURL.resourceValuesForKeys(keys)
if let creationDate = values[NSURLCreationDateKey]{
print("creationDate: \(creationDate)")
}
values[NSURLTagNamesKey] = ["test", "sample", "playground"]
values[NSURLCreationDateKey] = "now" // à creates an error
print(values[NSURLTagNamesKey])
print(values[NSURLCreationDateKey])
try fileURL.setResourceValues(values)
}
在我们的示例中,我创建了一个指向现有文件testFile.txt的引用。我想了解一些文件属性,这些属性可以通过将字符串数组传递给我的文件引用的resourceValuesForKeys方法来获取。我可以使用索引符号读取值,甚至更新值。此外,我可以用setResourceValues方法将新值写回文件。我们遇到的问题很微妙,直到我们执行这段代码才会显示出来。NSURLCreationDateKey期望其值是一个有效的NSDate。
然而,我传递了一个字符串值。在运行时,当我们尝试使用新的资源值更新我们的文件时,我们的代码块崩溃并出现错误。我们想要的是一种在编译时检查约束的方法。Swift 3 现在为我们提供了这样做的方法,Foundation 框架也已更新以反映这一新特性。让我们更新我们的示例,以反映使用类型安全访问我们的文件资源。
在 Swift 3 中:
if let filePath = Bundle.main.path(forResource: "testFile",
ofType: "txt") {
var fileURL = URL(fileURLWithPath: filePath)
let keys: Set<URLResourceKey> = [.creationDateKey, .pathKey,
.fileSizeKey, .tagNamesKey]
let values = try fileURL.resourceValues(forKeys: keys)
if let creationDate = values.creationDate{
print("creationDate: \(creationDate)")
}
var newvalues = values
newvalues.creationDate = "now"
//error: cannot assign value of type 'String' to type 'Date?'
// newvalues.creationDate = "now"
// ^~~~~
}
在我们的更新示例中,我们的资源类型现在是类型为URLResourceKey的强类型值。当我们从文件请求资源时,我们返回一个包含强类型属性的 struct。这两个变化使我们能够进行类型安全访问,并帮助我们编译时捕捉问题。这是一个相当好的特性,Foundation 团队也同意,这从他们更新的所有 API 中可以看出,这些 API 为我们提供了更好的类型约束。
测量和单位
随着 Swift 3 的发布,Foundation 团队发布了一个新的 API,使开发者处理测量变得更加容易。通过允许开发者从常见的内置单位(或创建自定义单位)创建测量,在单位之间进行转换,以及为显示格式化它们。我们将涵盖 Measurement API 的主要区域,以便你准备好在你的项目中使用它们。
测量
一个测量由一个单位和值组成。单位是一个包含单个属性以持有符号的类。我们稍后会回到单位。现在,让我们专注于你可以用测量做什么。
public struct Measurement<UnitType : Unit> : ReferenceConvertible, Comparable, Equatable {
public typealias ReferenceType = NSMeasurement
/// The unit component of the `Measurement`.
public let unit: UnitType
/// The value component of the `Measurement`.
public var value: Double
/// Create a `Measurement` given a specified value and unit.
public init(value: Double, unit: UnitType)
}
let initialAngle = Measurement(value: 30, unit: UnitAngle.degrees)
let maxAngle = Measurement(value: 90, unit: UnitAngle.degrees)
var currentAngle = initialAngle * 2.5 // 75.0 °
if currentAngle > maxAngle{
print("Angle is greater than max supported angle")
}
通过测量,我们可以定义一个值及其单位类型,这可以用于转换为其他类型。我们还获得了验证,确保我们不会混合不兼容的测量值。将长度单位添加到存储体积的单位上并不一定有意义。如果没有测量 API,你将不得不编写自己的方法来确保适当的约束保持有效。
单位和维度
Unit 是一个具有符号属性的类。Dimension 是 Unit 的子类,用于表示单位类别。Dimension 包含一个基本单位,并允许通过 UnitConverter 类型在单位之间进行转换。Dimension 类型还包含了许多常见单位的单例,这些单位在处理测量时可能会遇到。
class Unit : NSObject, NSCopying, NSSecureCoding {
open var symbol: String { get }
public init(symbol: String)
}
class Dimension : Unit, NSSecureCoding {
@NSCopying open var converter: UnitConverter { get }
public init(symbol: String, converter: UnitConverter)
class func baseUnit() -> Self
}
下表列出了 Foundation 中可用的 Dimension 子类。
表 2,Dimension 子类:
UnitAcceleration |
UnitAngle |
UnitArea |
|---|---|---|
UnitConcentrationMass |
UnitDispersion |
UnitDuration |
UnitElectricCharge |
UnitElectricCurrent |
UnitElectricPotentialDifference |
UnitElectricResistance |
UnitEnergy |
UnitFrequency |
UnitFuelEfficiency |
UnitLength |
UnitIlluminance |
UnitMass |
UnitPower |
UnitPressure |
UnitSpeed |
UnitTemperature |
UnitVolume |
当使用内置的 Dimension 子类时,你可以免费获得基本的转换。例如,我创建了一个 boxHeight 测量值,单位为 UnitLength.feet,以及一个 *smallBoxHeight*,单位为 UnitLength.inches。然后,我将这两个测量值相加并存储了值。totalHeight 变量使用 UnitLength 的基本单位米来存储组合值。
let boxHeight = Measurement(value: 3.2, unit: UnitLength.feet)
let smallBoxHeight = Measurement(value: 20, unit: UnitLength.inches)
let totalHeight = boxHeight + smallBoxHeight
print(totalHeight) // 1.48336 m
我们还可以使用我们的比较操作,如 ==、>、<,因为我们的测量值在转换之前会转换为基本单位。
if boxHeight > smallBoxHeight{
print("boxHeight is still larger")
}
自定义单位
将来会有需要创建一个不符合预构建 Dimension 子类之一的单元类型的时候。为了创建自己的单位,你需要在现有的 Dimension 上创建一个单元类型,提供符号和单位转换函数。
let burgerSymbol = "\u{1F354}" //
let burgers = UnitLength(symbol: burgerSymbol , converter: UnitConverterLinear(coefficient: 2))
你可以向任何 Dimension 子类添加新的单位。我们前面的例子在 UnitLength 上创建了一个新的 burgers 单位。如果你需要一个更多基于时间的单位,你可以在 UnitDuration 上创建一个单位。
let gittieSecond = UnitDuration(symbol: "gs", converter: UnitConverterLinear(coefficient: 0.5))
通过提供转换函数,你确保了你的单位可以在其单位和关联的 Dimension 的基本单位之间进行转换。
自定义维度类型
现在我们知道了如何为 Dimension 子类创建新的单位,接下来要问的问题是,你是否可以创建自定义的 Dimension 类型。正如你可能已经猜到的,创建一个新的 Dimension 子类也是可能的,并且很简单。
public class ToddlerMovement: Dimension{
static let tummyCrawl = ToddlerMovement(symbol: "crawls", converter: UnitConverterLinear(coefficient: 0.25))
static let allFours = ToddlerMovement(symbol: "allfours", converter: UnitConverterLinear(coefficient: 2.0))
static let shakySteps = ToddlerMovement(symbol: "shaky_steps", converter: UnitConverterLinear(coefficient: 1))
}
let playPenDistance = Measurement(value: 4, unit: ToddleMovement.tummyCrawl)
let kitchenFloorDistance = Measurement(value: 20, unit: ToddleMovement.shakySteps)
let almostThereDistance = Measurement(value: 10, unit: ToddleMovement.allFours)
print(playPinDistance) // 4.0 crawls
print(kitchenFloorDistance) // 20.0 shaky_steps
print(almostThereDistance) // 10.0 allfours
转换函数
在之前的 Measurement 示例中,我们使用了 UnitConverterLinear 函数作为我们的转换函数。如前所述,每次向 Dimension 添加新单位时,都需要包含一个转换函数。转换方法用于在 Dimension 基本单位值之间进行转换。UnitConverterLinear 转换器允许你在关系是线性的情况下(例如,y = mx + b)在单位之间进行转换。此类有两个初始化器:
-
当没有偏移量时,提供一个仅包含斜率(系数或我们线性方程中的 m)。
-
另一个允许你提供斜率和偏移量(b)。
class UnitConverterLinear : UnitConverter {
var coefficient: Double { get }
var constant: Double { get }
init(coefficient: Double)
init(coefficient: Double, constant: Double)
}
线性转换:
baseUnit = value * coefficient + constant
value = (baseUnit - constant) / coefficient
UnitConverterLinear 转换器非常有用,它将允许你通过仅提供两个单位值之间的比率来覆盖许多转换。然而,这个转换器可能不适合两个单位之间的所有关系。因此,你还可以子类化 UnitConverter 并在需要时创建一个满足你用例的转换公式。
class UnitConverter : NSObject {
open func baseUnitValue(fromValue value: Double) -> Double
open func value(fromBaseUnitValue baseUnitValue: Double) -> Double
}
如果你选择创建自己的自定义单位转换器,你需要重写 baseUnitValue(fromValue value: Double) 和 value(fromBaseUnitValue baseUnitValue: Double) 方法。这两个方法用于内部为你执行转换。
测量值格式化
处理格式化问题很棘手,你只需看看 NSDateFormatter 就能明白在处理日期时事情可能会多么复杂。幸运的是,Foundation 团队为我们添加了一个新的测量格式化器来承担所有繁重的工作。MeasurementFormatter 将处理我们的 Measurements 和 Unit 类型格式化,同时考虑到用户的地区设置。
class MeasurementFormatter : Formatter, NSSecureCoding {
var unitOptions: MeasurementFormatter.UnitOptions
var unitStyle: Formatter.UnitStyle
@NSCopying var locale: Locale!
@NSCopying var numberFormatter: NumberFormatter!
func string(from measurement: Measurement<Unit>) -> String
func string(from unit: Unit) -> String
}
MeasurementFormatter 上的 UnitOptions 属性是一个特殊的属性,我们需要花点时间来讨论。UnitOptions 属性是 MeasurementFormatter 上的一个结构体,并指示我们的格式化器默认使用用户地区设置的首选单位。
extension MeasurementFormatter {
public struct UnitOptions : OptionSet {
public init(rawValue: UInt)
public static var providedUnit:
MeasurementFormatter.UnitOptions { get }
public static var naturalScale:
MeasurementFormatter.UnitOptions { get }
public static var temperatureWithoutUnit:
MeasurementFormatter.UnitOptions { get }
}
}
在我们的示例中,我们创建了一个自定义单位(*gittieSecond*)和一个使用此单位的测量。然后我们创建了一个 MeasurementFormatter 并调用 string 方法,传递我们的测量,以获取使用 Dimension 基本单位格式的测量。尽管我们的自定义单位有一个 gs 符号,但格式化器会将我们的测量转换为并使用 seconds 显示。
let formatter = MeasurementFormatter()
let gittieSecond = UnitDuration(symbol: "gs", converter: UnitConverterLinear(coefficient: 0.5))
let getterThere = Measurement(value: 5, unit: gittieSecond) // 5.0 gs
let formattedGS = formmatter.string(from: getterThere)
print(formattedGS) // 2.5 sec
要将格式化器的输出从使用基本单位更改为使用我们的 gittieSecond 单位,我们需要修改格式化器上的 UnitOptions 属性。在我的地区设置中,UnitDuration 的首选单位是 seconds。通过更改 UnitOptions 使用提供的 referenceUnit,我们确保我们的格式化器将使用与传递给格式化器的 getterThere 测量相同的单位。
formatter.unitOptions = .providedUnit
formatter.string(from: getterThere) // 5 gs
摘要
在本章中,我们讨论了 Foundation 框架的变化。我们从 Foundation 中可变性和新值类型的话题开始讨论。你了解到在代码中,值类型和引用类型都有其合适的位置。接下来,我们介绍了嵌套枚举和强类型字符串枚举。我们探讨了类属性和值类型的类型安全访问优势。最后,我们花费了大量时间讨论了今年在 Foundation 中引入的新测量 API。在下一章中,我们将讨论测试和调试。Swift 3 在测试和调试方面引入了多项变化,这些变化应该有助于提高你编写的代码质量。
第九章:使用 Xcode 服务器和 LLDB 调试改进您的代码
您可以通过学习如何测试代码来提高代码质量的最大技能之一。使用 XCTest 测试框架将单元测试添加到您的代码中,将帮助您提高代码质量,并提供了记录代码工作方式的额外好处。当您从个人开发者项目过渡到多成员团队时,维护独立编写的测试变得更加困难。将自动化测试添加到服务器上的持续集成管道中,可以帮助解决这些问题,就像源代码库帮助管理大型项目中的代码一样。
在本章的第一部分,我们将介绍 Xcode 服务器作为持续集成服务器的能力,以及如何将自动化测试包含进来以改进您的测试工作流程。在第二部分,我们将描述如何使用 LLDB 在 Linux 上调试您的代码。
使用 Xcode 服务器进行持续集成概述
使用基于服务器的测试具有几个好处,这些好处可能足以证明您团队在时间投资上的合理性。如果您对是否走这条路适合您的团队持怀疑态度,请确保在评估时考虑以下因素:
-
将构建和单元测试移至服务器可以释放您的本地机器继续工作在功能开发和调试上,同时您的构建和测试套件在远程运行。
-
构建触发器可以在代码提交时运行测试,并在测试失败导致意外变化时通知您的团队。
-
服务器上的测试是一致的,每次都以相同的方式运行;这意味着个人开发环境和项目调整不会影响测试运行。
-
您可以安排完整的测试套件在您方便的时候运行,以及短运行测试在每次代码提交时执行,例如。
-
您可以让您的测试在多个开发环境和硬件上执行。例如,您可以使用服务器环境轻松地在多个 iPad 模型和操作系统版本以及几部 iPhone 上运行测试。这将是手动执行耗时的工作。
持续集成工作流程是一个从在开发机器上本地开发开始,然后将您的代码提交到仓库的过程。接下来,您将项目迁移到 Xcode 服务器进行处理。为了使 Xcode 服务器能够处理您的代码,您必须以 bots 的形式提供一些指令。在您的开发机器上,您创建带有在 Xcode 服务器上运行的规则 bots。bot 的执行称为 integration,可以手动运行或根据计划运行。一旦 integration 完成,活动将报告回您的开发 Mac。
机器人功能
没有使用机器人,您无法利用 Xcode 服务器做任何有用的事情。机器人构建您的代码并运行您的测试,使用提供的 Xcode 项目方案。一个精心制作的机器人可以控制其运行的时间和如何与您和您的团队沟通其活动。例如,您可以让机器人在发生新的提交时运行,并通过电子邮件将集成状态(例如,成功或失败)发送给您和/或您的开发团队。您还可以添加预集成和后集成触发器,这些触发器可以执行脚本以与 Web 服务通信或基于初始集成输出运行额外的性能测试。
监控和管理机器人
一旦您创建了您的机器人,您就可以在 Xcode Server 中管理和监控它们。Xcode 服务器可以被配置为将机器人的状态推送到您的开发 Mac,提供一个 web 钩子以在浏览器或第三方应用程序(例如,Slack/HipChat)中查看活动,或者发送电子邮件报告。在您的开发 Mac 上,您可以使用 Xcode 中的报告导航器查看非常详细的报告。
配置 Xcode 服务器
要使用 Xcode 服务器,您必须从 Mac 的 App Store 下载并安装 macOS Server(以前称为 OS X Server)。
注意
为 Xcode 服务器添加仓库
Xcode Server 需要一个代码仓库来执行工作,并且与 Git 或 Subversion 兼容。你创建的任何机器人都需要访问仓库。机器人会尝试通过 SSH 或 HTTPS 连接到仓库。苹果公司对您的仓库设置选项做了很好的描述。
注意
您可以参考他们设置指南的以下部分,以获取如何配置您的仓库以供 Xcode Server 和您的机器人访问的逐步说明:developer.apple.com/library/prerelease/content/documentation/IDEs/Conceptual/xcode_guide-continuous_integration/PublishYourCodetoaSourceRepository.html - //apple_ref/doc/uid/TP40013292-CH8-SW1。
配置机器人
“机器人”是由 Xcode Server 运行以从源代码库构建和测试代码的过程。每次你运行你的“机器人”实例时,你都在执行所谓的“集成”。你创建一个“机器人”,并将一个关联的方案添加到“机器人”中,该方案引用你的开发机器。创建“机器人”向导会引导你选择要使用的方案、设置运行频率以及提供在集成前后运行的任何 shell 脚本。
注意
你可以在[如何设置机器人](https://developer.apple.com/library/tvos/documentation/IDEs/Conceptual/xcode_guide-continuous_integration/ConfigureBots.html - //apple_ref/doc/uid/TP40013292-CH9-SW1)中了解更多信息。
去年,Xcode Server 引入了一个新功能,允许我们添加自定义环境变量,我们的“机器人”可以访问。今年 Xcode Server 允许我们添加在每次“集成”前后运行的预和后脚本。此功能可能允许你在“机器人”构建和执行测试之前,预先加载外部文件或数据以进行单元测试。另一个例子可能是后“集成”脚本与 Rest API 通信,以发送成功或失败状态。
管理和监控你的集成运行
你可以在 Xcode 的“报告导航器”中访问每个“机器人”的详细报告(视图 | 导航器 | 显示报告导航器)。此视图还允许你创建额外的“机器人”或编辑现有的“机器人”。选择侧边栏中列出的任何“机器人”都会给你一个集成结果的摘要报告。

报告导航器
使用 LLDB 进行调试
LLDB是驱动 Xcode 的调试器。在 Xcode 的调试控制台中,你可以找到一个控制台窗口,它为你提供了访问LLDB提示符的权限。在 Linux 或从命令行中,你可以从 Swift REPL访问LLDB。让我们探索如何使用LLDB通过一些你可能已经知道或不知道的命令来调试我们的程序。
注意
你可以在 LLDB 调试指南中了解更多关于使用 LLDB 进行调试的信息:[LLDB 调试指南](https://developer.apple.com/library/prerelease/content/documentation/General/Conceptual/lldb-guide/chapters/Introduction.html - //apple_ref/doc/uid/TP40016717-CH1-DontLinkElementID_42).
LLDB 命令语法
与LLDB交互就像在提示符中输入一个命令一样简单。命令结构包含零个或多个子命令,后跟零个或多个选项或参数。
<command> [<subcommand>...] [--<option> [<option-value>]]... [argument]...
子命令和参数是空格分隔的标记;而选项也是空格分隔的,但使用双横线(有时是单横线)作为前缀。一个示例 LLDB 命令是设置一个函数的断点。在以下示例中,我们将为speakToMe()函数设置一个breakpoint:
(lldb) breakpoint set -n speakToMe
你可以通过在LLDB提示符下输入help来始终获得帮助。不带参数输入help将列出所有可用命令及其简短描述:
(lldb) help
你可以通过输入help加上命令名称或命令名称和子命令名称来获取特定命令的帮助,以获得更专业化的结果。
(lldb) help breakpoint
(lldb) help breakpoint set
管理断点
断点是中断正在运行的程序以在特定点进行检查的主要方式。我们可以通过LLDB创建、修改、删除或列出断点。
创建断点
我们使用breakpoint set命令来创建断点:
4> func sayHello(){
5\. print("Hi")
6\. }
7> sayHello()
Hi
8> :breakpoint set --name sayHello
Breakpoint 1: where = $__lldb_expr5`__lldb_expr_4.sayHello () -> () + 4 at repl.swift:5, address = 0x00000001005c6064
列出断点
我们使用breakpoint list命令来列出程序中断点的名称和位置:
(lldb) breakpoint list
Current breakpoints:
1: name = 'sayHello', locations = 1, resolved = 1, hit count = 1
1.1: where = $__lldb_expr5`__lldb_expr_4.sayHello () -> () + 4 at repl.swift:5, address = 0x00000001005c6064, resolved, hit count = 1
修改断点
你可以使用断点添加激活条件。使用breakpoint modify命令,你可以使用以下任何选项来更改目标断点的行为。
-D ( --dummy-breakpoints ) : Sets Dummy breakpoints
-T <thread-name> ( --thread-name <thread-name> ) The breakpoint stops only for the thread whose thread name matches this argument.
-c <expr> ( --condition <expr> ) The breakpoint stops only if this condition expression evaluates to true.
-d ( --disable ) Disable the breakpoint.
-e ( --enable )Enable the breakpoint.
-i <count> ( --ignore-count <count> ) Set the number of times this breakpoint is skipped before stopping.
-o <boolean> ( --one-shot <boolean> ) The breakpoint is deleted the first time it stop causes a stop.
-q <queue-name> ( --queue-name <queue-name> ) The breakpoint stops only for threads in the queue whose name is given by this argument.
-t <thread-id> ( --thread-id <thread-id> ) The breakpoint stops only for the thread whose TID matches this argument.
-x <thread-index> ( --thread-index <thread-index> ) The breakpoint stops only for the thread whose index matches this argument.
启用和禁用断点
你可以使用断点 ID 或位置来启用和禁用断点。你将 ID 传递给enable或disable子命令:
(lldb) breakpoint enable 1.1
1 breakpoint enabled.
(lldb) breakpoint disable 1.1
1 breakpoint disabled.
删除断点
当你不再需要你的断点时,你可以简单地使用带有断点 ID 或位置的删除子命令来移除它:
(lldb) breakpoint delete 1.1
命令别名
命令别名允许你为常用命令创建更短的语法。你还可以提供帮助文本来伴随你的别名。你可以通过输入help command来查看如何管理命令别名的更多详细信息。

在以下示例中,我们创建一个命令别名来执行我们传递给别名的命令,使用 Unix shell:

摘要
在本章中,我们介绍了使用 Xcode Server 的持续集成工作流程的基本知识。你学习了什么是机器人以及它们是如何在 Xcode Server 上构建和测试你的代码的。随着 Swift 在 Linux 上的加入,我们需要探索 Xcode 之外的其他测试代码的选项。LLDB 功能非常强大,甚至为你的调试会话提供了定制选项。在我们下一章和最后一章中,我们将讨论在 Linux 服务器上编写 Swift。
第十章. 在服务器上探索 Swift
除非您正在制作一个非常基础的应用程序,否则您很可能需要某种形式的后端服务器来使您的应用程序真正有用。Swift 在 Linux 上运行是一个大事件,尤其是考虑到 Linux 在托管和运行服务器方面的普及。Swift 3 为开发者打开了使用与他们在 iOS、macOS、tvOS 和 watchOS 上创建应用程序相同的 Swift 创建服务器端应用程序的可能性。在本章结束时,您将拥有一个完全使用 Swift 编写的服务器端应用程序,它可以在 Linux 服务器上无缝运行。
IBM Swift 包目录
在 第二章 中,我们介绍了在 Linux 上安装 Swift 工具链和配置环境的步骤。我们用 Swift 编写了第一个程序,并利用 Swift 包管理器来管理我们的依赖项。在完成基础知识后,我想提及 IBM 的 Swift 包目录。
IBM Swift 包目录是一个托管 Swift 库和模块链接的网站,您可以使用 Swift 包管理器使用这些库和模块。IBM 希望它成为一个社区资源,让开发者能够找到并分享他们项目的代码。您可以在以下链接中找到该网站:developer.ibm.com/swift/the-ibm-swift-package-catalog。您绝对应该保留这个链接,因为它将是一个您经常使用的网站。您可以根据排名、受欢迎程度或关键词探索项目。
介绍我们的服务器项目
让我们以一场盛宴结束!对于最后一章,我们将创建一个小项目来测试使用 Swift 开发服务器应用程序。我们将使用 IBM Swift 包目录来查找网络服务器框架。我非常喜欢使用 Slack 进行团队沟通。如果您还没有尝试过,您应该考虑评估它是否可以成为您团队的好工具。Slack 的一个强大功能是您有一系列集成选项可以自定义团队体验。Slack 已经向开发者开放了许多 API,以便进行定制和集成。Slack 甚至为用户提供了一个应用商店,让他们可以添加第三方应用程序,以便团队共同使用。唯一的缺点是,您的第三方应用程序或集成必须托管在外部服务器上。我们将创建一个 Slack 集成,您稍后可以将其修改成您自己的完整 Slack 应用程序。我们的 Slack 集成将完全使用 Swift 编写,并且可以托管在云中的 Linux 虚拟机上,例如 Heroku、Digital Ocean 或亚马逊网络服务。
注意
您可以从 www.packtpub.com/support 下载此项目的代码。
项目描述和依赖项
我们的项目需要一个能够正常工作的网络服务器框架。我们可以从头开始编写一个,或者查看可用的第三方框架。使用 IBM Swift 包目录,我找到了几个高度评价且正在积极开发且深受开发者喜爱的网络应用框架。IBM 的 Kitura、Perfect by PerfectlySoft 和 Vapor by Vapor 都是可行的候选框架。如果你曾经接触过 Node.js 和 Express,或者 Ruby on Rails,那么这些项目对你来说都会感到熟悉。虽然这些框架中的任何一个都可以用于我们的项目,但我选择了 Vapor 项目作为我们的应用程序,因为它在我编写这一章的时候在“基本”类别中排名首位。
根据 Vapor 的说法,他们的项目是最常用的 Swift 编写的网络框架。你可以了解更多关于 Vapor 的信息,包括它支持的 Swift 版本和文档链接,请访问swiftpkgs.ng.bluemix.net/package/vapor/vapor。
现在我们已经介绍了我们将要使用的库和框架,让我给你描述一下我们将一起构建的服务器应用程序。Slack 为开发者提供了创建自定义集成或制作可供任何团队使用的 Slack 应用程序的选项。我们将为单个团队创建一个自定义集成。然而,你可以轻松地将我们的定制转换为任何团队都可以发现的全功能应用程序。
我们将构建一个用于销售小工具的店面网络应用程序。当用户从我们这里购买小工具时,我们将处理订单并将订单发送到我们的 Slack 订单跟踪频道。为了保持这个应用程序的简单性,我们将采取一些捷径。
设置我们的环境和项目
由于你在第二章中学习了如何安装 Swift,我们将跳过这一步,直接进入安装 Vapor 框架。我们将添加 Vapor 的命令行工具包,以便访问快捷命令和常见任务的辅助功能。
注意
你可以在vapor.github.io/documentation/找到详细的文档链接和使用 Vapor 的示例。
这就是如何做的:
要安装工具包,请在终端中运行以下命令:
curl -sL toolbox.vapor.sh | bash
你可以通过运行以下命令来验证命令是否成功:
vapor -help
接下来,让我们创建一个新的 Vapor 项目,并将其命名为storefront:
vapor new storefront
我们新创建的项目将具有以下文件结构:

文件结构应该对你来说很熟悉,因为它遵循 Swift Package Manager 所需的结构。在幕后,vapor 使用swift package init--type executable创建一个新的项目。Vapor 脚本还将在Package.swift中将 vapor 框架作为依赖项添加。我们的main.swift是我们的入口点,因为我们创建了一个可执行程序。
我打算在我的 Mac 上开发我的代码,然后将其部署到云中的 Linux 虚拟机。对我来说,好处是,我可以使用 Xcode 及其调试工具进行本地开发。实际上,Vapor 框架通过提供工具包中的命令来生成用于开发的 Xcode 项目,支持这一概念。让我们创建一个可以用于我们店面应用开发的 Xcode 项目:
vapor xcode
当 Vapor 创建一个配套的 Xcode 项目时,它首先检查你是否缺少在Package.swift文件中指定的任何依赖项。在创建 Xcode 项目之前,Vapor 会为你下载任何缺少的依赖项。此外,Vapor 将创建一个用于在 Xcode 中运行你的应用程序的方案。最后,Vapor 将显示 Xcode 项目期望链接的哪个工具链:
$vapor xcode
No Packages folder, fetch may take a while...
Fetching Dependencies [Done]
Generating Xcode Project [Done]
Select the `App` scheme to run.
Make sure Xcode > Toolchains > 3.0-GM-CANDIDATE is selected.
Open Xcode project?
y/n>nz
每次你为项目创建新的依赖项时,你必须重新构建项目,以便 Swift Package Manager 可以在尝试编译你的代码之前下载你的新依赖项:
vapor clean or vapor build --clean
让我们快速查看Package.swift文件,看看 Vapor 为我们创建了什么。我们可以使用更适合我们项目的名称来配置应用程序的名称。当前的默认名称是VaporApp,但我们可以将其更改为Storefront。你也应该注意到,vapor 框架已作为依赖项为我们添加。
import PackageDescription
let package = Package(
name: "VaporApp",
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 0, minor: 18)
],
exclude: [
"Config",
"Database",
"Localization",
"Public",
"Resources",
"Tests",
]
)
当你使用 Vapor CLI 创建新项目时,Vapor 会在项目中添加带有文档的示例代码。打开main.swift并浏览包含的路由和注释。删除此文件中的所有内容,我们将从头开始构建我们的应用程序。
Vapor 框架
在 Vapor 术语中,Droplet 是一个服务容器,充当 Vapor 服务提供的网关。使用 Droplet 注册路由并添加中间件以启动服务器。为了开始,我们需要导入 Vapor 并创建一个 Droplet 实例。
import Vapor
let drop = Droplet()
我们还可以通过属性来定制我们的 Droplet 实例的行为。你可以在 Vapor 的文档中了解更多关于选项的信息。
路由
现在我们已经有了 Droplet 实例,我们需要讨论路由。路由是每个 Web 框架的基本功能。当接收到一个传入请求时,我们需要有一种适当过滤和处理每个请求的方法。Vapor 为你提供了多种解决路由问题的选项。我们将为我们的应用程序创建两个路由:一个用于服务我们的商店页面,另一个用于当用户在我们的页面上购买商品时响应post请求。
在 Vapor 中,一个基本的路由由一个方法、路径和闭包组成。我们的两个路由属于这一类别。Vapor 路由支持标准的 RESTful HTTP 方法(get、post、put、patch、delete 和 options)。我们通过在 Droplet 实例上调用相应的方法来注册路由,传递我们的路由路径,并返回我们定义的闭包。
drop.get("/") { request in
return try drop.view.make("shop.html")
}
drop.post("purchase") { request in
// more stuff happening here but omitted
var response = try Response(status: .ok, json: json)
return response
}
我们的第一条路由处理我们网站根目录的所有get请求。当请求此路由时,我们返回shop.html视图。我们的第二条路由处理/purchase路由的post请求。一旦我们完成工作,我们就向请求者返回一个带有状态和 JSON 有效负载的响应。
Vapor 还支持嵌套路由和参数。创建一个嵌套路由就像在注册路由时将 URL 中的正斜杠替换为逗号一样简单。
// Nested route
drop.get("products", "vehicles", "trucks") { request in
return "You requested /products/vehicles/trucks"
}
Vapor 通过使参数类型安全来处理参数。许多 Web 框架默认使用字符串作为路由参数和类型,这可能导致错误。使用 Swift 的闭包允许更安全地访问路由参数。在以下示例中,我们定义了一个接受Int参数的路由。我们的路由匹配artboard/:id,其中我们的:id参数必须是一个整数值。
// Type Safe parameters
drop.get("artboard", Int.self) { request, productId in
return "You requested Artboard #\(productId)"
}
我们也可以不使用路由参数来编写这个,然后通过请求对象访问我们的参数。
drop.get("artboard", ":id") { request in
guard let productId = request.parameters["id"]?.int else {
throw Abort.badRequest
}
return "You requested Artboard #\(productId)"
}
创建视图
当你想从你的应用程序发送 HTML 时,你创建视图。你可以从一个 HTML 文档创建视图,或者使用 Mustache 等渲染器构建你的视图。默认情况下,视图存储在Resources/Views目录中。回到为我们根目录注册的第一个路由,我们使用视图返回一个 HTML 文档(shop.html)。你通过在 Droplet 实例上调用view.make方法来创建视图的实例。
drop.get("/") { request in
return try drop.view.make("shop.html")
}
更复杂的文档,如 mustache 模板,需要更多信息来处理和创建视图。这些附加信息作为第二个参数传递给view.make方法。
drop.get("shop_template") { request in
return try drop.view.make("shop.template", [
"countdown": "2 days left",
"shopper_count": "1,000"
])
}
公共资源
对于大部分情况,我们希望我们的服务器代码和文件免受窥探的眼睛和网页爬虫的侵害。Vapor 为我们处理这个问题。当我们需要提供可以从我们的视图中访问的资源时,我们使用在应用程序根目录下创建的Public文件夹。我们将我们的图片、脚本和样式存储在Public文件夹的嵌套目录中。
定义我们的商店视图
当我们在应用程序上提供根级文档时,我们返回一个shop.html视图。我们的简单页面显示一条欢迎信息和三个产品的详细信息。

当用户点击“立即购买”按钮时,我们执行一个 jQuery Ajax post 命令与我们的服务器通信。我们将我们想要购买的产品 ID 发送到我们的"/purchase"路由。
在服务器上,当我们收到与这个路由匹配的请求时,我们会提取产品 ID 并在我们的本地商店中搜索匹配的产品。当然,在一个生产应用中,我们会使用数据库来存储我们的产品,甚至填充我们的商店列表。在无法在我们的请求对象中找到有效的产品 ID 或无法为提供的产品 ID 找到匹配的产品的情况下,我们会抛出一个错误,并将其发送回客户端。
最后,我们创建一个包含我们产品的一些详细信息的 JSON 负载,并带有成功的状态码将其返回给客户端。
drop.post("purchase") { request in
drop.log.info("purchase request made")
guard let product_id = request.data["product_id"]?.int else {
throw Abort.badRequest
}
guard let product = products.filter({ (prod) -> Bool in
return prod.id == product_id
}).first else{
throw Abort.badRequest
}
let json = try JSON(node: [
"Product" : "\(product.name)",
"price" : "\(product.price)",
])
// more work happening and omitted
var response = try Response(status: .ok, json: json)
return response
}
当我们的客户端收到 post 响应时,我们会显示一个感谢用户购买的提示对话框。我们还在控制台中显示返回的 JSON 数据。
Slack 集成
现在我们已经了解了我们网络应用的基础,让我们通过集成 Slack 来让它变得更有趣。使用 Slack 的 API,我们可以扩展 Slack,使我们的工作流程更加完善。在我们的案例中,我们希望通知我们的运营团队新的订单,以便他们可以立即开始处理。我们将利用 incoming webhooks 从我们的 Swift 服务器向 Slack 发送消息。虽然这个 webhook 只会用于我们的团队,但你可以在api.slack.com/的文档中阅读,并轻松地将我们的自定义集成转换为任何团队都可以将其纳入其工作流程的 Slack 应用。
创建自定义集成
由于我们的自定义集成只与单个 Slack 团队一起工作,如果你还没有,你需要创建一个 Slack 账户和团队。一旦你这样做,你可以导航到 Slack 应用目录的构建部分,位于:
-
点击“创建自定义集成”按钮。
![创建自定义集成]()
-
接下来选择“Incoming WebHooks”链接。
![创建自定义集成]()
选择一个频道来发布你的消息或者创建一个新的频道。我选择将我的消息发送到我的订单频道。选择好你的频道后,点击“添加 incoming WebHooks 集成”按钮。

在这个视图中,你可以看到设置说明以及你可以为你的用例自定义的字段。你可以为这个集成提供一个可选的描述性标签以及用户名。默认名称是 incoming-webhook,但我将其改为了 OrderUp。我还添加了一个表情符号,作为我添加到这个频道的消息的图标。一旦你预览了你的设置,你只需点击“保存设置”按钮,你的更改就会生效。
更新我们的服务器以向 Slack 发布
在我们关闭这个视图之前,我们需要将 Webhook URL 复制到我们的外部服务中。你可以点击 复制 URL 按钮,它将被添加到你的剪贴板。让我们回到 Swift 并打开 main.swift。更新你的购买路由以创建 Slack 发送 POST 请求到 Slack 服务器的 JSON 有效负载:
drop.post("purchase") { request in
// omitted code above
let slack_payload = try JSON(node: [ "attachments":
try JSON(node: [
try JSON(node: [
"fallback": "New purchase Request",
"pretext": "New purchase Request",
"color": "#D00000",
"fields": try JSON(node: [
try JSON(node: [
"title" : "Product: \(product.name)",
"value" : "Price: \(product.price)",
"short" : "false"
])
])
])
])
])
_ = try drop.client.post("https://hooks.slack.com/services/<your hook id>", headers: [:], query: [:], body: slack_payload)
var response = try Response(status: .ok, json: json)
return response
}
我们格式化的消息有效负载发送了一条通用消息,该消息将在你的桌面和移动端 Slack 通知中显示("New purchase Request")。我们还使用附件语法提供了有关产品订单的详细信息。我们传递了产品名称和价格。
现在,当你在这个网站上购买时,你还会收到一条实时消息发送到 Slack 的订单频道。

摘要
在本章中,我们探讨了服务器上的 Swift。我们开发了一个完全用 Swift 运行的 Web 应用程序。我们的应用程序还与 Slack 通信以发布消息。我们使用 Vapor Web 框架创建了一个 Swift Web 服务器,并学习了如何从 Swift 调用 Slack Webhooks。虽然我们的例子相当简单,但很容易看出我们可以如何利用 Swift 的力量将其扩展到更大、更复杂的应用程序。
这就带我们来到了本书的结尾。我们涵盖了大量的内容,并学习了 Swift 3 的新特性。我们讨论了苹果对 Swift 进化的动机和目标。我向你展示了如何找到关于该语言的文档以及如何参与 Swift 的未来方向。我们探讨了 Linux 上的 Swift 以及这为服务器端 Swift 应用程序带来的可能性。Swift 3 添加了新的语言特性,这些特性改进了你将在日常编码项目中使用的许多库。
我希望这本书能成为你在深入开发 Swift 3 时的参考。虽然我们已经探讨了该语言的大部分特性,但全面覆盖语言的所有变化并不现实。虽然这本书为理解 Swift 3 的变化提供了良好的基础,但你仍需要在代码中应用你所学的知识。此外,你应该参考苹果的文档和 Swift 社区,以保持对语言最新发展的了解。最优秀的开发者永远不会依赖他们当前的知识。他们积极寻求学习更多。如果你想掌握 Swift 3,你必须利用你所能利用的所有资源来构建我们共同创建的基础。我知道你可以做到,并祝愿你在旅程中一切顺利。










浙公网安备 33010602011771号