本文翻译自raywenderlich.com中的文章《What’s New in Swift 4?》,由于本人水平有限,翻译中不准确或者有错误的地方,敬请谅解和指正。
提示:本教程使用集成在 Xcode 9 beta 1 中Swift 4 版本。
Swift 4 是苹果公司计划于2017年秋季发布的最新主要版本,它主要关注与Swift 3 的兼容性,以及继续向ABI稳定迈进。
皮皮Swift 4,我们走
Swift 4 包含于Xcode 9中。你可以从苹果公司的开发者页面下载最新的Xcode 9(你必须有一个开发者账号)。每个Xcode版本将会包含Swift 4当时版本的快照。
当你阅读时,你会发现链接都是SE-xxxx形式。这些链接将会为你展示相关的Swift演化提案。如果你想深入学习某个专题,可以翻阅它们。
我推荐在playground中尝试每个 Swift 4 的特性或更新。这将帮助强化你头脑中的知识,赋予你深入学习每个专题的能力。扩展他们、打破它们,与playground中的例子斗,其乐无穷。
提示:这篇文章将随着每个Xcode beta版本更新。如果你使用不同的快照,不能保证代码可以运行。
移植到 Swift 4
Swift 3 移植到 4 要比 2.2 到 3容易一些。总的来说,大多数变化都是新增的,不需要大量的个人情怀(personal touch)。因此,Swift移植工具将为你完成大部分的修改工作量。
Xcode 9同时支持 Swift 4 以及Swift 3 的中间版本 Swift 3.2。项目中的target既可以是 Swift 3.2 也可以是 Swift 4,让你在需要的时候一点一点移植。转换到 Swift 3.2不是完全没有工作量,你需要更新部分代码来兼容新的SDK,再者因为 Swift 还没有ABI稳定,所以你需要用Xcode 9 重新编译依赖关系。
当你准备移植到 Swift 4,Xcode再次提供了移植工具帮助你。Xcode中,你可以使用“Edit/Convert/To Current Swift Syntax”启动转换工具。
选择了想要转换的target之后,Xcode将提示你Objective-C推断的偏好设置,选择推荐选项以通过限制推断来减少二进制大小(更多这个专题,查看下面的限制@objc推断)
推断偏好设置为了更好的理解你代码中将会出现的变化,我们先来看看 Swift 4中的API 变化。
API变化
在转入Swift 4 新增特性之前,让我们先看下已有API的变化或更新。
字符串Strings
Swift 4 中String类收到颇多关爱,这份提案包含很多变化,来看看最大变化【SE-0163】。
假如你感觉怀旧,strings像Swift 2.0之前那样再次成为集合。这一变化移除了String必须的character数组。现在你可以直接在String对象上迭代。
let galaxy = "Milky Way 🐮"
for char in galaxy {
print(char)
}
你不仅可以对String做逻辑迭代,还可以得到Sequence和Collection中的额外特性。
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中声明的功能。这样做是为改进slice如何工作。Swift 4 增加了 Substring类型来引用String的子序列。
String和Substring都实现StringProtocol协议,所以他们功能的几乎完全相同:
// 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 的适配。原先,多编码点的unicode字符长度大于1,常见于emoji表情。下面是一些常见的例子:
"👩💻".count // Now: 1, Before: 2
"👍🏽".count // Now: 1, Before: 2
"👨❤️💋👨".count // Now: 1, Before, 4
这只是String Manifesto中的一部分变化,你可以在String Manifesto读到所有这些的原始动机,以及未来可预见的解决提案。
字典和集合
集合类型不断发展,Set和Dictionary不总是最直观的。幸运的是,Swift 团队给了他们很多的关注【SE-0165】
基于序列的初始化
第一项能力就是用键值对序列创建字典:
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]
重复Key解决方案
你现在可以在初始化字典时,使用任意方法处理重复key问题。这可以避免“无提示的改写键值对”。
// 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和“+”号,将重复key对应的值相加。
提示:如果你不熟悉zip,你可以快速的在苹果的Swift文档中学习。
筛选
字典和集合(Set)现在都获取了筛选能力,将筛选结果放入原始类型的新对象中。
// 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]
字典映射
字典获得了一个非常有用的方法,可以直接映射它的值:
// 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"]
字典缺省值
访问字典中的值时,常见的实践方法,是使用??操作符,当值为nil时赋予缺省值。在 Swift 4 中,这项操作变得简洁,允许你在代码行上“耍花枪”。
// 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 中,只需简单一行代码。
字典组
另外一个很有用的新增特性是,用序列初始化字典并把他们组合在一起:
// 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"]]
这在处理特殊形式的数据时变得很便利。
预留容量
序列和字典都有了显式预留容量的能力。
// Improved Set/Dictionary capacity reservation
starWordsCount.capacity // 6
starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity
starWordsCount.capacity // 24
对序列和字典重新分配内存空间,是非常耗费资源的操作。当你知道需要多少数据时,使用reserveCapacity(_:)是提升性能的简单方法。
这包含了很多信息,所以查看这两个类型的文档,找到优化代码的方法。
私有访问修饰符
有些人不喜欢 Swift 3 中新增的 fileprivate。理论上,它很赞,但实际上它的用法让人糊涂。这样做的目的是,在成员内部使用private,而在相同文件中想要在成员中分享访问权限则使用 fileprivate。
这个问题源自于,Swift鼓励使用扩展让代码逻辑分组。扩展被看做是原始成员声明作用域的外围,使得扩展需要 fileprivate。
Swift 4 认识到了在类型及其扩展之间分享相同访问作用域的原始动机,只有在相同的源文件中才起作用【SE-0169】:
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
现在让我们看看 Swift 4 的亮瞎双眼的新特性。这些特性不会打破已有代码,因为他们都是简单新增。
归档和序列化
讲到这里,序列化和归档自定义类型,你需要一些套路。对于类,你需要创建NSObject的子类、实现NSCoding协议。
struct和enum等值类型则需要一些小技巧,像创建能够继承NSObject和NSCoding的子类。
Swift 4 引入这三种类型的序列化来解决这个问题【SE-0166】。
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])
从例子可以看出,只要实现 Codable 协议就可以使得Swift类型可编码和解码。如果所有属性都是Codable,编译器自动生成协议的实现。
真正对一个对象编码,你需要将它传给一个编码器。Swift 4 正在积极推进Swift编码器。根据不同的模式给对象编码。【SE-0167】(注意:这个提案的部分内容还在修订中)。
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暴露出来的属性以便自定义输出结果。
最后是将数据解码到一个具体对象:
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,编码和解码变得类型安全,不需要依赖@objc协议的限制。
键值编码
因为函数在Swift中是闭包,所以你可以持有函数的引用而不调用它们。但做不到的是,持有属性的引用而不实际访问属性下的数据。
Swift 4 新增了让人兴奋的特性,可以引用类型的keypath来get/set实例的底层数据。
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)
这里创建了一些force的实例,设置了名字、光剑和主人。要创建key path,你只需要在属性名前加上右斜线:
// 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"
上面例子中,为ForceUser的name属性创建了一个key path,你可以把它传递给keyPath参数。下面是一些例子,使用key path扩展到子对象、设置属性等。
// 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"
Swift中key path的优雅之处在于它们是强类型的,不再有Objective-C的杂乱。
多行String字面量
很多编程语言的特性中包含创建多行字符串字面量的能力。Swift 4用三个引号"""包裹文本,实现了这个简单但有用的语法。
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信息或者创建长格式文本时,非常有用。
单边范围
为减少冗长、提高可读性,标准库可以使用单边范围确定开始、结束索引。这使得从集合截取变得方便。
// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..
能看到,单边范围减少了显示指定开始、结束索引的必要。
无穷序列
当开始索引是可数类型时,你可以创建一个无穷序列:
// 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
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
泛型下标
下标是访问数据类型的重要组成部分,同时也非常直观。为提升这种有用性,下标现在可支持泛型【SE-0148】:
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
}
}
例子中返回类型为泛型,你可以这样使用泛型下标:
// 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"]
不仅返回类型可以为泛型,下标类型也可以为泛型:
extension GenericDictionary { subscript(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"]
本例中,你可以传入两个不同的序列类型(数组和集合),得到相关值的数组。
杂项
MutableCollection现在有了可变方法swapAt(_:_:),正向看起来的那样,它交换给定数组中的值。
// Very basic bubble sort with an in-place swap
func bubbleSort(_ array: [T]) -> [T] {
var sortedArray = array
for i in 0..sortedArray[j] {
sortedArray.swapAt(j-1, j) // New MutableCollection method
}
}
}
return sortedArray
}
bubbleSort([4, 3, 2, 1, 0]) // [0, 1, 2, 3, 4]
关联类型约束
可以通过where语句包含关联类型约束【SE=0142】:
protocol MyProtocol {
associatedtype Element
associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element
}
使用协议约束,很多associatedtype声明可以直接包含自身的值,而不需要其他套路。
类和协议的存在性
区分Objective-C和Swift的重要特性就是,Swift可以定义一个类型,既遵从一个类又遵从一些协议【SE-0156】:
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()
限制@objc推断
要提供Swift API给Objective-C,你要使用@objc编译器属性。很多情况下,Swift编译器可以为你推断。推断的三个主要问题在于:
1、二进制大小大幅度增加的隐患
2、不确定@objc何时被推断。
3、无意间造成和Objective-C函数冲突的可能性增加。
Swift 4拿出大板斧,限制了@objc推断【SE-0160】。这意味着,当你需要Objective-C全部动态分发能力时,必须显式使用@objc。
NSNumber桥接
NSNumber和Swift numbers之间有很多恼人的恶臭,萦绕在语言周围太长时间了。幸运的是,Swift解决了它们【SE-0170】。
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包管理器
过去几个月,Swift包管理器已经有一定数量的更新,其中大的变更包括:
1、根据分支或者提交哈希进行源码以来
2、可接受包版本的更多控制
3、替换不直观的命令,使用一些更常用的解决模式
4、使用编译器定义Swift版本的能力
5、为每个target指定源文件路径
这些大变化都是Swift包管理器需要做的,SPM还有很长的路要走,我们都可以通过提案,帮助它更好发展。
一直在路上
在写这篇文章时,仍有15分已接受的提案在排队。如果你想知道接下来会发生什么,查看Swift演化提案、选择“Accepted”。
路怎么走,你们自己挑?
Swift语言这几年不断发育成熟。提案进程和社区参与,使得大家能够跟踪语言变化,也使得我们每个人都可以直接影响语言的演化。
上面的 Swift 4 变化,我们终于发现ABI稳定就在下一个转角。Swift升级的阵痛在变小。构建性能和工具都大幅度提升。在苹果生态外使用Swift变得越发可行了。设想一下,我们离一个直观的实现,还差一小部分的String重写。
Swift还会迎来很多改变。跟上每个变化的节奏,查看以下资源:
网友评论