[ WWDC2018 ] - Swift 4.2革新 What's new in Swift
前言
Swift每半年有两个重大更新的版本,4.1和4.2就是继4.0之后2018年的两个版本。
Swift 4.2版本主要有两方面的更新,一是关注提升开发人员生产力,你可以看到项目构建速度的提升,利于开发的语言改进;二是在二进制兼容性方面做出了大量的努力。
Swift的发展
苹果希望在所有平台上支持Swift,大概一个月前,苹果扩展了公共持续集成系统,现在可以无缝插入自己的硬件支持,以便在那里进行测试。
六个月前,Swift社区从邮件列表转向论坛,可以让大家更方便的参与讨论。
在Swift 4.2版本更新中,有4个主要内容:
更快的编译速度 新的语言特性提高效率,移除样板代码 Swift的SDK改进 改进二进制兼容性Swift 5版本将在2019年初发布,在这个版本里,二进制兼容性将会稳定下来,意味着编译完的二进制文件可以和其他编译器构建的代码进行互操作,这对于语言的成熟度来说是具有里程碑意义的,将使得苹果能够直接在操作系统中发布Swift运行时,而不需要再包含在App里面,对应用启动速度,运行效率,和对减少应用大小都有很大帮助。
改进
代码兼容性
Xcode 10包含一个Swift 4.2编译器,这个编译器兼容Swift 3, Swift 4。但Xcode 10是支持兼容Swift 3的最后一个版本。
编译器改进
Swift 4.2在编译Swift代码速度上有了3倍的提升。在大部分项目中,编译速度将提高2倍,这取决于你的项目中Swift代码所占的比例。以往被大家所诟病的Swift编译龟速,总算引起苹果重视了。
Xcode10 从Optimization Level分离出Compilation Mode,Compilation Mode可选值如下图,建议在Debug模式下开启增量模式(Incremental),以提高编译速度,而在Release下开启全量模式(Whole Module)。
而Optimization Level新增了Optimize for Size,大部分项目在开启这个模式后,Swift代码生成的二进制文件减少了10%到30%,但是运行时的性能变慢了5%。如何你的项目对应用包大小比较重视,可以考虑开启这个模式。
运行时优化
Swift也是使用和Objective-C是一样ARC方式管理内存的。
Swift 4.1及以前的版本,ARC以"Owned"方式自动插入Retain和Release,调用方负责 Retain,被调用方负责Release。容易出现多个额外相互抵消的Retain-Release,造成性能的浪费。
func caller() {
// ‘x’ created with +1 reference count
let x = X()
// retain x
bar(x) // release x in callee
// retain x
baz(x) // release x in callee
foo(x) // release x in callee
}
Swift 4.2则改成以"Guaranteed"方式插入Retain和Release。由调用方管理对象的生命周期,不再是被调用方负责Release。这个优化减少了无用的Retain、Release代码,提升了性能,减少了一部分二进制文件的大小。
func caller() {
// ‘x’ created with +1 reference count
let x = X()
bar(x)
baz(x)
foo(x)
// release x
}
Small String
Swift 4.2在64位机器上,字符串的编码由24bytes改为16bytes,并且对于足够小的字符串,将直接存进这16bytes,而不需要分配堆内存,提高性能,减少内存占用。
新的特性
枚举遍历
Swift 4.1及以前的版本并没有提供遍历枚举的方法,所以遍历枚举往往需要事先手动管理一个包含所有枚举项的数组,这种方法不仅繁琐,在后续迭代中,如果要新增枚举项,容易忘记添加进allCases。
enum Gait {
case walk
case trot
case canter
case gallop
static var allCases: [Gait] = [.walk, .trot, .canter, .gallop]
}
for gait in Gait.allCases {
print(gait)
}
Swift 4.2 新增了 CaseIterable 协议,遍历枚举变得方便简单
enum Gait: CaseIterable {
case walk
case trot
case canter
case gallop
}
for gait in Gait.allCases {
print(gait)
}
Conditional Conformance
首先,我们看下面Swift 4.0的代码
extension Sequence where Element: Equatable {
func contains(_ element: Element) -> Bool
}
let animals = ["cat", "dog", "weasel"]
animals.contains("dog") // OK
let coins = [[1, 2], [3, 6], [4, 12]]
coins.contains([3, 6]) // Error
这是因为在Sequence有contains的方法,元素是Equatable时,我们就可以通过contains方法判断是否包含该元素,而当元素是[Int]时,[Int]不是Equatable(在Swift 4.0及以前),所以调用contains方法时会编译出错。
但是如果一个集合的元素都是Equatable的,那么我们就应当认为该集合也是Equatable的,因为我们可以便利所有元素去判断集合是否相等。在Swift 4.0及以前,可以通过给集合加扩展实现,而Swift 4.1之后,只要元素是Equatable的,则集合也默认是Equatable。
同理的还有其他协议如Hashable、Encodable、Decodable,甚至是自定义的协议。
let coins = [[1, 2], [3, 6], [4, 12]]
coins.contains([3, 6]) // This now works!
Equatable和Hashable自动合成
在Swift 4.0,要比较两个结构体是否相等,需要手动实现方法,同时列出所有属性来判断相等。当新增一个属性时,还需要记得手动更新方法,容易出错。
struct Restaurant {
let name: String
let hasTableService: Bool
let kidFriendly: Bool
}
extension Restaurant: Equatable {
static func ==(a: Restaurant, b: Restaurant) -> Bool {
return a.name == b.name &&
a.hasTableService == b.hasTableService &&
a.kidFriendly == b.kidFriendly
}
}
而Swift 4.1仅仅只需要声明该结构为Equatable,将自动生成==方法,比较所有属性。
struct Restaurant: Equatable {
let name: String
let hasTableService: Bool
let kidFriendly: Bool
}
同理,Hashable也支持自动合成代码。
Hashable功能增强
对一个结构体实现Hashable,往往是手动书写hashValue的实现,而这种实现难以理解,容易出现问题,其次,这种实现往往不够高效,在Dictionary和Set中使用有性能问题,同时,也不够安全,容易被黑客利用。
struct City {
let name: String
let state: String
let population: Int
}
extension City: Hashable {
var hashValue: Int {
return (name.hashValue &* 58374501) &+ state.hashValue
}
}
所以Swift4.2提供了生成hashValue的方法来解决这个问题
extension City: Hashable {
func hash(into hasher: inout Hasher) {
name.hash(into: &hasher)
state.hash(into: &hasher)
}
}
需要注意的是,苹果自带的Hash值生成算法,使用了随机数种子来加密Hash值,每个进程拥有一个随机数种子,在应用启动时生成。意味着,相同的内容在应用重启后或者不同的进程里计算的到的Hash值是不一样的。 如果业务对Hash值有比较强的依赖,需要保证每次生成的Hash值一样,可以在Xcode的环境变量里设置变量SWIFT_DETERMINISTIC_HASHING=1,来关闭使用随机数种子。
生成随机数
在Swift 4.1及以前的版本,通过C语言生成随机数(如下方代码),使用这种方法有两个问题,一是需要引入Foundation,而且Foundation在Linux系统下不支持,二是得到的随机数不是均匀分布的,例子中,1到4出现的概率比5和6大
let x = 1 + Int(arc4random() % 6) //range 1...6
Swift 4.2为了解决这些问题,对数值类型新增了random方法实现获取随机数。
let intValue = Int.random(in: 1 ..< 6)
let floatValue = Float.random(in: 0 ..< 1)
此外,还对集合提供了randomElement方法获取随机元素,shuffled方法对数组“洗牌”,打乱顺序。
let greetings = ["hey", "hi", "hello", "hola"]
// 注意randomElement方法返回值是可选值
print(greetings.randomElement()!)
// 打乱数组从未如此简单
let randomlyOrderedGreetings = greetings.shuffled()
新的编译条件
Swift 4.2新增了canImport(X),hasTargetEnvironment(X)的编译条件来优化像(os(iOS) || os(watchOS) || os(tvOS)) && (cpu(i386) || cpu(x86_64)) 这种长串、不易读、目的又不明确的编译条件。使用Swift跨平台开发的建议使用。
// Before
#if (os(iOS) || os(watchOS) || os(tvOS)) && (cpu(i386) || cpu(x86_64))
...
#endif
// Now
#if hasTargetEnvironment(simulator)
...
#endif
Implicitly Unwrapped Optionals改动
在Swift 4.2,IUO不再是一个Type,而是一个变量声明的标记,编译器将带标记的变量的类型检查为可选值,当使用到该变量时,先给这个变量加上强制解包,再检查类型。
let x: Int! = 1 // x 被标记为IUO,类型其实还是可选值
let y = x + 1 // 实际上编译时,编译器会转化为 x! + 1 去进行编译
因为IUO不再是一个Type,下面的代码将会导致编译时错误:
let array: [Int!] = [] // 编译不通过而当将创建IUO的别名时,会得到一个警告,同时别名实际上只是可选值类型。
typealias T = Int! // 将会得到警告
let array: [T] = [3] // array为[Int?]类型
强制内存独占访问
在Swift 4及以前的版本,新增了内存独占访问检查的特性。它是编译时和运行时检查的组合,它们限制某些操作被执行,特别是禁止重复访问相同的内存位置。比如下面的例子:
struct Path {
var components: [String] = []
mutating func withAppended(_ name: String, _ closure: () -> Void) {
components.append(name)
closure()
components.removeLast()
}
}
var path = Path(components: ["usr", "local"])
path.withAppended("bin") {
print(path) // 这里的path与上面的path造成独占访问冲突
}
Swift可以在编译时检查到这种错误。Swift 4.2改进了这种检查,可以在更多情况下捕获这种错误。
最后
4.2版本带来了ABI稳定性的提升,一部分新的特性,提高开发效率,值得升级。而Apple将4.2描述为“在Swift 5中实现ABI稳定性的中继点”,希望Swift 5能带来更稳定的ABI。
文章来源:
Author:caojiaxin
link:https://techblog.toutiao.com/2018/06/19/untitled-9/