简介
Swift 3即将在今年晚些时候正式发行,它将为所有Swift开发人员带来重大影响。如果你没有一直密切关注Swift项目的进展,你可能想知道Swift 3中将有哪些变化、它将如何影响你的编码以及何时开始将你的代码移植到Swift 3。
在本文中,我将向你重点介绍Swift 3将为你的编码带来的重大变化。至于是哪些变化,让我们来一探究竟吧!
准备工作
Swift 3的预览版本已经随同Xcode 8 beta版本一起发行了。虽然Swift 3的巨大改进已经不大可能,但在未来几个月中仍可能还有几个可被接受的变化。当Xcode在2016年底放出其GM版本时其功能将不再发生更改,所以在你把Swift 3应用程序发布到苹果应用程序商店之前你不得不继续推迟发行你的应用。
为了允许开发人员根据他们自己的需求对Swift 3进行调整,苹果特意在Xcode 8中提供了Swift 2.3版本作为Swift的小规模更新。作为一名开发人员,Swift 2.3很大程度上与Swift 2.2一致,但支持很多苹果在WWDC大会上宣布的新的SDK和Xcode特征。一旦Xcode 8越过测试版本,你就能够提交使用Swift 2.3开发的应用程序——如果你还没有把你的代码迁移到Swift 3的话。
因此,我建议你试验性地尝试一下我们在本文中讨论的特点,甚至在你的项目上运行一下迁移助手(Migration Assistant);这样,你可以感受一下正在发生的变化。但是,因为你在Xcode 8和Swift 3正式发行前还不能把应用程序发布到苹果应用程序商店,所以,你可能要考虑等一切有了着落后再将代码移植到Swift 3。
迁移到Swift 3
当转换为Swift 3版本时,你会发现几乎每个文件都需要更改!这主要是因为所有Cocoa API名称都已更改。或者,更确切地讲,尽管API相同,但还是有一个名称适合Objective C而另外一个名称适合Swift。Swift 3的目标是要实现在未来多年中编写Swift代码将变得更为自然。
苹果公司在Xcode 8中包括了一个迁移助手程序,它能够出色地帮助解决上述的大规模修改代码的问题。不过,你不要为还得修改几个地方而感到惊讶,因为移植器程序在这些地方是不会自动处理的。
你可以立即把以前的代码转换为Swift 2.3或Swift 3。如果你需要把它改回来,你可以随时使用Xcode中提供的命令“Edit > Convert > To Current Swift Syntax…”。所幸的是,编译器具有与迁移助手一样的智能性。如果你在方法调用上偶然使用了旧式API,编译器会提供一个“Fix It”选项,它将帮助你使用正确的新式API。
最好的消息是,Swift 3的目的是使其成为大规模更改源码的最后一个版本。所以,从长远来看,你应该保持你的程序的Swift代码从一个版本升级到另一个版本。虽然Swift核心团队不能预测未来,但他们已经承诺,如果他们确实需要打破源代码兼容性,他们将提供长期的废弃代码重用支持。这意味着,该语言已经取得源码的稳定性保证,这将有助于鼓励保守的公司采用它。
这就是说,实现二进制稳定的目标还没有实现。在本文末尾,你会发现更多的方面受这种影响。
实现Swift进化建议
目前,社区成员已经就swift改进方面提交了超过100条的建议,因为它是开放源码的。大量的建议 (目前70多条)在经过讨论和修改后已被接受。当然,那些已被驳回的建议也引发了一些激烈的讨论。然而,最终,将由核心团队就各种建议作最后决定。
核心团队和更广泛的社区之间的合作已经令人印象深刻。事实上,Swift已经在Github上赢得了3万颗星。每周都会有几项新建议被提交。即使是苹果工程师当他们想要进行更改时也要通过Github代码仓库提交建议。
在下面的段落中,你将看到如[SE-0001]这样的链接标签。它们是Swift演化建议数字。这里提供的建议编号都已被接受,并将在最终的Swift 3.0正式发行版本中使用。我之所以提供这些数据是为了方便你找到每个特定更改的全部详情对应的链接。
API变化
Swift 3中最大的更新就是其标准库API都使用了一致的命名约定。苹果的API设计指导原则(https://swift.org/documentation/api-design-guidelines/)中指明了开发团队在构建Swift 3项目时应当遵循的规则,这些规定极有助于新程序员对源码的可读性和可访问性。核心团队工作的一条重要指导原则是“好的API设计总是为调用方考虑”。他们力图使代码变得清晰易用。费话少说,下面将展示最有可能影响你的API中的一些变化。
首参数标签
让我们从一个你可能每天都使用的Swift例子开始吧。
现在,函数和方法的第一个参数总是带有一个标签,除非你以其它方式请求去掉。以前,当你调用一个函数或方法时总是省略第一个参数标签(SE-0046)。参考如下代码:
//注意下面代码中第一行是老式的Swift 2形式,第二行对应新式的Swift 3形式
- "RW".writeToFile("filename", atomically: true, encoding: NSUTF8StringEncoding)
- "RW".write(toFile: "filename", atomically: true, encoding: NSUTF8StringEncoding)
- SKAction.rotateByAngle(CGFloat(M_PI_2), duration: 10)
- SKAction.rotate(byAngle: CGFloat(M_PI_2), duration: 10)
- UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
- UIFont.preferredFont(forTextStyle: UIFontTextStyleSubheadline)
- override func numberOfSectionsInTableView(tableView: UITableView) -> Int
- override func numberOfSections(in tableView: UITableView) -> Int
- func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView?
- func viewForZooming(in scrollView: UIScrollView) -> UIView?
- NSTimer.scheduledTimerWithTimeInterval(0.35, target: self, selector: #selector(reset), userInfo: nil, repeats: true)
- NSTimer.scheduledTimer(timeInterval: 0.35, target: self, selector: #selector(reset), userInfo: nil, repeats: true)
注意,上面的方法定义中如何使用像“of”、“to”、“with”和“in”这样的介词。这些都是努力优化代码可读性的一部分。
如果方法调用可读性在不使用介词时就已经很好并且不需要标签,那么你应明确使用下划线来排除第一个参数名称:
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
- override func didMoveToView(_ view: SKView) { ... }
在许多编程语言中,方法可能共享使用基本命名并提供不同的参数名称。Swift也没有例外。因此,现在你会遇到大量的重载方法命名,因为以前的Api都已被翻译得更直接易读。下面提供了index()方法的两种表达形式:
- let names = ["Anna", "Barbara"]
- if let annaIndex = names.index(of: "Anna") {
- print("Barbara's position: \(names.index(after: annaIndex))")
- }
总之,参数名称的改变使得方法命名更一致和更容易学习。
忽略不必要的关键字
在以前若干个版本的苹果开发库中,方法都是包括一个名称以表明它们的返回值。由于Swift编译器提供了新的类型检查功能,这样的格式已经没大有必要了。开发团队认真审视了如何过滤掉所有不必要的部分,从而只有必需的部分保留下来;因此,大量的单词已被删除。
在如何把Objective C转化为本机Swift方面,新的API已经变得十分明智(SE-005):
//注意下面代码中第一行是老式的Swift 2形式,第二行对应新式的Swift 3形式
- let blue = UIColor.blueColor()
- let blue = UIColor.blue()
- let min = numbers.minElement()
- let min = numbers.min()
- attributedString.appendAttributedString(anotherString)
- attributedString.append(anotherString)
- names.insert("Jane", atIndex: 0)
- names.insert("Jane", at: 0)
- UIDevice.currentDevice()
- UIDevice.current()
GCD及核心图形库更新
提到顽固的老式API,GCD及核心图形库都迫切需要改造。如今,这项工作已经开始。
GCD库适用于长时间的计算或者与服务器通信这样的多线程任务。通过将活动移动到一个不同的线程,你可以防止用户界面锁定。Libdispatch库是使用C语言编写的并一直保持C风格的API。现在,在本机Swift中,该API已经被重写(SE-0088)。请参考如下代码:
- // old way, Swift 2
- let queue = dispatch_queue_create("com.test.myqueue", nil)
- dispatch_async(queue) {
- print("Hello World")
- }
- // new way, Swift 3
- let queue = DispatchQueue(label: "com.test.myqueue")
- queue.async {
- print("Hello World")
- }
类似地,核心图形库部分也是使用C语言编写的并以老风格调用(SE-0044)。如今也得以改写。请参考下面的示例代码展示:
- // old way, Swift 2
- let ctx = UIGraphicsGetCurrentContext()
- let rectangle = CGRect(x: 0, y: 0, width: 512, height: 512)
- CGContextSetFillColorWithColor(ctx, UIColor.blueColor().CGColor)
- CGContextSetStrokeColorWithColor(ctx, UIColor.whiteColor().CGColor)
- CGContextSetLineWidth(ctx, 10)
- CGContextAddRect(ctx, rectangle)
- CGContextDrawPath(ctx, .FillStroke)
- UIGraphicsEndImageContext()
- // new way, Swift 3
- if let ctx = UIGraphicsGetCurrentContext() {
- let rectangle = CGRect(x: 0, y: 0, width: 512, height: 512)
- ctx.setFillColor(UIColor.blue().cgColor)
- ctx.setStrokeColor(UIColor.white().cgColor)
- ctx.setLineWidth(10)
- ctx.addRect(rectangle)
- ctx.drawPath(using: .fillStroke)
- UIGraphicsEndImageContext()
- }
枚举类型中的大写
Swift 3中的枚举表达中使用小驼峰(lowerCamelCase,即第一个词首字符小写,其他的后续每一个词的首字母大写)表达方式,这可使之与其它属性(或者值)保持一致(SE-0006):
// 注意:下面代码中第一行是Swift 2方式,紧接着的是新式的Swift 3写法
- UIInterfaceOrientationMask.Landscape
- UIInterfaceOrientationMask.landscape
- NSTextAlignment.Right
- NSTextAlignment.right
- SKBlendMode.Multiply
- SKBlendMode.multiply
如今,大驼峰写法仅为类型和协议命名而保留。尽管习惯这一点可能还要花费一点时间,但Swift开发团队的确是在努力确保整体表达的尽可能一致性。
方法命名上的变化
标准库在方法命名方面也变得更加一致。一般的建议是,你应当根据要创建的方法的作用或者行为进行命名。经验法则是,如果方法包含一个像"-ed"或"-ing"这样的后缀,那么认为该方法是名词性的。名词性的方法会返回一个值。如果方法没有后缀,那么它很可能是命令式的动词性方法。这些"动词"方法在引用内存上执行操作;这也被称为原地修改(modifying in place)。标准库中存在好几对遵循此名词/动词约定的方法(SE-0006)。下面是其中的几个:
- customArray.enumerate()
- customArray.enumerated()
- customArray.reverse()
- customArray.reversed()
- customArray.sort() // changed from .sortInPlace()
- customArray.sorted()
下面的代码片断给出了它们的实际用法:
- var ages = [21, 10, 2] // variable, not constant, so you can modify it
- ages.sort() // modified in place, value now [2, 10, 21]
- for (index, age) in ages.enumerated() { // "-ed" noun returns a copy
- print("\(index). \(age)") // 1. 2 \n 2. 10 \n 3. 21
- }
函数类型
函数声明与函数调用从来都是要求使用小括号把它们的参数包围起来:
- func f(a:Int){…}
- f(5)
然而,当你使用函数类型作为参数时,你在Swift 2中可以这样写:
- func g(a: Int -> Int) -> Int -> Int { ... } // old way, Swift 2
你可能注意到了这种表达方式有点难懂。参数部分将在哪里结束?返回类型是从哪里开始的?在Swift 3中定义上述函数的语法是这样表达的(SE-0066):
- func g(a: (Int) -> Int) -> (Int) -> Int { ... } // new way, Swift 3
现在,你注意到了,参数列表使用小括号包围,后面跟着的是返回类型。这种表达相当清楚;当然,函数类型也更易于识别。下面的小例子提供了更有力的对比:
- // old way, Swift 2
- Int -> Float
- String -> Int
- T -> U
- Int -> Float -> String
- // new way, Swift 3
- (Int) -> Float
- (String) -> Int
- (T) -> U
- (Int) -> (Float) -> String
API扩展
尽管Swift 3的最大更新点主要集中在现有API的现代化方面,但是仍然存在很多的Swift社团在这方面进行努力工作,并且为Swift API提供许多非常有益的扩展。
访问容器类型
当你定义一个静态属性或者方法时,你常常在其上进行这样的调用:
- CustomStruct.staticMethod()
如果你在某个类型的上下文中写代码,你仍然需要包括此类型的名字来调用其相应的静态方法。为了使这种操作更清晰明了,现在你可以调用Self来取得容器类型。这里大写的Self代表self的类型,而小写的self则代表self的实例。
下面代码展示了其工作方式(SE-0068):
- struct CustomStruct {
- static func staticMethod() { ... }
- func instanceMethod() {
- Self.staticMethod() // in the body of the type
- }
- }
- let customStruct = CustomStruct()
- customStruct.Self.staticMethod() // on an instance of the type
内联序列
- sequence(first:next:)和sequence(state:next:)
都是能够返回无限序列的全局函数。如果你给它们指定一个初始值和一个可变状态,它们将自动应用闭包(SE-0094)。例如:
- for view in sequence(first: someView, next: { $0.superview }) {
- // someView, someView.superview, someView.superview.superview, ...
- }
当然,你还可以使用prefix来约束序列(SE-0045),例如:
- for x in sequence(first: 0.1, next: { $0 * 2 }).prefix(while: { $0 < 4 }) {
- // 0.1, 0.2, 0.4, 0.8, 1.6, 3.2
- }
其他杂项
l #keyPath()工作方式类似于#sselector(),这将有助于帮助程序员迅速修改强类型Api中的拼写错误。
l 现在你可以像这样使用pi:Float.pi,CGFloat.pi。在大多数情况下,编译器能够推断出相应的类型,例如:
- let circumference=2*.pi*radius(SE-0067)
l NS前缀被从一些老式基本类型上移除,现在你可以直接使用Calendar和Date,而不必再使用像NSCalendar和NSDate这样形式。
开发工具改进
Swift是一种编程语言,因此,编写Swift程序的很大一部分自然要涉及到开发环境的使用——这对于苹果开发者来说很可能是Xcode!开发环境工具的变化也必将影响你编写Swift代码的每一天。
Swift3进一步修复了编译器和IDE功能中的错误。此外,它还提高了报告错误和警告消息的精确度。如你所料,在每一个版本中,Swift运行速度都变得越来越快,这与其运行和编译方式是密不可分的:
l 通过改进字符串哈希技术,字符串字典方面达到3倍的加速
l 通过把对象从堆移动到堆栈存储,在某些情况下达到24倍的加速效率
l 编译器现在能够一次缓存多个文件(在作整个模块优化时)
l 代码大小优化技术极大地减少了Swift代码的编译后尺寸。例如,苹果公司提供的著名的演示程序Demobots(https://developer.apple.com/library/ios/samplecode/DemoBots/Introduction/Intro.html)经代码优化技术处理后其编译后尺寸减少到原来的77%
另外,值得注意的是,如今的Xcode也正在学着如何以本机Swift方式进行“思考”:
l 以前,当你右键单击像sort()这样的API对象时系统将会跳转到其所在定义,于是你会被导航到一个神秘的头文件。在现在的Xcode 8版本中,你会看到sort()成为数组的扩展对象,而这正是你所期望的。
l Swift的快照技术很像其演变过程中的Nightly发行版本一样。这为其充分融合到Xcode前提供了一种使用这种新语法的机会。Xcode 8支持在Playground中加载和运行Swift快照。
Swift包管理器
开放源码的Swift语言实际上是一个包的集合,包括语言、核心库和包管理器共三个部分。其中,Swift程序包管理器(Package Manager)为你想分享并导入到其他项目的任何Swift代码定义了一个简单的目录结构。
类似于软件包管理器,你可能习惯了Cocoapods或Carthage这样的工具,Swift的软件包管理器将下载依赖项,编译它们,并把它们链接起来以创建库和可执行文件。Swift3是包括Swift程序包管理器的第一个版本。现在有1,000多个库为之提供支持;在未来几个月里,你将会看到为之增加的更多的支持。
计划中的未来特征
前面提到,Swift3的目的是保持你的代码在新版本的不断推出中能够保持向前有良好的兼容性。虽然情况是这样,但是对于时下这个版本来说还有一些较难达成的内容,即泛型技术的引入和应用程序二进制接口(ABI)的稳定等目标。
泛型引入将包括递归协议约束以及支持约束扩展符合新协议(例如,一个Equatable元素数组应当是Equatable,等等)。在完成这些功能之前,Swift尚无法确保ABI稳定性。
ABI稳定将允许使用不同版本的Swift编译的应用程序和库能够链接到一起并彼此交互。这是在第三方库没有提供源代码时仍能够进行框架迁移必需的关键一步,因为新版本的 Swift不仅需要这些第三方库来更新他们的代码,而且能够重建其框架。
此外,ABI稳定性将使得Swift标准库不必与二进制文件一同发行,对于目前使用Xcode创建的iOS和macOS应用程序情况就是这样。现在的二进制文件被捆绑了2 MB大小的额外文件尺寸,以确保它们运行于未来的操作系统上。
现在来总结一下,你现在可以使你的源代码从版本更换到另一个版本,但编译后的二进制兼容性在从一个版本到另一个版本方面还没有实现。
小结
Swift将随着社团公众的最佳做法继续发展。虽然仍处于起步阶段,该语言已经取得了很大成功并预示着一个辉煌的未来。Swift已经成功地运行在Linux 上,而且你可能会看到它在未来的几年中将运行于除设备外的服务器上。从零开始设计一种语言当然会有其优势来打破ABI稳定性。这是一个没有带着遗憾纠正语言的独特机会。
Swift也在不断扩大其覆盖面。苹果公司正在把Swift应用于自己产品的开发方面。例如,苹果团队在ipad音乐应用程序开发,控制台开发,Sierra中的画中画开发,Xcode文档查看器和新的Swift Playground应用等方面都在广泛使用Swift语言。
说到这里,当前又有一股很大的热潮使非程序员争相学习Swift,无论在iPad开发方面还是通过教育目的方面。
如今的Swift正处在持续上升期:名字更好、代码更清晰易读,而且你还有辅助工具帮助你实现代码迁移。如果你想要更深入地挖掘Swift,你可以看看WWDC会议录像(https://developer.apple.com/videos/wwdc2016/)。
在2016年底Swift 3正式发行之时肯定会有更多的功能加入。我们将基于这里的所有更新,并继续关注新的教程、书籍公告和相关视频推出。