原文:What’s New in Swift 3.1?
作者:Cosmin Pupaza
译者:kmyhy重要消息:Xcode 8.3 以及 Swift 3.1 现在推出了 Beta 版!这个版本包含了期待已久的 Swift 包管理器以及语言自身的一些改进。
如果你最近没有关注过 Swift 路线图,请接着阅读——这篇文章正是你需要的!
在本文,我将集中介绍 Swift 3.1 的重要改变,这些都会对你的代码产生重大影响。让我们开始吧:]
开始
Swift 3.1 和 Swift 3.0 保持源代码兼容,因此新特性不会破坏你的代码,如果你的项目已经用 Xcode 的 Edit\Convert\To Current Swift Syntax… 迁移到 Swift 3.0 的话。但是,苹果已经在 Xcode 8.3 中终止了对 Swift 2.3 的支持。因此如果你还没有从 Swift 2.3 迁移过来,现在你就需要去迁移了。
在后面的内容中,你会看到一些类似 [SE-0001] 一样的标签。这些是 Swift 路线图的提案编号。我引用了每个提案编号,这样你可以了解每个改变的完整细节。我建议你在 playground 中测试这些特性,这样你就会对每个改变有更深刻的理解。
打开 Xcode,选择 File\New\Playground…。选择 iOS 平台,命名随意,保存位置随意。在阅读本文的过程中,这个 Playground 会用于测试每个特性。
注意:如果你需要复习一下 Swift 3.0 的内容,请阅读我们的What’s New in Swift 3。
语法改进
首先,来看一眼这个版本的语法改进,包括:数值类型的失败初始化、新的sequence 函数等等。
允许失败的数值转换的初始化方法
Swift 3.1 实现了对所有数值类型(Int,Int8,Int16,Int32,Int64,UInt,UInt8,UInt16,UInt32,UInt64,Float,Float80,Double)的允许失败的初始化方法,要么成功不丢失任何信息,要么简单返回 nil。[SE-0080]
这个特性是有用的,例如,可以以一种能够恢复的方式,将来自于外部源的松散类型数据转换成安全类型。例如,你用这样的方式处理一个关于学生的 JSON 数组:
class Student {
let name: String
let grade: Int
init?(json: [String: Any]) {
guard let name = json["name"] as? String,let gradeString = json["grade"] as? String,let gradeDouble = Double(gradeString),let grade = Int(exactly: gradeDouble) // <-- 3.1 feature here
else {
return nil
}
self.name = name
self.grade = grade
}
}
func makeStudents(with data: Data) -> [Student] {
guard let json = try? JSONSerialization.jsonObject(with: data,options: .allowFragments),let jsonArray = json as? [[String: Any]] else {
return []
}
return jsonArray.flatMap(Student.init)
}
let rawStudents = "[{\"name\":\"Ray\",\"grade\":\"5.0\"},{\"name\":\"Matt\",\"grade\":\"6\"},{\"name\":\"Chris\",\"grade\":\"6.33\"},{\"name\":\"Cosmin\",\"grade\":\"7\"},{\"name\":\"Steven\",\"grade\":\"7.5\"}]"
let data = rawStudents.data(using: .utf8)!
let students = makeStudents(with: data)
dump(students) // [(name: "Ray",grade: 5),(name: "Matt",grade: 6),(name: "Cosmin",grade: 7)]
在 Student 类的允许失败的指定初始化方法中,你使用了一个允许失败的初始化方法,将 grade 属性从 Double 转换成 Int:
let grade = Int(exactly: gradeDouble)
如果 gradeDouble 是一个小数值比如 6.33,它会转换失败。如果它是一个整数字,比如 6.0,它会转换成功。
注意:另一种方法是在初始化方法中抛出异常来替代允许失败的初始化方法。社区觉得允许失败的初始化方法更好,更符合工效学设计。
新的 Sequence 函数
Swift 3.1 新增了两个函数用于数据过滤,它们被添加到标准库的 Sequence 协议中:prefix(while:) 和 drop(while:) [SE-0045]。
以斐波那契无限序列为例:
let fibonacci = sequence(state: (0,1)) {
(state: inout (Int,Int)) -> Int? in
defer {state = (state.1,state.0 + state.1)}
return state.0
}
在 Swift 3.0 中,你可以简单地指定迭代次数,来产生斐波那契序列:
// Swift 3.0
for number in fibonacci.prefix(10) {
print(number) // 0 1 1 2 3 5 8 13 21 34
}
Swift 3.1 允许你使用 prefix(while:) 和 drop(while:) ,它们带有一个条件参数,允许获得位于两个值之间的完整序列:
// Swift 3.1
let interval = fibonacci.prefix(while: {$0 < 1000}).drop(while: {$0 < 100})
for element in interval {
print(element) // 144 233 377 610 987
}
prefix(while:) 返回满足指定条件的最大序列。这个序列从序列的起始值开始,当闭包返回 false 时的序列截止。
drop(while:) 则相反: 它返回一个子序列,当该序列中的元素不满足闭包中指定的条件时,子序列开始,直到序列的最后一个值。
注意:你可以将闭包语法简化为:
let interval = fibonacci.prefix{$0 < 1000}.drop{$0 < 100}
具化带约束的扩展
Swift 3.1 允许你用一个具体的类型约束来扩展泛型。在此之前,你无法扩展这种类型,因为约束肯定是一种协议。让我们来看一个例子。
例如,RoR 提供了一个非常有用的 isBlank 方法,用于检查用户输入。用 Swift 3.0 你可以这样实现它,在 String 类型的扩展中用一个计算属性:
// Swift 3.0
extension String {
var isBlank: Bool {
return trimmingCharacters(in: .whitespaces).isEmpty
}
}
let abc = " "
let def = "x"
abc.isBlank // true
def.isBlank // false
如果你想在一个 Optional 的 String 上实现 isBlank 计算属性,在 Swift 3.0 中你可以这样做:
// Swift 3.0
protocol StringProvider {
var string: String {get}
}
extension String: StringProvider {
var string: String {
return self
}
}
extension Optional where Wrapped: StringProvider {
var isBlank: Bool {
return self?.string.isBlank ?? true
}
}
let foo: String? = nil
let bar: String? = " "
let baz: String? = "x"
foo.isBlank // true
bar.isBlank // true
baz.isBlank // false
这里声明了一个 StringProvider 协议用于给 String 进行实现。然后你扩展了 Optional 类型,同时要求 wrapped 类型为 StringProvider,并在其中添加了 isBlank 方法。
Swift 3.1 允许你扩展具体的类型,而不是协议:
// Swift 3.1
extension Optional where Wrapped == String {
var isBlank: Bool {
return self?.isBlank ?? true
}
}
这提供了同样的功能,需要编写的代码变少了!
嵌套的泛型
Swift 3.1 允许你将嵌套类型和泛型混用。设想一下(很疯狂)这个例子。比如 raywenderlich.com 某个团队的 lead 想发表一篇博客,他让一队专门的程序员去实现,以便满足网站的优质标准:
class Team<T> {
enum TeamType {
case swift
case iOS
case macOS
}
class BlogPost<T> {
enum BlogPostType {
case tutorial
case article
}
let title: T
let type: BlogPostType
let category: TeamType
let publishDate: Date
init(title: T,type: BlogPostType,category: TeamType,publishDate: Date) {
self.title = title
self.type = type
self.category = category
self.publishDate = publishDate
}
}
let type: TeamType
let author: T
let teamLead: T
let blogPost: BlogPost<T>
init(type: TeamType,author: T,teamLead: T,blogPost: BlogPost<T>) {
self.type = type
self.author = author
self.teamLead = teamLead
self.blogPost = blogPost
}
}
在外部类 Team 中嵌套了 BlogPost 内部类,同时两个类都是泛型化的。Team 要查找我在网站上发布的文章和教程时要怎么做:
Team(type: .swift,author: "Cosmin Pupăză",teamLead: "Ray Fix",
blogPost: Team.BlogPost(title: "Pattern Matching",type: .tutorial,
category: .swift,publishDate: Date()))
Team(type: .swift, blogPost: Team.BlogPost(title: "What's New in Swift 3.1?",type: .article, category: .swift,publishDate: Date())) 实际上,这段代码可以简化。如果嵌套的内部类型使用了外部泛型类的类型,它会继承父类的类型。因此并不需要声明这个类型,这样: ```swift class Team<T> { // original code class BlogPost { // original code } // original code let blogPost: BlogPost init(type: TeamType,author: T,teamLead: T,blogPost: BlogPost) { // original code } } <div class="se-preview-section-delimiter"></div>
注意:如果你了解更多 Swift 泛型,请阅读我们最近更新的教程getting started with Swift generics。
检测 Swift 版本
你可以用 #if swift(>= N) 静态结构来检测 Swift 版本是否是某个特定的版本。
// Swift 3.0
<div class="se-preview-section-delimiter"></div>
#if swift(>=3.1)
func intVersion(number: Double) -> Int? {
return Int(exactly: number)
}
<div class="se-preview-section-delimiter"></div>
#elseif swift(>=3.0)
func intVersion(number: Double) -> Int {
return Int(number)
}
<div class="se-preview-section-delimiter"></div>
#endif
<div class="se-preview-section-delimiter"></div>
但是,这种方式用在某些地方比如 Swift 标准库时有一个很大的缺点。它需要编译编译每个标准库和每个所支持的老的语言版本。因为你运行的 Swift 编译是是以向后兼容模式运行的,例如你想在 Swift 3.0 下使用,就需要用这个版本的标准库编译这个兼容版本。如果你在用 3.1 的标准库中用,这段代码将不正确。
因此,Swift 3.1 扩展了 @available 属性,以支持某个 Swift 版本号以及存在的平台版本。
// Swift 3.1
@available(swift 3.1)
func intVersion(number: Double) -> Int? {
return Int(exactly: number)
}
@available(swift,introduced: 3.0,obsoleted: 3.1)
func intVersion(number: Double) -> Int {
return Int(number)
}
<div class="se-preview-section-delimiter"></div>
这个新特性提供了同样的功能,也就是说某个 intVersion 方法只会在对应的 Swift 版本下有效。此外它还允许向标准库这样的库只需要编译一次。编译会为指定兼容的版本选择对应的功能。
注意:如果你想了解更多 availability 属性的内容,请阅读我们的教程availability attributes in Swift。
将非逃逸闭包转换为逃逸闭包
Swift 3.0函数的闭包参数默认是非逃逸闭包 [SE-0103]。但是,这个提议那时并没有实现。在 Swift 3.1,你可以临时性地将非逃逸闭包转换成逃逸闭包,使用一个新的 withoutActuallyEscaping() 函数。
为什么要这样做?这种做法不太常用,但可以参考一下提议中的例子。
func perform(_ f: () -> Void,simultaneouslyWith g: () -> Void,on queue: dispatchQueue) {
withoutActuallyEscaping(f) { escapableF in // 1
withoutActuallyEscaping(g) { escapableG in
queue.async(execute: escapableF) // 2
queue.async(execute: escapableG)
queue.sync(flags: .barrier) {} // 3
} // 4
}
}
<div class="se-preview-section-delimiter"></div>
这个函数同时执行了两个闭包,然后在两者都完成后返回。
f 和 g 是非逃逸闭包,它们被转换成了 escapableF 和 escapableG。
async(execute:) 函数用到的是逃逸闭包。幸好已经通过前面的步骤获得了逃逸闭包。
通过 sync(flags:.barrie),你可以确保 async(execute:)方法已经完成,闭包不会在后面调用。
作用域决定了 escapableF 和 escapableG 的使用。
如果你想将临时的逃逸闭包保存(比如真的 escaped 它们),它会是一个 bug。在未来的标准库版本中,标准库可能会在你试图调用它们时进行检查并捕获。
Swift 包管理器升级
哈,期待已久的包管理器来了!
允许编辑的 Package
在 Swift 3.1 中,包管理器中添加了一个新概念,允许编辑的包。[SE-0082]
swift package edit 命令允许用一个已有的包为参数,然后将它转换成可编辑的包。可编辑的包会替换所有传统包在依赖图谱中的出现的地方。用 –end-edit 命令将可编辑包转换回传统 resolved 的包。
Version Pinning
Swift 3.1 在包管理器中添加了一个新概念,版本植入,将包依赖植入到指定版本[SE-0145]。pin 命令用于植入一个或所有依赖:
$ swift package pin --all // pins all the dependencies
$ swift package pin Foo // pins Foo at current resolved version
$ swift package pin Foo --version 1.2.3 // pins Foo at 1.2.3
<div class="se-preview-section-delimiter"></div>
unpin 命令转回原有的包版本:
$ swift package unpin —all
$ swift package unpin Foo
<div class="se-preview-section-delimiter"></div>
包管理器将 activie 版本的每个包的 pin 信息保存在 Packege.pins 中。如果这个文件不存在,包管理器自动根据 package manifest 中的必要信息创建文件,同时自动 pinning。
其它
swift package reset 命令将包重置为干净状态,去掉依赖检查或编译文件。
swift test –parallel 命令执行并行测试。
杂项
Swift 3.1 中还有几个小改变,并不太常见:
多次返回函数
C 语言中能够返回两次的函数比如 vfork 和 setjmp 是无效的。它们以一种有趣的方式改变持续的控制流。因此 Swift 社区觉得应当禁止它们的使用,并引发一个编译时错误。
禁用 Auto-Linking
Swift 包管理器禁用了 C 语言 target 中的模块映射的 auto-linking 功能:
// Swift 3.0
module MyCLib {
header “foo.h" link “MyCLib"
export *
}
// Swift 3.1
module MyCLib {
header “foo.h”
export *
}
结束
Swift 3.1 完善了 Swift3.0 的功能,并准备将更激烈的改变方到今年稍晚的 Swift 4.0 中去。包括对泛型的改进、正则式、更符合工效学的字符串设计等等。
如果你觉得意犹未尽,请看一下Swift standard library diffs 或者官方的 Swift CHANGELOG,那里会有更多的关于这次修改的信息。或者,你可以继续等待 Swift 4.0 的到来!
如果你真的想知道 Swift 4 以后还会有什么变化,请访问 Swift 演进计划,在那里你可以获得当前正在提出的计划。如果急切地想知道对为什么某个正在评审中的提议迟迟得不到回复, 你甚至可以提出自己的提议 ;]
你喜欢或者不喜欢 Swift 3.1 的什么特性?请在下面留言。