Swift 五年,ABI 终于稳定了!
等啊等,盼儿盼,终于在 2019 年的 WWDC 大会之前,Swift 5 正式发布了,而更让大家想要奔走相告的是这一版本的 ABI 稳定了!
曾于 2017 年正式担任 Swift 语言开发项目组主管 Ted Kremenek 于近日在 Swift 官方博客上正式宣布,Swift 5 发布了!而这一次他也成功完成了彼时走马上任时立下的 flag,为 Swift 带来 ABI 的稳定。如今 Swift 可以更好地为当前和未来版本的苹果操作系统 macOS、iOS、tvOS 和 watchOS 服务。
与此同时,Swift 5 还引入了构建块的新功能,包括重新实现 String、在运行时对执行内存的独占访问和新数据类型,以及对动态可调用类型的支持。接下来,我们将一一探讨 Swift 5 中的变化。
ABI 的稳定意味着什么?
当前有关最新版本的 Swift 5,最为热议的话题之一毋庸置疑就是“ABI 稳定性”。在此,要论 ABI 的稳定对 Swift 意义为何如此重大?
其实,从开发者角度而言,早在 4 年前 Swift 2.0 发布之际,大家就希望 Swift 中的 ABI 能够稳定,因为只有这样 Swift 才能算是一门成熟的编程语言,否则用程序员的话来调侃,「自从学了 Swift 之后,每年都要学一门新语言」。
从技术角度来看,ABI 的稳定指的是二进制接口稳定,这意味着 Swift 应用程序不用再包含用于 Swift 标准库和 Swift SDK 的动态链接库,这些基础库将会被植入系统中。简而言之,以后如同 Objective-C runtime 一样,Swift runtime 和标准库会随着 iOS、macOS、tvOS、watchOS 的发布一起被提供。这样带来最为直接的影响就是可以为 App 瘦身。
不过当前仍有局限性的是,ABI 稳定虽然可以让 runtime 支持多个 Swift 版本,但是前提是苹果应用的开发者和框架、库的作者必须使用相同版本的编译器。如果想要删除此限制,根据 Swift 官方博客描述,库作者需要一个当前正在实现的功能,称之为 Module Stability 。通过这一点也可以预料到,在接下来的 Swift 5.1 版本中,Module Stability 应会是一个重要的目标。
那么 ABI 的稳定是否完全是一件好事?其实不然,正如知名 iOS 开发者王巍(江湖人称“喵神”)日前在自己的博客上(https://onevcat.com/2019/02/swift-abi/)所言:
在 ABI 稳定之前,Swift runtime 是作为开发工具的一部分,被作为库打包到 App 中的。这样一来,在开发时,我们可以随意使用新版本 Swift 的类型或特性,因为它们的版本是开发者自己决定的。不过,当 ABI 稳定后,Swift runtime 变为了用户系统的一部分,它从开发工具,变为了运行的环境,不再由我们开发者唯一决定。
那么除了实现了 ABI 的稳定性之外,,在 Swift 5 上还有哪些功能特性更新?
Swift 语言更新
新特性:
现在可以使用增强分隔符来表示字符串文字。在引号的前面加上一个或多个“#”,并以 # 号结尾,此时它会将反斜杠以及双引号视为字符。使用增强分隔符可以避免将包含多个双引号或反斜杠字符的字符串文本与额外的转义符混淆。
print(#"<a href="\#(url)" title="Apple Developer">"#)
// Equivalent to:
print("<a href=\"\(url)\" title=\"Apple Developer\">")
如果声明的类型与标准库中的类型重名,那么你所声明的类型会影响标准库中的类型。
@dynamicCallable 属性允许你调用命名类型,就像使用简单的语法糖调用函数一样。主要用例是动态语言互操作性。
关键路径现在支持 keypath (\.self),一个引用其整个输入值的 WritableKeyPath。
在 Swift 5 之前,你可以编写一个带有可变参数的枚举案例:
enum X {
case foo(bar: Int...)
}
func baz() -> X {
return .foo(bar: 0, 1, 2, 3)
}
但现在你要这么做的话,可能会出现错误。这样情况下,你可以将参数改成一个数组,并且显示传入数组:
enum X {
case foo(bar: [Int])
}
func baz() -> X {
return .foo(bar: [0, 1, 2, 3])
}
在 Swift 5 模式下,可以用 ? 和 Optional 类型的表达式来扁平化生成的 Optional,而不是返回嵌套的 Optional。
如果类型 T 符合 Initialized with Literals 中的一个协议,例如ExpressibleByIntegerLiteral,并假设 literal 是一个文字表达式,那么T(literal)使用相应的协议创建一个 T 类型的文字,而不是调用 T 的初始化器协议的默认文字类型的值。
字符串插值提高了性能、清晰度和效率。
Swift 标准库
Swift 5 中的标准库包括以下新功能:
标准库现在包括带有 Result.success(_ :) 和 Result.failure(_ :) 两种情况的 Result 枚举。在 do-catch 语句和 try 表达式无法使用的情况下,可以使用 Result 手动传递和处理错误,譬如当你使用可能失败的异步 API 时。
作为此添加的一部分,错误协议现在遵循自身,这使得更容易处理通用上下文中的错误。
如今的标准库定义了 SIMD 类型和基本运算符。simd 框架提供的类型,如 float2 和 float3,现在是新标准库类型的类型别名。
SIMD 类型在标量元素类型上是通用的。例如,旧的 float3 类型是 SIMD3 <Float> 的类型别名。遵循 SIMDScalar 协议的任何类型都可以用作 SIMD 向量的标量类型,但有效的向量化取决于选择良好的数据布局并为相关的 SIMDS 存储类型提供有效的下标操作。
大多数使用 simd 类型的现有代码将可以继续使用新的通用 SIMD 类型,但需要注意一些变化。
新类型增加了一些新的一致性,SIMD 向量现在是 Hashable、Equatable 和 Codable。这将允许开发者删除一些现有代码中提供的一致性的扩展。
通过重载可以提供向量-标量算法的运算符集。这使得编写一些代码变得更容易,但在某些情况下会给类型检查器带来歧义,并且可能需要拆分某些表达式或使用显式类型进行注释。
由于类型现在是通用的泛型而不是具体的,如果开发者已经在 simd 框架类型上定义了自己的协议,则可能需要重构的一致性,因为 Swift 泛型类型不能对协议具有多个条件一致性。这种情况相对较少,但你通常需要做的是重构代码,如下所示:
protocol MyVectorProtocol { /* ... */ }
extension float2: MyVectorProtocol { /* ... */ }
extension double2: MyVectorProtocol { /* ... */ }
要改为使用以下结构:
protocol MySIMDScalarProtocol: SIMDScalar { /* ... */ }
extension SIMD2 where Scalar: MySIMDScalarProtocol { /* ... */ }
// Or even:
protocol MySIMDScalarProtocol: SIMDScalar { /* ... */ }
extension SIMD where Scalar: MySIMDScalarProtocol { /* ... */ }
这种更改通常允许用户删除许多冗余实现,但它要求你定义任何必要的实现 hooks,这些 hooks 引用 Darwin 系统上标量类型的 C 头文件中的具体函数。
Set 和 Dictionary 现在为每个新创建的实例使用不同的哈希种子。因此,相同 Set 和 Dictionary 中元素的顺序与以前的版本相差较大:
let a: Set<Int> = [1, 2, 3, 4, 5]
let b: Set<Int> = [1, 2, 3, 4, 5]
a == b // true
print(a) // [1, 4, 3, 2, 5]
print(b) // [4, 2, 5, 1, 3]
如果错误地假定两个不相关但相等的 Set 和 Dictionary 将包含相同顺序的元素,那么现有代码在 Swift 5 中将更容易产生不正确的结果。虽然元素排序在不同的 Set 或 Dictionary 实例之间不稳定,但在同一个实例上进行多次迭代,顺序不会变。除了强调这些集合不能保证一致的元素排序之外,这种变化还修复了许多大型操作(例如 union(_ :))表现出二次性能的情况。
为了帮助防止 Cocoa 对象出现不一致哈希,NSObject 上的 hashValue 属性将不可以重写。其实,早在 Swift 4.2 中已不推荐使用 hashValue 了。这样情况下,可以在 NSObject 子类中重载 hash 属性以自定义哈希值。示例实现如下:
class Person: NSObject {
let name: String
init(name: String) {
self.name = name
super.init()
}
override func isEqual(_ other: Any?) -> Bool {
guard let other = other as? Person else { return false }
return other.name == self.name
}
override var hash: Int {
var hasher = Hasher()
hasher.combine(name)
return hasher.finalize()
}
}
在此,哈希与 equality 齐头并进。如果重写 hash,则还需要重写 isEqual:,反之亦然。
DictionaryLiteral 类型被重命名为 KeyValuePairs。
桥接到 Objective-C 代码的 Swift 字符串现在可以在适当的时候从 CFStringGetCStringPtr 返回一个非零值,并且从 UTF8String 方法返回的指针与字符串的生命周期相关联。正确的程序应该不会出现任何问题,并且开发者还可以看到性能有着显著的提升。但是,它也可能会导致以前未经测试的代码在运行时暴露潜在的 Bug。
Sequence 协议不再具有 SubSequence 关联类型。先前返回 SubSequence 的 Sequence 方法现在返回具体类型。例如,suffix(_:) 现在返回一个数组。
使用 SubSequence 的 Sequence 扩展应该修改为使用的具体类型,或者修改为 Collection 上的扩展,而此时 SubSequence 仍然可用。
String 结构的原生编码从 UTF-16 切换到 UTF-8,与 String.UTF16View相比,这可以提高 String.UTF8View 的性能。
Swift 编译器
Swift 5 默认为调试和发布版本强制执行对内存的独占访问。Swift 5 支持动态可调用类型,有助于提高与 Python、JavaScript 和 Ruby 等动态语言的互操作性。
在编译器方面,Swift 5 还实现了一些功能:
为了减小 Swift 元数据占用的大小,Swift 中定义的便捷初始化器现在只提前分配一个对象,如果它们调用的是 Objective-C 中定义的指定初始方法。在大多数情况下,这对开发者的 App 没有影响,但如果从 Objective-C 调用你的便捷初始化程序,并且不调用暴露给 Objective-C 的 self.init 方法,那么从最初通过 alloc 分配的内存空间会在没用调用任何初始化程序的情况下被释放。对于不希望发生任何类型对象替换的初始化器的用户来说,这可能会产生一些疑问。其中一个例子是使用 initWithCoder :: 方法,如果 NSKeyedUnarchiver 调用 Swift 中的 initWithCoder,并且归档对象图包含循环,那么 NSKeyedUnarchiver 的实现可能会出错。
为了避免这种情况,请确保不支持对象替换的便捷初始化程序始终委托给也暴露给 Objective-C 的初始化程序,因为它们是在 Objective-C 中定义的,也是因为它们用 @objc 标记,或者因为它们覆盖并暴露给 Objective-C 的初始化程序,还或者因为它们满足 @objc 协议的要求。
Swift 中不再提供超过 16 字节对齐的 C 类型。其实,Swift 编译器也从未正确处理过这些类型。
在 Swift 5 模式中,非 final class 的便捷初始化程序中的 self 类型现在是动态 Self 类型,而不是具体类类型。
现在,在优化(-O 和 -Osize)构建中,默认情况下在运行时强制执行独占内存访问。在运行时倘若程序违反排他性将会生成诊断消息:“Simultaneous accesses to […], but modification requires exclusive access”。对此,你可以使用命令行标志 -enforce-exclusivity = unchecked 禁用此功能,但这样做可能会导致未定义的行为。运行时违反排他性通常是由于同时访问类属性或全局变量(包括顶层代码中的变量或转义闭包捕获的变量)。
Swift 3 模式已被删除。 -swift-version 标志支持的值为 4、4.2 和 5.0。
现在,默认参数打印在 SourceKit 生成的 Swift 模块接口中,而不是仅使用占位符默认值。
SE-0192:处理未来的 Enum 案件。在 Swift 5 模式中,需要通过 Objective-C 声明或来自系统框架的枚举来处理未知 case,其中包括可能在将来添加的 case,或者可能在 Objective-C 实现文件中私下定义的 case。形式上,Objective-C 允许在枚举中存储任何值,只要它适合底层类型即可。可以使用新的 @unknown 默认情况来处理这些未知 case,如果从 Swift 中省略任何已知情况,它仍会提供警告。它们也可以使用普通的默认情况进行处理。
如果你已在 Objective-C 中定义了自己的枚举,并且不需要客户端来处理未知案例,则可以使用 NS_CLOSED_ENUM 宏而不是 NS_ENUM。Swift 编译器识别出这一点,并且不需要在迭代时提供默认值。
在 Swift 4 和 4.2 模式下,你仍然可以使用 @unknow 默认值。如果忽略它并将未知值传递给 switch,程序将在运行时报错,这与 Xcode 10.1 中的 Swift 4.2 的行为相同。
unowned 和 unowned(unsafe)变量现在支持 Optional。
包管理器更新
Swift 5 为 Swift 包管理器带来了许多新功能:
SE-0219 :程序包管理器依赖镜像。该依赖项镜像允许顶层程序包覆盖依赖关系 URL,可使用以下命令设置镜像:
$ swift package config set-mirror \
--package-url <original URL> --mirror-url <mirror URL>
SE-0236:软件包管理器平台部署设置。在使用 Swift 5 Package.swift 工具版本时,软件包现在可以自定义 Apple 平台的最低部署目标设置。如果程序包的任何依赖项指定的最小部署目标大于程序包自身的最低部署目标,则构建程序包会抛出错误。
SE-0238:软件包管理器目标特定的构建设置。如今在使用 Swift 5 Package.swift 工具版本时,目标可以声明一些常用的特定于目标的构建设置。新设置可以基于平台和构建配置进行条件化。当前构建设置支持 Swift 和 C 语言,C 语言头文件搜索路径、链接库和链接框架。
Swift 测试命令可以使用 --enable-code-coverage 标志,以生成适合其他代码覆盖工具使用的代码覆盖率数据的标准格式。生成的代码覆盖率数据在 <build-dir> / <configuration> / codecov 中可用。
Swift 5 不再支持 Swift 3 Package.swift 工具版本。Swift 3 Package.swift 工具版本上的软件包应该更新到最新的工具版本。
目前对大包的包管理器操作现在明显更快。
Swift 包管理器有一个新的 --disable-automatic-resolution 标志,当 Package.resolved 条目不再与 Package.swift 清单文件中指定的依赖项版本兼容时,该标志强制包解析失败。此功能对于持续集成系统非常有用,可以检查包的 Package.resolved 是否已过期。
Swift run 命令现在具有在 REPL 中导入库而无需构建可执行文件的功能。即在 Swift run 命令新增了一个 --repl 选项,它将启动 Swift REPL,支持导入包的库目标。这使开发者可以轻松地从包目标中试用 API,而无需构建调用该 API 的可执行文件。
有关 Swift 包管理器的更多信息,可访问:https://swift.org/getting-started/#using-the-package-manager。
迁移到 Swift 5
对于 Swift 开发者而言,这一次不用「每年再去学习一门新语言了」。最新版本的 Swift 5 与 Swift 4、Swift 4.1、Swift 4.2 兼容。
同时为了帮助开发者从早期版本的 Swift 迁移到 Swift 5 上,Apple 默认编译器版本为 Swift 5.0 的 Xcode 10.2 中包含一个代码迁移器,可以自动处理更改许多迁移所需的源代码。或者你可以参考迁移指南:https://swift.org/migration-guide-swift5/
下载
Linux
基于 Ubuntu 18.04、Ubuntu 16.04 和 Ubuntu 14.04 的下载地址(https://swift.org/download/#releases);
Apple
在 Apple 平台上的开发,Swift 5 可以作为 Xcode 10.2 的一部分(https://itunes.apple.com/app/xcode/id497799835)或者也可以在 Swift.org(https://swift.org/download/#release) 中下载。
参考:
https://developer.apple.com/documentation/xcode_release_notes/xcode_10_2_release_notes/swift_5_release_notes_for_xcode_10_2
https://onevcat.com/2019/02/swift-abi/
https://swift.org/blog/swift-5-released/
https://swift.org/blog/abi-stability-and-more/
热 文 推 荐
☞JavaScript 力压 Java 成最受欢迎编程语言,TypeScript 大涨!
☞春招季如何横扫 Javascript 面试核心考点? | 技术头条