【译】Swift 3.0更新概要

原贴:What’s New in Swift?
作者:Ben Morrow
译者: kemchenj

Swift 3.0年末就会发布正式版, 并且会给所有Swift开发者带来很多改变.

如果你没有一直紧跟Swift Evolution的话, 你也许会想知道都有哪些改变, 以及它将会怎么影响你的代码, 并且你该什么时候开始着手把代码convert到3.0, 那这篇文章就是写给你的.

这篇文章里, 我将会着笔于那些对你的代码会有重大影响的Swift 3的新特性. 开始吧!

引言

Swift 3.0预览版在Xcode 8.0 beta已经可以使用了. 当Swift 3.0的改革已经接近尾声的时候, 接下来的几个月也许还会再接受几个提案, 功能性修改将会在2016年年末xcode发布正式版之后停止, 所以你必须等到那个时候才能把你的Swift 3应用提交到App Store

为了让开发者能够开始着手移植到Swift 3.0, Apple已经把Swift 2.3内置到Xcode 8.0里. 对开发者来说, Swift 2.3跟2.2一样, 但2.3支持WWDC上宣布的那些新的SDK和Xcode的新功能, Xcode 8.0出来之后, 你就可以提交使用Swift 2.3写的app, 而不用等到3.0才能体验到那些新的特性.

我建议尝试一下我们在playground实现的功能, 甚至可以在你的某个项目里跑一下Migration Assistant感受一下那些改变. 但Xcode 8.0和Swift 3.0还没出正式版之前, 你还不可以提交app到App sSore里, 你也许需要考虑一下等Swift 3安定下来之后再把自己的所有代码移植过去.

Swift 3的迁移工作

转到Swift 3.0的时候, 你必须意识到所有的文件都需要做修改! 这么大的改变是因为Cocoa的API名字都改了. 或者为了更加明确, API会保持原样, 但会有一个合适的名字给Objective-C和另一个给Swift. 接下里的几年里, Swift 3.0将会让Swift变得更加Natrual to write

Apple在Xcode 8.0里内置了能够帮你智能地完成大部分迁移工作的Migration Assistant. 但你还是需要点几个按钮去完成这个过程.

你立刻能迁移到Swift 2.3或者Swift 3.0, 如果你需要迁移回去2.2的话, 你随时可以到Xcode的菜单栏Edit -> Convert -> To Current Swift Syntax…迁移回去. 编译器也能像Migration Assistant一样智能, 如果你使用了旧的API的话, 编译器会提供一个Fix-I他的选项来帮助你改成正确的API.

这里面最好的消息是, Swift 3.0的目标是作为最后一个需要大量修改代码的版本存在(API将会稳定下来). 往前看的话, 你将能够Swift的版本升级过程中保持你的代码不变. 当然, Swift的核心团队没办法预知未来, 但他们承诺即使以后需要修改代码以完成版本升级的时候, 也会保持长时间的向后兼容. 语言的稳定意味着越来越多保守的公司也采用Swift.

但二进制接口稳定下来的目标还是没能达成, 文章末尾你能看到这会带来的影响

Swift Evolution计划的提案

自从Swift宣布开源之后, 社区成员已经提交了超过100个Proposal了. 大部分提案 (70个以上 )都在讨论和修改之后被接受了. 那些被驳回的提案也都经过了激烈的讨论. 最后, 由核心团队来对所有提案做最后决定.

核心团队跟社区的合作让人印象深刻. 实际上, Swift已经在Github上有了三万个star了. 每周都会有几个新的提案提交上去. 甚至连Apple自己的工程师在想要做出改变的时候也会打开Repo去写提案.

在接下里的章节里, 你会看到类似于[SE-0001]这样子的超链接. 这些是Swift Evolution的提案编号. 这些提案都已经被接受并且将会在Swift 3.0的最终版本里实现. 每个提案的链接点进去你都能看到关于每个修改的全部细节.

API修改

Swift 3.0最大的升级是标准库将会沿用之前的命名规范, API Design Guidelines包括了团队在完成Swift 3.0的时候定下来的规范, 对于新程序员有更高的可读性和易用性. 核心团队坚守”Good API Design always consider the call site”. 他们力争明确每一个使用的场景. 以下是一些马上会影响你的改变.

第一个参数的参数名

让我们先来颠覆一个你每天都在Swift里的操作吧

函数和方法的第一个参数名不会再自动省略了, 除非你用了”_”. 之前你每次调用一个方法或者函数的时候都会自动省略第一个参数的参数名.

// 第一个是 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, within这样的介词作为外部参数名(而不再放在函数或者方法名里). 这一部分的修改是为了提高可读性.

如果调用方法的时候不需要介词和外部参数名的时候, 你应该在第一个参数名前面加一个下划线_来表明:

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))")
}

现在, 变量名的改变让方法名能够更加具有一致性, 并且更容易理解

移除多余的字词

在之前Apple的库里面, 方法名通常会包含一个名字去表明返回值的类型, 但现在得益于Swift编译器的类型推断能力, 这件事情变得不再那么必要了. Swift核心团队花了很多功夫去过滤杂音, 提取提案里的真正需求, 就这样很多重复的词都去除掉了.

Objective-C和C语言库的API变得更加接近原生Swift [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, GCD和Core Graphics的语法都得到了美化

GCD被用来处理诸如耗时运算和服务器通信的多线程任务, 通过把任务移到别的线程, 你可以防止自己的用户界面线程被阻塞. libdispatch这个库是用C语言写的, 并且API也是C语言风格的. 这一套API现在被重新塑造成原生的Swift风格 [SE-0088]:

// Swift 2的写法
let queue = dispatch_queue_create("com.test.myqueue", nil)
dispatch_async(queue) {
print("Hello World")
}
 
// Swift 3的写法
let queue = DispatchQueue(label: "com.test.myqueue")
queue.asynchronously {
print("Hello World")
}

同样的, Core Graphics也是C语言写的, 并且以前都是使用很怪异的方法去调用. 新的用法就像这样 [SE-0044]:

// 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()
 
// 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代码的方式, 枚举的值现在要用首字母小写的驼峰命名法(lowerCamelCase). 这会让他们看起来跟其他Property和Values更加一致 (向Struct和Class靠拢) [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)现在只有类型(Types)和协议(Protocols). 你也许需要一些时间去适应, 这是Swift团队对于一致性的执着.

方法名: 修改 / 返回修改后的副本

标准库里方法名的动词和名词都更加具有一致性, 你可以根据action造成的效应去选择命名方式(You choose a name based on side effects or the action taken). 最重要的原则是如果方法名包含了-ed或者-ing这样的后缀的话, 那么就可以把方法当做一个名词, 一个”名词方法”将会返回一个值, 如果它不包含后缀, 那很像是一个动词, 这些”动词方法”将会在引用对象的内存里进行操作, 也就是修改对象. 这里有几对动词和名词方法遵循这样的原则 [SE-0006]:

customArray.enumerate()
customArray.enumerated()
 
customArray.reverse()
customArray.reversed()
 
customArray.sort() // 原来的写法 .sortInPlace()
customArray.sorted()

这里是一些他们的使用时的片段:

var ages = [21, 10, 2] // 这里是变量而不是常量, 所以可以修改
ages.sort() // 在这里进行修改, 当前值为 [2, 10, 21]
 
for (index, age) in ages.enumerated() { // "-ed" 后缀意味着会返回一个修改后的副本
print("\(index). \(age)") // 1. 2 \n 2. 10 \n 3. 21
}

函数类型

函数声明和调用总是会要求用圆括号()把参数包起来:

func f(a: Int) { ... }
 
f(5)

不过, 当你使用函数作为参数传入的时候, 你也许会写成这样:

func g(a: Int -> Int) -> Int -> Int { ... } // Swift 2的写法

你也许会发现这样可读性很差, 参数在哪里结束? 返回值在哪里开始? Swift 3.0里定义函数的正确方式是这样的 [SE-0066]:

func g(a: (Int) -> Int) -> (Int) -> Int { ... } // Swift 3的写法

现在参数必须被圆括号()包住, 后面跟着返回值类型. 一切都变得很清晰, 具有连贯性, 函数的类型更容易看出来. 下面一个很明显的对比:

// Swift 2的写法
Int -> Float
String -> Int
T -> U
Int -> Float -> String
 
// Swift 3的写法
(Int) -> Float
(String) -> Int
(T) -> U
(Int) -> (Float) -> String

API的补充

Swift 3.0最大的更新是现有API变得更加现代化, 有越来越多的Swift社区在致力于这件事, 包括一些额外的, 实用API

获取类型

当你定义一个static的property或者method的的时候, 你必须通过他们的Type去调用他们:

CustomStruct.staticMethod()

如果你是在一个type里写代码, 你还是需要在type里通过type名去调用static method. 为了让代码更加清晰, 你可以用Self去替代当前type的, S大写的话Self代表type本身, s小写的话self代表实例对象(instance)本身.

这里是它的具体例子 [SE-0068]:

struct CustomStruct {
static func staticMethod() { ... }
func instanceMethod()
Self.staticMethod() // 在结构体CustomStruct内部使用Self指向CustomStruct这个结构体本身
}
}
let customStruct = CustomStruct()
customStruct.Self.staticMethod() // 在结构体实例(instance)里的调用

Inline Sequences

sequence(first: next:)sequence(state: next:)是返回无限的sequence的全局方法. 你可以给他们一个初始值或者一个可变的状态, 然后他们会通过闭包进行懒加载 [SE-0094]

for view in sequence(first: someView, next: { $0.superview }) {
// someView, someView.superview, someView.superview.superview, ...
}

你可以通过prefix关键字来给sequence增加限制 [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
}

Miscellaneous Odds and Ends

  • #keypath()#selector()的工作方式很像, 能够帮助你纠正那些字符串类型的API
  • 你可以在你想要使用的类型里调用pi, 就像这样: Float.pi, CGFloat.pi. 大部分时候编译器还可以帮你推导出类型: let circumference = 2 * .pi * radius[SE-0067]
  • 在那些老的Foundation里NS开头的类已经去掉了, 现在你可以直接使用Calendar, Date而不是NSCalendarNSDate.

工具链的提升

Swift是一门语言, 编写代码的大部分工作需要开发环境, 对于Apple的开发者来说就是Xcode! 这些工具改变将会影响你每天编写Swift代码的方式.

Swift 3修复了编译器和IDE的bug. 也提高了error和warning信息的可读性. 就像你所预想的, 每一次更新, Swift在运行和编译的时候都会变得更快:

  • 字典里字符串的hashing速度是以前的三倍
  • 把对象从移到的速度是以前的24倍(某些情况下)
  • 编译器可以一次性缓存多个文件(在整个模块优化的时候)
  • 代码大小优化可以减少Swift代码编译后的大小. Apple的demo Demobots把能够把渲染后的大小变为原来的77%

Xcode也学会了如何去解构原生Swift:

  • 当你右键点击某个API的方法, 例如sort()的时候就会跳到它的定义, 以前你会被带到一个晦涩难懂的头文件里. 但现在, Xcode 8, 如你预想的, 你会看到的sort()会是Array的一个拓展
  • Swift Snapshots会像Swift Evolution的Nightly Builds(每天编译一次的最新版本). 它提供了一个机会去在完全加入Xcode之前测试那些新的语法, Xcode 8可以在playground里加载和运行Swift Snapshots

Swift包管理器

开源的Swift实际上包括了语言本身, 核心库, 和包管理器这些repo. 这些套件的组合构成了我们认识的那个Swift. Swift Package Manager定义了一个简单的路径结构, 能够把代码让你分享和导入项目里

类似于你可能用过的Cocoapods或者Carthage一样的包管理器, Swift的包管理器可以下载和编译依赖, 并且自动把他们链接到一起去创建库或者可执行程序. Swift 3是第一个包含包管理器的版本, 已经有超过1000个库支持这个功能, 并且在之后的几个月里, 你会看到它的更多种形式

未来的计划

就像之前所说的, Swift 3的目标是让你能够保持你的代码能够在以后版本里稳定下来, 避免未来会有破坏性的改变. 虽然如此, 但还是有些相关的重要目标在这个版本里没有完成, 泛化和ABI兼容.

泛化(Generics)还有递归协议约束, 以及新协议对于拓展(Extesion)的约束.(例如: 两个具有相同元素的Array是无法判断是否相等的Euqaltble). 在这部分工作完成之前, Swift是不能完成ABI兼容的.

ABI兼容可以让不同版本编译出来的程序和库能够链接到一起并且进行交互. 对于第三方库来说这是很重要的, 它们可以直接把自己的库分享出去而不必连同代码也一并提供, Swift的升级的时候, 这些第三方库也不必升级代码以及重新build框架.

另外, 二进制接口的稳定可以让程序不再需要连同标准库一起打包, 就像现在发生在Xcode编译出来的iOS和macOS程序一样, 现在这些二进制可执行程序全都包括了2MB左右的Swift标准库以保证他们能够在以后的操作系统里也能运行.

总结起来, 你现在可以让你的代码在版本升级的时候保持稳定, 不必做修改, 但编译出来的二进制可执行程序则可能还是会出现不兼容的状况.

之后的走向

在社区的努力下Swift还在一直进化中. 现在还是Swift的萌芽期, 这门语言拥有无限的潜能和美好的未来. Swift已经可以在Linux上运行, 再过几年我们也许很可能就可以看到它在服务器上跑起来. 设计一门语言, 对于语言的修改肯定会有它的好处和破坏接口稳定的可能性, 但当它稳定下来之后, 你会发现一切都是值得的, 这是一次难得的机会可以让这门语言变得更好.

Swift的影响力也慢慢地在扩大, Apple is eating their own dogfood (Angular的梗, 讽刺Google不用自己家出的Angular JS), 苹果的团队已经在Music, Console, Sierra的画中画, Xcode的文档浏览器和新的Swift Playground的iPad版本上用了Swift了.

对于非程序员人群在iPad上学习Swift和通识教育有极大的推动作用.

简单来说, Swift还在持续上升期: 命名变得更好, 代码更清晰, 并且你也有工具能够帮助完成迁移, 如果你还想了解更多, 可以看WWDC的视频