What's New in Swift4?

翻译自Raywenderlich的文章:What’s New in Swift 4?
Swift 4 是Swift下一个大版本更新,预计在2017年秋推出Beta版。其重点在与Swift3的兼容性和ABI稳定性。
本文重点介绍一些这个版本中显著影响你代码的变化。好了,让我们开始吧。

Getting Started

Swift 4包含在Xcode 9。你可以从苹果的developer portal下载最新的Xcode9(需要一个有效的开发者账号)。每个Beta版的Xcode 都会捆绑最新的Swift 4 快照。

在你阅读时,你会注意到[SE-xxxx]格式的链接,这些链接将带你进去相关的Swift Evolution提案。如果你想获得很多和主题相关的信息,请务必点开它们。

我推荐在Playground中尝试Swift 4 的新特性和更新,这有助于在脑海中巩固知识,并使你深入每个主题。把玩这些例子,并尝试扩展/打断他们。玩得开心。

注:这篇文件将在为每个Bate版的Xcode更新,如果你使用的是不同的Swift快照,这些代码不保证可以运行。

Migrating to Swift 4

从Swift 3到 4 的迁移比 Swift 2.2 到 3 少了很多麻烦,一般来说,绝大多数的改变都是附加的,并不需要大量的人为修改。因此,Swift 迁移工具为你处理多数的改变。
Xcode 9 同时支持 Swift 4 和 Swift 3.2。如果需要,你可以逐个指定项目中的每个 target 使用 Swift 3.2 还是 Swift 4。 迁移到 并不是完全不受限制的,你可能需要重新部分代码来兼容修的 SDK .而且由于 Swift ABI 仍然尚未稳定所以你需要用 Xcode 9 重新编译你依赖。
当你准备迁移到 Swift 4 , Xcode 9 再次提供一个工具来帮助你,你可以点击Edit/Convert/To Current Swift Syntax…来呼出转换工具。
选择你想要转换的 targets 后,Xcode 将提示你 Objective-C 推导中的偏好, 选择推荐的选项通过限制推断来减少你的二进制文件的大小。(关于这个的主题的更多信息,参阅Limiting @objc Inference).

为了更好地理解你代码中预期的变化,我们将首先介绍Swift 4 中变更的API。

API Changes

在跳转到 Swift 4 加入的新特新前,让我们首先来看一下它对现有的 API 有哪些改成/改善。

String

Swift 4 中的 String 获得了很多应得的称赞。这个提案包含大量的改变,让我们来分解下[SE-0163]

怕你怀旧,strings 再次变为集合像之前的 Swift2.0 一样。这个改变使你不在依赖 characters 数组,现在你直接遍历 String 对象 :

1
2
3
4
let galaxy = "Milky Way 🐮"
for char in galaxy {
print(char)
}

不仅仅是循环,你还可以获得 SequenceCollection 所有附加功能;

1
2
3
4
5
6
7
8
9
10
galaxy.count       // 11
galaxy.isEmpty // false
galaxy.dropFirst() // "ilky Way 🐮"
String(galaxy.reversed()) // "🐮 yaW ykliM"

// Filter out any none ASCII characters
galaxy.filter { char in
let isASCII = char.unicodeScalars.reduce(true, { $0 && $1.isASCII })
return isASCII
} // "Milky Way "

ASCII 的例子演示了对 Character 一个小改进,现在你可以从 Character 直接访问 UnicodeScalarView,以前你需要实例化一个新的 String[SE-0178]

还有一个附加的时候 StringProtocol,
它声明以前在 String 上声明的大部分功能。这种变化的原因是为了改善切片工作。Swift 4 在 String 中增加了 Substring 类型,继承自 StringProtocol

StringSubstring 都实现了 StringProtocol,这是两者几乎拥有相同的功能。

1
2
3
4
5
6
7
8
9
10
 // Grab a subsequence of String
let endIndex = galaxy.index(galaxy.startIndex, offsetBy: 3)
var milkSubstring = galaxy[galaxy.startIndex...endIndex] // "Milk"
type(of: milkSubstring) // Substring.Type

// Concatenate a String onto a Substring
milkSubstring += "🥛" // "Milk🥛"

// Create a String from a Substring
let milkString = String(milkSubstring) // "Milk🥛"

另一项改进是 String 对字形簇的解读,这个决议来自于Unicode 9的改编。在这之前,多码点(multiple code points)构成的 unicode 字符的 count 结果大于1,这通常发生在选定肤色的emoji表情,下面是一些前后对比:

1
2
3
"👩‍💻".count // Now: 1, Before: 2
"👍🏽".count // Now: 1, Before: 2
"👨‍❤️‍💋‍👨".count // Now: 1, Before, 4

这只是[String Manifesto]的一部分,你可以阅读预期改动的原始动机和被提议后的答复。

Dictionary 和 Set

至于 Collection 类型,SetDictionary 不是很直观,幸运的是,Swift 团队给了它们很多必要的关照 [SE-0165].

Sequence Based Initialization

第一个是可以创建一个dictionary 从序列的键值对(元组):

1
2
3
4
5
6
let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B", "Barnard's Star", "Wolf 359"]
let nearestStarDistances = [4.24, 4.37, 4.37, 5.96, 7.78]

// Dictionary from sequence of keys-values
let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances))
// ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Barnard's Star": 5.96]

Duplicate Key Resolution

现在你可以用喜欢的方式复制 keys 实例化一个 dictionary,这使我们不需要写繁琐的键值对关系语句:

1
2
3
4
5
// Random vote of people's favorite stars
let favoriteStarVotes = ["Alpha Centauri A", "Wolf 359", "Alpha Centauri A", "Barnard's Star"]

// Merging keys with closure for conflicts
let mergedKeysAndValues = Dictionary(zip(favoriteStarVotes, repeatElement(1, count: favoriteStarVotes.count)), uniquingKeysWith: +) // ["Barnard's Star": 1, "Alpha Centauri A": 2, "Wolf 359": 1]

上面的代码使用了 zip 和 缩写 + 处理重复的 keys 和冲突的 values 。

注: 如果你不熟悉 zip 你可以快速的在Apple的 [Swift Documentation] 学习它。

Filtering

DictionarySet 现在可以筛选将结果赋给一个新的同类型对象。

1
2
3
// Filtering results into dictionary rather than array of tuples
let closeStars = starDistanceDict.filter { $0.value < 5.0 }
closeStars // Dictionary: ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Alpha Centauri B": 4.37]

Dictionary Mapping

Dictionary 获得了一个非常有用的方法用来 map 它的 values:

1
2
3
// Mapping values directly resulting in a dictionary
let mappedCloseStars = closeStars.mapValues { "\($0)" }
mappedCloseStars // ["Proxima Centauri": "4.24", "Alpha Centauri A": "4.37", "Alpha Centauri B": "4.37"]

Dictionary Default Values

访问 Dictionary 的 value 时,常见的做法是使用 nil coalescing operator 来给一个默认值(译者注 :a != nil ? a! : b 的方式,见[Basic Operators]。在 Swift 4 现在有了简洁的多的方式:

1
2
3
4
5
6
7
8
9
10
// Subscript with a default value
let siriusDistance = mappedCloseStars["Wolf 359", default: "unknown"] // "unknown"

// Subscript with a default value used for mutating
var starWordsCount: [String: Int] = [:]
for starName in nearestStarNames {
let numWords = starName.split(separator: " ").count
starWordsCount[starName, default: 0] += numWords // Amazing
}
starWordsCount // ["Wolf 359": 2, "Alpha Centauri B": 3, "Proxima Centauri": 2, "Alpha Centauri A": 3, "Barnard's Star": 2]

以前这种住转变需要包裹在臃肿的 if-let 语句中,在 Swift 4 中可能只需要一行。

Dictionary Grouping

另一个出奇有用的附加功能可以从 Sequence 实例化一个 Dictionary ,并将其分组:

1
2
3
4
// Grouping sequences by computed key
let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }

// ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]

当对特定的数据进行归类时会变得很方便。

Reserving Capacity

SequenceDictionary 现在都具备保留容量的功能。

1
2
3
4
// Improved Set/Dictionary capacity reservation
starWordsCount.capacity // 6
starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity
starWordsCount.capacity // 24

在这些类型中,重新分配是代价高昂的任务。使用 reserveCapacity(_:) 可以很容易的改善执行效率。
这是一个巨大的改变,所以务必检查这两个类型,想办法使用这些新特性来优化你的代码。

Private Access Modifier

在 Swift 3 上大家并不是很喜欢加入的 fileprivate, 理论上,它很不错,但是实践中它的用法时常让人困惑。在成员内部使用 private,当你在相同的文件共享成员变量的访问时很少使用 fileprivate
问题是 Swift 鼓励使用 extensions 来将代码按逻辑分组。extension 在成员变量的原始作用域之外,导致广泛地需要使用 fileprivate
Swift 4 意识到上述在类型和 extension 共享访问权的初衷,但是它只在相同的源文件中有效 [SE-0169]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct SpaceCraft {
private let warpCode: String

init(warpCode: String) {
self.warpCode = warpCode
}
}

extension SpaceCraft {
func goToWarpSpeed(warpCode: String) {
if warpCode == self.warpCode { // Error in Swift 3 unless warpCode is fileprivate
print("Do it Scotty!")
}
}
}

let enterprise = SpaceCraft(warpCode: "KirkIsCool")
//enterprise.warpCode // error: 'warpCode' is inaccessible due to 'private' protection level
enterprise.goToWarpSpeed(warpCode: "KirkIsCool") // "Do it Scotty!"

这让你可以实现原本 fileprivate 的目的,而不需要杂乱的代码。

API Additions

现在让我们一起来看一下 Swift 4 的新特性,这些改变只是一下简单的附加功能,不会破坏你现有的代码。

Archival and Serialization

目前为止,Swift 中自定义类型的序列化和归档有太多的坑,对于 class 类型,你需要子类化 NSObject 并且实现 NSCoding协议。

而像 structenum 这样的值类型,需要创建一个子类通过扩展 NSObjectNSCoding 的 hacks 来实现。
Swift 4 解决了这三种类型的序列化问题[SE-0166]:

1
2
3
4
5
6
7
8
9
10
11
struct CuriosityLog: Codable {
enum Discovery: String, Codable {
case rock, water, martian
}

var sol: Int
var discoveries: [Discovery]
}

// Create a log entry for Mars sol 42
let logSol42 = CuriosityLog(sol: 42, discoveries: [.rock, .rock, .rock, .rock])

上面的例子中,你可以看到在 Swift 中类型的 EncodableDecodable 只需要实现 Codable 协议,如果所有的属性都实现了 Codable 协议,那么编译器将自动完成协议的实现。
对对象进行编码,你需要把它交给一个编码器,Swift 4 开始积极的实现编码器。每个编码器按照不同的 schemes 。(注:该提案的部分内容仍在开发中):

1
2
3
4
5
6
let jsonEncoder = JSONEncoder() // One currently available encoder

// Encode the data
let jsonData = try jsonEncoder.encode(logSol42)
// Create a String from the data
let jsonString = String(data: jsonData, encoding: .utf8) // "{"sol":42,"discoveries":["rock","rock","rock","rock"]}"

它将一个对象自编码成 JSON 对象,请务必检查 JSONEncoder 的属性来定制它的输出。

过程的最后一部分是解码数据为一个具体对象:

1
2
3
4
5
6
let jsonDecoder = JSONDecoder() // Pair decoder to JSONEncoder

// Attempt to decode the data to a CuriosityLog object
let decodedLog = try jsonDecoder.decode(CuriosityLog.self, from: jsonData)
decodedLog.sol // 42
decodedLog.discoveries // [rock, rock, rock, rock]

通过使用 Swift 4的 encoding/decoding,获得Swift的类型安全性。同时不依赖 @objc 协议的开销和限制。

Key-Value Coding

目前为止,由于函数是一个闭包的缘故,你可以在不调用函数的情况下对函数进行引用。你不能做的通过属性访问是没有暴露借口的私有变量。

令人兴奋的是 Swift 4 可以用对象的 Key paths 来 get/set 私有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Lightsaber {
enum Color {
case blue, green, red
}
let color: Color
}

class ForceUser {
var name: String
var lightsaber: Lightsaber
var master: ForceUser?

init(name: String, lightsaber: Lightsaber, master: ForceUser? = nil) {
self.name = name
self.lightsaber = lightsaber
self.master = master
}
}

let sidious = ForceUser(name: "Darth Sidious", lightsaber: Lightsaber(color: .red))
let obiwan = ForceUser(name: "Obi-Wan Kenobi", lightsaber: Lightsaber(color: .blue))
let anakin = ForceUser(name: "Anakin Skywalker", lightsaber: Lightsaber(color: .blue), master: obiwan)

在这里你创建了一些 ForceUser 实例,通过设置他们的 name 、 lightsaber 和 master 。创建 key path ,你只需使用一个反斜杠后面跟上你感兴趣的属性:

1
2
3
4
5
// Create reference to the ForceUser.name key path
let nameKeyPath = \ForceUser.name

// Access the value from key path on instance
let obiwanName = obiwan[keyPath: nameKeyPath] // "Obi-Wan Kenobi"

在这个例子中,你给 ForceUsername 属性创建了一个 key path。然后使用这个 key path 通过新的下标 keyPath 这个下下标现在在每种类型都可以用。

这里有一些通过 key path 访问子对象,设置属性,构建 key path 引用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Use keypath directly inline and to drill down to sub objects
let anakinSaberColor = anakin[keyPath: \ForceUser.lightsaber.color] // blue

// Access a property on the object returned by key path
let masterKeyPath = \ForceUser.master
let anakinMasterName = anakin[keyPath: masterKeyPath]?.name // "Obi-Wan Kenobi"

// Change Anakin to the dark side using key path as a setter
anakin[keyPath: masterKeyPath] = sidious
anakin.master?.name // Darth Sidious

// Note: not currently working, but works in some situations
// Append a key path to an existing path
//let masterNameKeyPath = masterKeyPath.appending(path: \ForceUser.name)
//anakin[keyPath: masterKeyPath] // "Darth Sidious"
```

key path 之美在于它在 Swift 中是坚固的,不像 Objective-C 中的 string 那么凌乱。

### Multi-line String Literals
创建多行文本是很多编程语言一个非常普遍的特性。Swift 4 加入这个简单但是有用的语法,用三个引号包装文本[[SE-0168]](https://github.com/apple/swift-evolution/blob/master/proposals/0168-multi-line-string-literals.md):

```swift
let star = "⭐️"
let introString = """
A long time ago in a galaxy far,
far away....

You could write multi-lined strings
without "escaping" single quotes.

The indentation of the closing quotes
below deside where the text line
begins.

You can even dynamically add values
from properties: \(star)
"""
print(introString) // prints the string exactly as written above with the value of star

当构建 XML/JSON 信息或者 UI 上的长文字排版时,这是极为有用的。

One-Sided Ranges

为了减少冗余和提高可读性,标准库现在可以使用半开区间来推断开始和结束的索引[SE-0172]
从集合的一个索引到开始或者结束的索引创建一个区间,有了非常便利的方法:

1
2
3
4
// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..<planets.endIndex]
let firstThree = planets[..<4] // Before: planets[planets.startIndex..<4]

如你所见,半开区间不需要指明开始或者结束的索引。

Infinite Sequence

同时允许你从一个可计算的开始索引定义一个无限的 Sequence

1
2
3
4
5
6
7
// Infinite range: 1...infinity
var numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (8, "Neptune")]

planets.append("Pluto")
numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (9, "Pluto")]

Pattern Matching

半开区间另一个很好的用法是模式匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Pattern matching

func temperature(planetNumber: Int) {
switch planetNumber {
case ...2: // anything less than or equal to 2
print("Too hot")
case 4...: // anything greater than or equal to 4
print("Too cold")
default:
print("Justtttt right")
}
}

temperature(planetNumber: 3) // Earth

Generic Subscripts

下标是一种直观且重要的访问数据的方式,为了改善效率,现在可以用在普通类型[SE-0148]

1
2
3
4
5
6
7
8
9
10
11
struct GenericDictionary<Key: Hashable, Value> {
private var data: [Key: Value]

init(data: [Key: Value]) {
self.data = data
}

subscript<T>(key: Key) -> T? {
return data[key] as? T
}
}

例子中的返回值是泛型,你可以在这个泛型中这样使用下标:

1
2
3
4
5
6
7
8
// Dictionary of type: [String: Any]
var earthData = GenericDictionary(data: ["name": "Earth", "population": 7500000000, "moons": 1])

// Automatically infers return type without "as? String"
let name: String? = earthData["name"]

// Automatically infers return type without "as? Int"
let population: Int? = earthData["population"]

不仅仅是返回值可以是泛型的,下标也可以是泛型的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extension GenericDictionary {
subscript<Keys: Sequence>(keys: Keys) -> [Value] where Keys.Iterator.Element == Key {
var values: [Value] = []
for key in keys {
if let value = data[key] {
values.append(value)
}
}
return values
}
}

// Array subscript value
let nameAndMoons = earthData[["moons", "name"]] // [1, "Earth"]
// Set subscript value
let nameAndMoons2 = earthData[Set(["moons", "name"])] // [1, "Earth"]

在这个例子中你可以看到,传递两个不同的 Sequence 类型(ArraySet)会得到各自的 vlaues 组成的数组。

Miscellaneous

以上列举的囊括了swift4中最大变化的部分, 现在让我们快速看看其他方面的小改动。

MutableCollection.swapAt(_:_:)

MutableCollection 现在有 swapAt(_:_:) 方法,正如它的命名,用来交换给定下标的值 [SE-0173]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Very basic bubble sort with an in-place swap
func bubbleSort<T: Comparable>(_ array: [T]) -> [T] {
var sortedArray = array
for i in 0..<sortedArray.count - 1 {
for j in 1..<sortedArray.count {
if sortedArray[j-1] > sortedArray[j] {
sortedArray.swapAt(j-1, j) // New MutableCollection method
}
}
}
return sortedArray
}

bubbleSort([4, 3, 2, 1, 0]) // [0, 1, 2, 3, 4]

Associated Type Constraints

现在你可以使用 where 从句约束相关的类型[SE-0142]:

1
2
3
4
protocol MyProtocol {
associatedtype Element
associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element
}

通过约束协议,associatedtype 声明可以直接限制它们的值,不必在大费周折。

Class and Protocol Existential

最后一个从 Objective-C 搬过来的特性是可以定义一个类型遵守一类或一组协议[SE-0156]:

1
2
3
4
5
6
7
8
9
10
11
protocol MyProtocol { }
class View { }
class ViewSubclass: View, MyProtocol { }

class MyClass {
var delegate: (View & MyProtocol)?
}

let myClass = MyClass()
//myClass.delegate = View() // error: cannot assign value of type 'View' to type '(View & MyProtocol)?'
myClass.delegate = ViewSubclass()

Limiting @objc Inference

要向 Objective-C 暴露你的 Swift API ,可以使用 @objc 编译标志。在很多情况下编译器可以为你推导。但是大量的推理会导致三个主要的问题:

  1. 可能显著的增加你的二进制文件大小
  2. 有时候无法准确的推倒
  3. 增加构成 Objective-C 方法选择器冲突的风险

Swift 4 通过限制 @objc 的推导来解决这个问题,这意味着当你需要 Objective-C 所有的动态调度功能是,你主要明确的使用 @objc
举几个你需要修改的示例包括 private 方法,动态声明 和 NSObject 基类的一些方法。

NSNumber Bridging

在很长时间内,NSNumber 和 Swift numbers 有很多奇怪的行为都困扰这这门语言。Swift 4 干掉了这些bug [SE-0170]

这里有一个示范:

1
2
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231

在 Swift 3 中会出现的怪异现象,如果数字溢出,它会简单的从0开始。在这个例子中 999% 2 ^ 8 = 231。

Swift 4 解决了这个问题,只有当强转后的类型可以安全的容纳时,才会返回值。

Swift Package Manager

近几个月,Swift 包管理有许多更新,一些比较大的更新如下:

  1. 从 branch 或者 comint hash 中获取依赖
  2. 对可接受的包更多的支配
  3. 用更为常见的解决方案代替不直观的命令
  4. 能够定义用来编译的 Swift 版本
  5. 为每个 target 指明 source files 路径。

这些是 SPM 在必经之路上迈出的大步伐,它还有很长的路要走,我们可以通过积极参与提议来帮忙完善它。

有关最近已解决的提案的详细描述,请查看Swift 4 Package Manager Update

Still In Progress

在撰写本文时,队列中仍有15个接受的提案。如果你想一睹为快,访问Swift Evolution Proposals 然后用 Accepted 筛选.