概览
Swift 5 发布了,这是一个重要里程碑。
此版本终于迎来了 ABI 稳定,因此 Swift 运行时现在可以引入到 Apple 平台各类操作系统的不同版本中,包括 macOS、iOS、tvOS 与 watchOS。Swift 5 还引入了构建块的新功能,包括重新实现 String、在运行时对执行内存的独占访问与新数据类型,以及对动态可调用类型的支持。
Swift 5 兼容 Swift 4、Swift 4.1 和 Swift 4.2,Xcode 10.2 中包含了一个代码迁移器,可以自动处理许多迁移需要用到的源码更改。
最重要的更新:ABI稳定
- 什么是ABI。
在运行时,Swift程序二进制文件通过ABI
与其他库和组件交互,ABI
定义了很多底层细节:如何调用函数,如何在内存中表示数据,甚至是元数据的位置以及如何访问它。 - 什么是ABI稳定
此前发布的Swift版本中ABI
还没稳定,Swift并没包含在操作系统中,所以每一个应用内部都包含其Swift版本所对应的动态链接库
。
Swift 5将为Swift的标准库提供稳定的ABI
,Swift将包含在操作系统中,新版本的编译器会根据稳定的ABI
来编译生成的应用程序二进制文件。也就是说,以后新发布的编译器也可以编译旧版Swift 5代码,源代码实现兼容。 - ABI稳定带来的便利
- 因为源代码兼容,开发者能够跨多个Swift版本维护单个代码库;
- Swift应用程序包会变得更小;
- Swift语言变化以及变化频率都会有所下降;
- ABI稳定下阶段的目标
关于Swift ABI 稳定规划,Swift 5完成的是第一阶段的源兼容(Source compatibility
),下半部分是二进制framework和运行时兼容(Binary framework & runtime compatibility
),详细描述可见宣言Swift ABI Stability Manifesto。
新特性
Raw Strings (原始字符串)
SE-0200,增加了创建原始字符串的功能,使得写带有特殊符号的字符串更加简单。
- 要使用原始字符串, 可使用
#
将字符串包裹起来;
let str = #"This is also a Swift string literal"#
- 原始字符串中反斜杠和双引号会被视为字符串中的文字字符;
// before
let rain = "The \"rain\" in \"Spain\" falls \\mainly on the Spaniards."
// after
let rain = #"The "rain" in "Spain" falls \mainly on the Spaniards."#
- 由于反斜杠作为原始字符串中的字符,所以在插入值的时候需要在后面再加个 #;
let answer = 42
// before
let dontpanic = "The answer to life, the universe, and everything is \(answer)"
// after
let dontpanic = #"The answer to life, the universe, and everything is \#(answer)"#
- 如果需要在原始字符串包含 # 时, 前后应用 ## 包裹字符串
let str = ##"My dog said "woof"#gooddog"##
- 多行原始字符串用 #""" 开头 """#结尾
let multiline = #"""
The answer to life,
the universe,
and everything is \#(answer).
"""#
- 由于不用反斜杠转义,使得正则表达式更加简洁明了
// before
let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"
// after
let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#
标准类型Result
SE-0235,增加Result类型到Swift标准库,用于统一在异步完成处理程序中看到的笨拙的不同参数。
public enum Result {
case success(Success), failure(Failure)
}
例如,在URLSession
完成处理包含三个参数,处理起来就不怎么优雅:
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error != nil else { self.handleError(error!) }
guard let data = data, let response = response else { return /* Impossible? */ }
handleResponse(response, data: data)
}
这几行代码暴露了Swift缺乏对错误自动处理的缺点,在这里不仅是因为需要对error做处理,而且缺少比如data和response都为空等特殊情况的处理,使用Result
则会非常的优雅:
URLSession.shared.dataTask(with: url) { (result: Result<(response: URLResponse, data: Data), Error>) in
switch result {
case let .success(success):
handleResponse(success.response, data: success.data)
case let .error(error):
handleError(error)
}
}
自定义字符串插值
SE-0228大大改进了Swift的字符串插值系统,使其更高效,更灵活。
主要用法就是在String.StringInterpolation的拓展里添加自定义的插值方法,比如加一个格式化字符串的方法:
extension String.StringInterpolation {
public mutating func appendInterpolation(_ number: Double?, format formatString: String) {
if let number = number{
return appendLiteral(String(format: formatString, number))
} else {
return appendLiteral("nil")
}
}
}
然后就可以在字符串中使用自定义的插值方法
let number = 123.666
print("Hello, number is \(number")
print("Hello, number is \(number, format:"%.2f")")
print("Hello, number is \(nil, format:"%.2f")")
============================================================================
Hello, number is 123.666
Hello, number is 123.67
Hello, number is nil
所有字符串的处理、统计、添加html属性等操作都可以直接通过新的插值方式实现。
Dynamically callable types(动态可调用类型)
SE-0216@dynamicCallable
为Swift 添加了一个新属性,它带来了将类型标记为可直接调用的功能,支持应用于结构,枚举,类和协议,但扩展不可以。
如果需要添加@dynamicCallable
属性, 就必须要实现以下方法中的一个或者两个:
// 不需要指定参数名
func dynamicallyCall(withArguments args: [Int]) -> Double
// 指定参数名的方法
func dynamicallyCall(withKeywordArguments args: KeyValuePairs) -> Double
对比Swift 5之前的定义和调用方式:
// 定义方式
struct RandomNumberGenerator {
func generate(numberOfZeroes: Int) -> Double {
let maximum = pow(10, Double(numberOfZeroes))
return Double.random(in: 0...maximum)
}
}
// 调用方式
let random = RandomNumberGenerator()
let num = random.generate(numberOfZeroes: 2)
使用Swift 5 的@dynamicCallable
属性
// 定义方式
@dynamicCallable
struct RandomNumberGenerator {
func dynamicallyCall(withArguments args: [Int]) -> Double {
let numberOfZeroes = Double(args.first ?? 0)
let maximum = pow(10, numberOfZeroes)
return Double.random(in: 0...maximum)
}
}
// 调用方式
let random = RandomNumberGenerator()
let num = random(2) // random(2)等同于random.dynamicallyCall(withArguments: [2])
处理未来添加的枚举类型
SE-0192 在枚举新增加一个 @unknown
属性,用于区分两种稍有不同的情况:1. default 的 case 里面处理所有其他的 case;2. 需要单独处理所有 case,只有真正不符合所有 case,才会进入 default
提示。比如:
enum PasswordError: Error {
case short
case obvious
case simple
}
func showOld(error: PasswordError){
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
default:
print("Your password was too simple.")
}
}
上面代码假如我们再加个 case old,执行代码时它会自动进入到default分支,这是显然是,因为这个密码是一个旧密码而不是密码太简单,这时候可以用 @unknown,如下
func showNew(error: PasswordError) {
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
@unknown default:
print("Your password wasn't suitable.")
}
}
这时Xcode会产生一个警告⚠️Switch must be exhaustive. Do you want to add missing cases?
,因为新增了case old ,switch没有明确地处理每一个分支。
可变参数
在Swift 5
之前,可以编写一个带有可变参数的枚举, 如下:
enum X {
case foo(bar: Int...)
}
func baz() -> X {
return .foo(bar: 0, 1, 2, 3)
}
在Swift 5
之后, 上述定义改成数组参数, 而不是可变参数, 如下:
enum X {
case foo(bar: [Int])
}
func baz() -> X {
return .foo(bar: [0, 1, 2, 3])
}
try?
可选值嵌套
SE-0230 让try?
返回的多层 optional value
嵌套值变成只有一层,无论有多少可嵌套的可选值,返回值永远只是一个可选值。先看一下这个例子
struct User {
var id: Int
init?(id: Int) {
if id < 1 {
return nil
}
self.id = id
}
func getMessages() throws -> String {
// complicated code here
return "No messages"
}
}
let user = User(id: 1)
let messages = try? user?.getMessages()
在Swift4.2
及其之前的版本中,上面返回的是一个String??
,2层嵌套的可选值,如果有多层嵌套处理起来也是相当更麻烦。在Swift 5
中就完美的解决了这个问题,如果当前值是可选的,那么try?
将不会将值包装在可选值中,因此最终结果只是一个String?
。
整型倍数判断
SE-0225为整数类型添加了一个方法isMultiple(of:)
,可以检查一个整数是否为另一个整数的倍数。
let rowNumber = 4
if rowNumber.isMultiple(of: 2) {
print("Even")
} else {
print("Odd")
}
count
函数
SE-0220,在Swift
之前的版本中,有一个函数filter
可以过滤出数组中符合条件的的元素,组成一个新的数组,在Swift 5
中新增了一个函数count(where:)
, 可以获取数组中符合条件的元素的个数。
let arr = [1, 2, 34, 5, 6, 7, 8, 12, 45, 6, 9]
let filter = arr.filter({ $0 > 10 })
print(filter) // [34, 12, 45]
let count = arr.count(where: { $0 > 10 })
print(count) // 3
compactMapValues过滤字典元素
SE-0218compactMapValues()
为字典添加了一种新方法,将Swift4.x
的版本有两个函数compactMap
和mapValue
结合在一起。
-
compactMap
: 返回一个操作后得到的新的数组, 类似flatMap
-
mapValues
: 字典中的函数, 对字典的value
值执行操作, 返回改变value
后的新的字典
let times = [
"first": 2,
"second": 43,
"three": 12,
"four": 3
]
let compact = times.compactMap({ $0.value > 10 })
// [true, false, true, false]
let mapValues = times.mapValues({ $0 + 2 })
// ["second": 45, "first": 4, "three": 14, "four": 5]
compactMapValues
是将上述两个方法的功能合并在一起, 返回一个对value
操作后的新字典, 并且自动过滤不符合条件的键值对,下面的例子展示把非整形类型的键值对过滤掉:
let times = [
"Hudson": "38",
"Clarke": "42",
"Robinson": "35",
"Hartis": "DNF"
]
let finishers1 = times.compactMapValues { Int($0) }
// ["Clarke": 42, "Robinson": 35, "Hudson": 38]
参看文献:
- swift-evolution
- Swift5.0新特性