Swift 3 新特性
原文:What's New in Swift 3? ,作者:Ben Morrow,译者:kmyhy
Swift 3将于今年下半年推出,为Swift开发者们带来了很多核心代码的改变。如果你没有关注过 Swift Evolution 项目,你可能会好奇Swift 3中有什么改变,它会对你的代码带来什么影响,以及何时可以将代码移植到Swift 3。本文就将为你答疑解惑!
本文中,我将重点介绍 Swift 3 中的重大改变,以及这些改变对你的代码的深刻影响。让我们开始吧!
开始
目前,Swift 3 预览版仅在 Xcode 8 beta版中可用。在未来几个月中,Swift 3 仍然在不断改变中,它还会发生一些变化。在 2016 年末,Xcode GM 发布时,Swift 3 的新特性才会稳定。所以各位开发者要沉住气,直到那个时候才可以向 App Store 提交用 Swift 3 编写的 App。
为了便于开发者们将代码迁移到 Swift 3,苹果在 Xcode 8 中增加了一个 Swift 版本,即 Swift 2.3,对 Swift 作了一个小小的升级。如果你是一个开发者,Swift 2.3 和 2.2 实际并无不同,但 Swift 2.3版本能够支持本次WWDC中提到的各种新的SDK 和 Xcode 特性。在 Xcode 8 推出 beta 版时,你可以用 Swift 2.3 来提交你的 app,而不用将代码迁移到 Swift 3。
我建议你在 Playground 中测试本文讨论的新特性,还可以用你的某个项目来测试 Migration Assistant,以便你对这些改变有所了解。由于在 Xcode 8 beta 下一个版本及 Siwft 3 正式发布之前,你无法向 App Store 提交 App,那么我建议暂时先不要讲代码迁移到Swift 3。
升级到 Swift 3
升级到Swift 3时,你会发现基本上每个文件都需要改动!之所以这样,是因为所有的 Cocoa API 名称都改变了。简而言之,API还是原来的 API,但这个 API 在 Objective-C 中是一种叫法,而在 Swift 中是另一种叫法。Swift 3的语法书写起来要更贴近于自然语言。
在Xcode 8中,苹果提供了Migration Assistant,它可以完成大部分的迁移工作。当然,仍然有一部分工作是需要你手动完成的。
你可以立即将代码升级到 2.3 或者 3.0。如果你需要将代码又转回来,你可以用 Xcode 的 Edit > Convert > To Current Swift Syntax… 菜单。编译器会和 Migrateion Assistant 一样智能。如果你在调用方法时,偶然使用了老的 API,编译器会显示一个 Fixt-It 选项,让你使用正确的新 API。幸好 Swift 3 在最终发布时,才会停止改变源代码。因此,你可以将你的 Swift 代码保存为不同的版本。但是 Swift 核心团队不能保证这一点以后不会改变,如果在某个时候不在保证源码上的兼容,他们会提供一个较长的过渡期。这意味着源码是稳定的,这样能鼓励更多的保守的公司去使用它。
这也说明,二进制稳定的目标还没有达到。本文最后将讨论这将导致的影响。
已实现的 Swift Evolution 提议
自从 Swift 开源以来,社区成员已经提交了超过 100 个改进建议。其中大部分(70多个)提议经过讨论修改之后已经被接受。还有一些仍然在激烈的争论之后被拒绝了。但最终,所有提议都要经过核心团队的最终拍板。
核心团队和广大社区之间的合作是卓有成效的。事实上,Swift 在 Github 上获得了 30,000 多颗星。每周都会有新的提议提交,周周如此。甚至苹果的工程师也会在 Github 上提交他们的提议。
在下一节中,你会看到一些类似 [SE-0001] 这样的标注。这是已经接受的提议编号,并且将在最终版的 Swift 3.0 中进行实现。每项提议都会标出,以便你查看每项改变的具体细节。
API 的改变
Swift 3 中最大的改变是标准库中在每个库中都采用了统一命名方式。API Design Guidleines中包含了这些规则,核心团队在构建 Swift 3 时采用了这些规则,对新手来说,这高度增强了可读性和易用性。核心团队遵循的是"好的 API 设计应当总是从调用者的角度看待问题"的原则。他们努力让 API 简单易用。不再多说,让我们开始介绍这些对你来说非常重要的改变。
第一个参数的 label
我们以一个开发者每天都会在 Swift 中用到的例子开始。
在函数或方法中的第一个参数现在必须有一个 label ,除非你显式地声明不要。以前,我们调用一个函数或方法时,可以忽略第一个参数的 label[SE-0046]:
// old way, Swift 2, followed by new way, 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"作为外部参数名。这是为了增加代码的可读性。
如果这个方法不使用介词也不使用 label,你应该在方法定义时,显式地在第一个参数名之前加一个下划线:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... } override func didMoveToView(_ view: SKView) { ... }
在许多编程语言中,许多方法可以共用一个方法名,但参数名不同。Swift 也不例外,现在,你可以重载方法,APIs 能够将直接将它们转换成合适的调用。下面是一个例子,展示了 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-0005]:
// old way, Swift 2, followed by new way, 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 和 Core Graphics
一提到遗留下来的“元老级”API,GCG 和 Core Graphics 更需要被重新“装扮一新”。
Grand Central Dispatch 常用于长时间计算或者与服务器通讯。将任务放到不同的线程,你可以避免阻塞用户界面。libdispatch 库是用 C 语言编写的,提供了 C 风格的 API。这个 API 现在在 Swift 中被重新设计为[SE-0088]:
// old way, Swift 2, followed by new way, 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()
类似的情况还有 Core Graphics。Core Graphics 是用 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() }
枚举中 case 值的大小写
另一个和过去的 Swift 代码不同的地方是,在枚举中定义的 case 值现在使用小驼峰命名法。这是为了和属性名或者变量名保持一致[SE-0006]:
//old way, Swift 2, followed by new way, Swift 3 UIInterfaceOrientationMask.Landscape UIInterfaceOrientationMask.landscape NSTextAlignment.Right NSTextAlignment.right SKBlendMode.Multiply SKBlendMode.multiply
UpperCamelCase命名法现在只在类型名和协议名上使用。当你习惯这一切之后,Swift团队对于追求一致性的努力才没有白费。
返回值的方法或者修改值的方法
标准库中对方法名中使用动词和名词的规定也更加统一。你应当根据这个方法会导致什么后果或者要采取一些动作来进行方法命名。首要原则是如果这个方法名中包含"ed"或"ing"后缀,则表明这是一个名词。方法名为名词的方法有返回值。如果不包含这些后缀,则很可能这是一个动词。以动词命名的方法会对某块引用的内存进行一些操作。即所谓的"修改某个值"。下面是几个符合名词/动词命名规则的方法[SE-0006]:
customArray.enumerate() customArray.enumerated() customArray.reverse() customArray.reversed() customArray.sort() // changed from .sortInPlace() customArray.sorted()
下面是一些使用这些方法的代码片段:
var ages = [21, 10, 2] // 变量,不是常量,这样你才能修改它 ages.sort() // 修改值,现在值变成了 [2, 10, 21] for (index, age) in ages.enumerated() { // "-ed" 是名词,表示会返回一个 ages 拷贝 print("\(index). \(age)") // 打印:1. 2 \n 2. 10 \n 3. 21 }
函数类型
函数在声明和调用时,都需要用括号将参数括住:
func f(a: Int) { ... } f(5)
但是,当你用函数类型作为参数时,你可能会写出这样的代码:
func g(a: Int -> Int) -> Int -> Int { ... } // old way, Swift 2
你会发现代码很难读懂。参数在哪里结束,返回值从哪里开始?在 Swift 3 中,正确的定义方法是[SE-0066]:
func g(a: Int -> Int) -> Int -> Int { ... } // old way, Swift 2
现在,参数列表被括号包裹,然后才是返回类型。事情变得简单,同时函数类型更容易被识别出来。通过下面的比照,你会更清楚:
// 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中新增功能
除了对已有 API 进行“旧瓶新装”这个最大的改变以外--有非常多的 Swift 社区正致力于此,也有对 Swift API 的一些功能上的增加。
访问所属类型
当你定义一个静态属性或方法时,你直接通过类或类型来调用它们:
CustomStruct.staticMethod()
如果你当前正在编写类型内部的代码,还是要使用类型名来调用静态方法。为了使表述更加清晰,现在你可以通过 Self 来引用当前实例所属类型。S 为大写的 Self 表示引用当前实例的类型,而 s 为小写的 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
行内Sequences
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 }
杂项
-
keyPath() 等同于 #selector() ,帮助你减少输入错误
-
你可以在某些类型上调用 pi,比如:Float.pi、CGFloat.pi。大部分时候编译器能够推断出类型:let circumference = 2 * .pi * radius [SE-0067]
-
NS 前缀从老的 Foundation 类型中移除,现在可以用 Calendar、Date来替代 NSCalendar、NSDate 了
工具的改善
Swift 只是一门语言,大部分时候你都无法离开书写它的开发环境--对于苹果开发者来说,也就是 Xcode!工具上的改变影响着每天编写代码的方式。
Swift 3 修正了编译器和 IDE 中的 Bug,还改善了报告错误和信息的精确性。就像你所期望的,每个版本的发布都会让 Swift 和编译器的运行变得更快:
-
改善了字符串的 Hash 算法,导致在将字符串存入字典后,性能有 3 倍的提升
-
将对象从堆移到栈中存放,导致性能有 24 倍的提升(某些情况下)
-
编译器可以一次缓存多个文件(在整个模块被优化过的情况下)
-
优化了代码的大小,导致 Swift 代码的编译尺寸更小。以苹果的 Demobots 为例,编译尺寸缩减了原大小的 77%。
Xcode 也会更加智能地理解 Swift 代码:
-
过去,当你在一个 API 方法比如 sort() 上右击并跳转到定义时,你会看到一个不太容易理解的头文件。现在,在 Xcode 8 中,你会看到 sort() 方法实际上是 Array 类的扩展。
-
Swift Snapshots就如今晚发布的 Swift Evolution 所说。在完全合并到 Xcode 之前,Snapshots 提供了一个使用新语法的机会。Xcode 8 能够在 playgournd 中加载和运行 Swift Snapshots。
Swift 包管理器
开源后的 Swift 实际上包含了 Swift 语言本身、核心库和包管理器。这三者一起构成了我们所见到的 Swift。Swift 包管理器定义了一个简单的目录结构,其中包括了你所共享和导入到项目中的 Swift 代码。
类似于你所用过 Cocoapods 或 Carthage 这些包管理器,Swift 包管理器会下载依赖项并编译它们,把它们 link 成库或可执行文件。已经有 1000 个库支持 Swift 包管理器,并且在未来几个月中,还会有更多的库支持它。
计划中的特性
前面说过,Swift 3 尽量避免不兼容的改变,从而使你的代码能够从一个版本与下一个版本兼容。如果这是真的,那么在这次发布中应该还有一些更远大的目标未能达到,即泛型增强(generic additions)和 ABI(程序二进制接口)稳定。
泛型增强包括递归协议约束和能够将一个受约束的扩展符合新协议(例如,一个用于存放 Equatable 对象的数组也应当遵守 Equatable 协议)。在这些特性尚未实现之前,Swift 不能被视作 ABI 稳定的。
ABI 稳定要求应用程序和库无论使用哪个版本的编译器编译,都能被 link 和能够彼此进行交互。这对于第三方库来说是个重要的进步,因为这样就可以不需要提供源码就可以封装出框架了,而现在新版本的 Swift 不但要这些第三方库修改代码,还要重新进行编译。
此外,如果 ABI 稳定就可以移除包含在二进制文件中的 Swift 标准库,因为就目前来说 iOS 和 macOS APP都是用 Xcode 创建的。目前的二进制文件中包含有 2 MB 左右的文件大小是为了确保能够兼容未来的操作系统。
总之,你现在只能保持源代码的版本兼容,而二进制版本的兼容当前仍然是不可能的。
结尾
Swift 仍然在不断演进,因为社区的目标是最佳实践。虽然它还很年轻,但它同时也面临着无比巨大的机遇和美好未来。Swift 现在已经能够在 Linux 上运行了,在未来几年,你还会看见它在更多的服务器和设备上运行。从头设计一门语言必然会破坏 ABI 稳定性,一旦等它稳定后这种破坏就变得小多了。这(开源)是唯一能够让这门语言变得更好的机会,否则我们必将后悔莫及。
Swift 正在扩大它的影响力。苹果正在身体力行。苹果的团队们正在 Music app、Console、Sierra画中画、Xcode 文档查看器、新的 iPad 版的Swift Playground app 中使用 Swift。
说到这里,苹果好像有一种让非程序员学习 Swift 意图,例如可以在 iPad 上使用 Swift 以及某些教育措施都表明了这一点。
另外,Swift 正在持续改进:命名变得更规范,代码读起来更清晰,你可以用工具来迁移代码。如果你想进一步深入了解,请看WWDC 的会议视频。
可以肯定的是,2016年末 Swift 3 的正式发布必将令人期待。我们将持续跟进所有改变,请继续关注我们的教程、新书预告和视频,我们将开始使用这些令人兴奋的改进。