Swift 4.2 新特性

原文:ole/whats-new-in-swift-4-2

Bool.toggle

SE-0199Bool增加了一个新的方法toggle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Layer {
var isHidden = false
}

struct View {
var layer = Layer()
}

var view = View()

// before
view.layer.isHidden = !view.layer.isHidden
view.layer.isHidden // true

// Now:
view.layer.isHidden.toggle()
view.layer.isHidden // false

Sequence and Collection 协议的修改

allSatisfy

SE-0207Sequence协议添加了allSatisfy方法。如果序列中的每个元素都满足给定的条件,allSatisfy方法返回true

1
2
3
let digits = 0...9
let areAllSmallerThanTen = digits.allSatisfy { $0 < 10 } // true
let areAllEven = digits.allSatisfy { $0 % 2 == 0 } // false

last(where:), lastIndex(where:) 和 lastIndex(of:)

SE-0204Sequence协议添加了last(where:)方法, Collection添加了lastIndex(where:)lastIndex(of:)方法。

1
2
3
4
5
6
7
let lastEvenDigit == digits.last { $0 % 2 == 0 }  // 8

let text = "Vamos a la playa"
let lastWordBreak = text.lastIndex(where: { $0 == " " })
let lastWord = lastWordBreak.map { text[text.index(after: $0)...] } // "playa"

text.lastIndex(of: " ") == lastWordBreak // true

把index(of:) 和 index(where:) 分别改为firstIndex(of:) 和 firstIndex(where:)

1
2
let firstWordBreak = text.firstIndex(where: { $0 == " " })
let firstWord = firstWordBreak.map { text[..<$0] } // "Vamos"

removeAll(where:)

SE-0197RangeReplaceableCollection 添加了removeAll(where:) 方法,允许你从集合中删除匹配给定条件的所有元素。

1
2
3
var numbers = Array(1...10)  // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers.removeAll(where: { $0 % 2 != 0 })
numbers // [2, 4, 6, 8, 10]

枚举类型的 CaseIterable 协议

SE-0194 — Derived Collection of Enum Cases: 编译器可以自动为枚举生成allCases属性,为您提供所有的枚举cases。你所要做的就是让你的枚举符合新的CaseIterable协议。

1
2
3
4
5
6
7
8
9
enum Terrain: CaseIterable {
case water
case forest
case desert
case road
}

Terrain.allCases // [Terrain]: [water, forest, desert, road]
Terrain.allCases.count // 4

请注意,自动生成allCases属性,仅适用于没有关联值的枚举。因为关联值的枚举意味着有无限多的可能值。

如果所有可能值的列表都是有限的,你总是可以手动实现CaseIterable协议。作为一个例子,下面是Optional怎么手动实现CaseIterable协议的:

1
2
3
4
5
6
7
8
9
extension Optional: CaseIterable where Wrapped: CaseIterable {
public typealias AllCases = [Wrapped?]
public static var allCases: AllCases {
return Wrapped.allCases.map { $0 } + [nil]
}
}

Terrain?.allCases // [Terrain?]: [water, forest, desert, road, nil]
Terrain?.allCases.count // 5

请注意,上面的例子知识一个有趣的实验,我很怀疑这样的实现在实践中是否有用。我写了一篇关于CaseIterable 的文章,详细介绍了 枚举类型的 CaseIterable 协议

随机数

在Swift中,使用随机数会有点痛苦,因为你必须直接调用C API,并且没有一个好的跨平台随机数API。

SE-0202 在标准库中添加了生成随机数的函数。

生成随机数

所有的数值类型都有一个random(in:)方法,用来生成指定范围的随机数:

1
2
3
4
Int.random(in: 1...1000)           // 858
UInt8.random(in: .min ... .max) // 112
Double.random(in: 0..<1) // 0.9178840468857299
Bool.random() // true

新的 API 可以保护你免于意外引入模数偏差,这是生成随机数时的常见错误。

Bool.random 可以像下面这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func coinToss(count tossCount: Int) -> (heads: Int, tails: Int) {
var tally = (heads: 0, tails: 0)
for _ in 0..<tossCount {
let isHeads = Bool.random()
if isHeads {
tally.heads += 1
} else {
tally.tails += 1
}
}
return tally
}

let (heads, tails) = coinToss(count: 100)

取集合中的随机元素

集合有一个randomElement方法(如果集合是空的,和minmax一样,它将返回nil):

1
2
let emotions = "😀😂😊😍🤪😎😩😭😡"
let randomEmotion = emotions.randomElement()! // "😊"

你还可以使用shuffled方法来打乱序列或者集合。

1
2
let numbers = 1...10               // ClosedRange<Int>
let shuffled = numbers.shuffled() // [Int]: [6, 10, 8, 9, 3, 4, 1, 5, 7, 2]

还有一个名为shuffle的方法。它适用于符合MutableCollection协议和RandomAccessCollection协议的所有类型:

1
2
3
var mutableNumbers = Array(numbers)  // [Int]: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
mutableNumbers.shuffle()
mutableNumbers // [Int]: [6, 2, 7, 8, 4, 10, 3, 5, 1, 9]

自定义随机数生成器

标准库附带一个默认的随机数生成器Random.default,对于大多数简单用例来说这可能是一个不错的选择。

如果你有特殊要求,你可以通过实现RandomNumberGenerator协议来实现你自己的随机数生成器。所有用于生成随机值的API都允许用户传入他们想要的随机数生成器:

1
2
3
4
5
6
7
8
9
struct MyRandomNumberGenerator: RandomNumberGenerator {
var base = Random.default
mutating func next() -> UInt64 {
return base.next()
}
}

var customRNG = MyRandomNumberGenerator()
Int.random(in: 0...100, using: &customRNG) // 34

扩展你自己的类型

您可以按照相同的模式为您自己的类型提供随机数API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Suit: String, CaseIeterable {
case diamonds = "♦"
case clubs = "♣"
case hearts = "♥"
case spades = "♠"

static func random<T: RandomNumberGenerator>(using generator: inout T) -> Suit {
return allCases.randomElement(using: &generator)
}

static func random() -> Suit {
return Suit.random(using: &Random.default)
}
}

重新设计的 Hashable 协议

在Swift 4.1(SE-0185)中引入了编译器合成的EquatableHashable一致性,大大减少了您必须手动编写的Hashable实现的数量。

但是如果你想要自己实现Hashable协议,重新设计的Hashable协议(SE-0206)使得这个任务变得更容易。

在新的Hashable世界中,您现在必须实现hash(into:)方法,而不是实现hashValue。此方法提供了一个Hasher对象,并且您在实现中需要做的所有操作都是通过反复调用hasher.combine(_:)将其添加到散列值中。

与旧方法相比的优势在于,您不必拿出自己的算法来组合类型所组成的哈希值。标准库(以Hasher的形式)提供的散列函数几乎肯定比我们大多数人写的更好,更安全。

作为一个例子,这里有一个存储属性的类型,作为一个昂贵的计算缓存。我们应该在我们的EquatableHashable实现中忽略distanceFromOrigin的值:

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
struct Point {
var x: Int { didSet { recomputeDistance() } }
var y: Int { didSet { recomputeDistance() } }

private(set) var distanceFromOrigin: Double

init(x: Int, y: Int) {
self.x = x
self.y = y
self.distanceFromOrigin = Point.distanceFromOrigin(x: x, y: y)
}

private mutating func recomputeDistance() {
distanceFromOrigin = Point.distanceFromOrigin(x: x, y: y)
}

private static func distanceFromOrigin(x: Int, y: Int) -> Double {
return Double(x * x + y * y).squareRoot()
}
}

exension Point: Equatable {
static func ==(lhs: Point, rhs: Point) -> Bool {
return lhs.x == rhs.x && lhs.y == rhs.y
}
}

在我们的hash(into:)实现中,我们所需要做的就是将相关属性提供给hasher

这比使用我们自己的散列组合函数更容易(也更高效)。例如,一个简单的hashValue实现可能是:异或两个坐标 return x ^ y。这将是一个效率较低的散列函数,因为Point(3,4)Point(4,3)将会有相同的散列值。

1
2
3
4
5
6
7
8
9
10
11
extension Point: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}

let p1 = Point(x: 3, y: 4)
let p2 = Point(x: 4, y: 3)
p1.hashValue // 3230925633424072712
p2.hashValue // 4649842771694471452

增强的条件一致性(Conditional conformance enhancements)

动态转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Optional: Equatable where Wrapped: Equatable { ... }
extension Array: Equatable where Element: Equatable { ... }
extension Dictionary: Equatable where Value: Equatable { ... }

extension Optional: Hashable where Wrapped: Hashable { ... }
extension Array: Hashable where Element: Hashable { ... }
extension Dictionary: Hashable where Value: Hashable { ... }

extension Optional: Encodable where Wrapped: Encodable { ... }
extension Array: Encodable where Element: Encodable { ... }
extension Dictionary: Encodable where Key: Encodable, Value: Encodable { ... }

extension Optional: Decodable where Wrapped: Decodable { ... }
extension Array: Decodable where Element: Decodable { ... }
extension Dictionary: Decodable where Key: Decodable, Value: Decodable { ... }
1
2
3
4
5
6
7
8
9
10
func isEncodable(_ value: Any) -> Bool {
return value is Encodable
}

let encodableArray = [1, 2, 3]
isEncodable(encodableArray) // true

struct NonEncodable {}
let nonEncodableArray = [NonEncodable(), NonEncodable()]
isEncodable(nonEncodableArray) // false

在扩展中合成一致性

对编译器合成的协议一致性有一个小而重要的改进,例如引入的自动EquatableHashable一致性。

协议一致性现在可以在扩展中进行合成,而不仅仅在类型定义上进行合并(扩展必须仍然与类型定义在同一个文件中)。这不仅仅是一个表面上的改变,因为它允许自动合成EquatableHashableEncodableDecodable的条件一致性。

这个例子来自于WWDC 2018 What’s New in Swift。我们可以有条件地遵守EquatableHashable协议:

1
2
3
4
5
6
7
8
9
enum Either<Left, Right> {
case left(Left)
case right(Right)
}

extension Either: Equatable where Left: Equatable, Right: Equatable {}
extension Either: Hashable where Left: Hashable, Right: Hashable {}

Either<Int, String>.left(42) == Either<Int, String>.left(42) // true

删除 CountableRange 和 CountableClosedRange

在 Swift 4.1 中引入条件协议一致性 (SE-0143) 允许标准库消除以前需要的大量类型,但其功能现在可以表示为基类型上的约束扩展。

例如,MutableSlice<Base> 的功能现在由“普通” Slice<Base> 以及 extension Slice: MutableCollection where Base: MutableCollection 实现。

Swift 4.2 引入了类似的范围合并。之前的具体类型 CountableRangeCountableClosedRange 已被删除,这样有利于 RangeClosedRange 的条件一致性。

可数范围类型的目的是允许范围成为集合,如果其基础元素类型是可数的(即它符合 Strideable 协议)。例如,整数范围可以是集合,但浮点数的范围不可以。

1
2
3
4
5
6
let integerRange: Range = 0..<5
let integerStrings = integerRange.map { String($0) } // ["0", "1", "2", "3", "4"]

let floatRange: Range = 0.0..<5.0
// 这里会发生错误,因为 Double 类型的范围不是一个集合
// floatRange.map { String($0) } // 报错

CountableRangeCountableClosedRange 仍然存在; 它们已转换为 typealiases 以获得源码兼容性。你不应再在新代码中使用它们。

半开放范围和闭合范围之间的区别 ClosedRange 仍然存在于类型系统级别,因为它不能如此容易地消除。ClosedRange 永远不能为空,并且 Range 永远不能包含其元素类型的最大值(例如 Int.max)。此外,对于不可数类型来说,差异很重要:将 0.0...5.0 这样的范围重写为等效的半开范围并非易事。

Dynamic member lookup

SE-0195 引入了用于类型声明的@dynamicMemberLookup属性。

@dynamicMemberLookup类型的变量可以用任何属性样式的访问器(使用点符号)调用 - 编译器不会检查具有给定名称的成员是否存在。相反,编译器会将这些访问转换为以字符串形式传递成员名称的下标访问器的调用。

此功能的目标是促进Swift和动态语言(如Python)之间的互操作性。谷歌的Swift for Tensorflow 团队推动了这一提议,实现了一个Python桥接器,可以从Swift调用Python代码。 PedroJoséPereira Vieito将其包装在名为PythonKit 的SwiftPM包中。

不需要SE-0195来实现这种互操作性,但它使得生成的Swift语法好得多。值得注意的是,SE-0195仅处理属性式成员查找(即简单的getter和setter没有参数)。动态方法调用语法的第二个“动态可调用”提议仍在工作中。

虽然Python一直是参与提案工作的人员的主要关注点,但使用Ruby或JavaScript等其他动态语言的互操作层也可以利用它。

并且它也不限于这个用例。当前具有基于字符串的下标式API的任何类型都可以转换为动态成员查找样式。SE-0195 显示了一个JSON类型,您可以使用点符号深入到嵌套字典中。

这是Doug Gregor提供的另一个例子:一种Environment类型,可以让您通过对进程环境变量的属性式访问。请注意,mutations也是有效的。

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
import Darwin

@dynamicMemberLookup
struct Environment {
subscript(dynamicMember name: String) -> String? {
get {
guard let value = getenv(name) else { return nil }
return String(validatingUTF8: value)
}
nonmutating set {
if let value = newValue {
setenv(name, value, /*overwrite:*/ 1)
} else {
unsetenv(name)
}
}
}
}

let environment = Environment()
environment.USER // "Derek"
environment.HOME // "/Users/Derek"
environment.PATH // "/usr/bin:/bin:/usr/sbin:/sbin"

environment.MY_VAR = "Hello world"
environment.MY_VAR // "Hello world"

这是一个很大的特性,但是可能会被滥用。通过在一个看起来“安全”的结构背后隐藏一个“不安全”的基于字符串的访问,您可能会给编码读者错误的印象,即编译器已经检查过这些东西。

在你自己的代码中采用这个之前,问问自己是否environment.USERenvironment["USER"]更具可读性。在大多数情况下,我认为答案应该是“不”。

隐式解包可选值

SE-0054 已于2016年3月被接受,但直到 Swift 4.2 完全实现它。

在 Swift 4.2 中,隐式解包可选值 仍然存在 - 也就是说,你可以使用 ! 代替 ? 来标注类型声明,从而将自动解包可选值。但是不再有单独的 ImplicitlyUnwrappedOptional 类型。

相反,隐式解包可选值只是普通的选项(并且具有 Optional<T> 类型),带有一个特殊的注释,告诉编译器在需要时自动添加强制解包。

官方 Swift 博客上有一篇很棒的文章,详细介绍了这种变化的含义:重新实现隐式解包可选值

guard let self = self

SE-0079 是另一个最初被 Swift 3.0 接受的提案,但需要一段时间才能实现。

它允许通过使用可选值绑定将 selfweak(和可选)变量重新绑定到强变量。那就是说,你现在可以编写 if let self = self {...} 或者 guard let self = self else {...} 在一个已经自我弱化的闭包表达式中有条件地将 self 通过 if letguard let 重新绑定到强变量上。

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
import Dispatch

struct Book {
var title: String
var author: String
}

func loadBooks(completion: @escaping ([Book]) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
DispatchQueue.main.async {
completion([
Book(title: "Harry Potter and the Deathly Hallows", author: "JK Rowling"),
Book(title: "Pippi Långstrump", author: "Astrid Lindgren")])
}
}
}

class ViewController {
var items: [Book] = []

func viewDidLoad() {
viewDidLoad()
loadBooks { [weak self] books in
guard let self = self else { return }
self.items = books
self.updateUI()
}
}

func updateUI() {
// ...
}
}

在之前的 Swift 版本中,可以通过将名称包装在反引号中来重新绑定 self,并且许多开发人员都赞成这样做而不是提出一个新名称,例如 strongSelf。 然而,这么做可以工作的事实是一个错误,而不是一个功能。

#error and #warning 指令

SE-0196 引入了#error#warning指令,用于触发源代码中的构建错误或警告。

例如,在提交代码之前使用#warning记住一个重要的TODO:

1
2
3
4
func doSomethingImportant() {
#warning("TODO: missing implementation")
}
doSomethingImportant()

如果你的代码不支持某些环境,#error会很有帮助:

1
2
3
4
5
6
7
#if canImort(UIKit)
// ...
#elseif canImport(AppKit)
// ...
#else
#error("This playground requires UIKit or AppKit")
#endif

#if compiler 版本指令

SE-0212 引入了一个编译器指令,用于使用 #if 编译时条件。

它具有与现有 #if swift(>=4.2) 语法相同的语法,但 #if 编译器 会检查实际的编译器版本,无论它运行的是哪种兼容模式,而 #if swift 是语言版本检查。

例如,在 Swift-4.0 兼容模式下运行 Swift 4.2 时,#if swift(>=4.2) 将为 false,但在这种情况下 #if compiler(>=4.2) 将为 true。该提议还有许多例子,说明 #if swift 检查可以变得多么复杂,以及 compiler 指令如何简化它们。

1
2
3
4
5
6
7
8
9
10
11
#if compiler(>=4.2)
print("Using the Swift 4.2 compiler or greater in any compatibility mode")
#endif

#if swift(>=4.2)
print("Using the Swift 4.2 compiler or greater in Swift 4.2 or greater compatibility mode")
#endif

#if compiler(>=5.0)
print("Using the Swift 5.0 compiler or greater in any compatibility mode")
#endif

MemoryLayout.offset(of:)

SE-0210MemoryLayout类型添加了一个offset(of:)的方法,补充了获取类型大小,跨度和对齐的现有API。

offset(of:)方法获取类型存储属性的key path并返回属性的字节偏移量。一个很有用的例子是将一系列交错的像素值传递给图形API。

1
2
3
4
5
6
7
struct Point {
var x: Float
var y: Float
var z: Float
}

MemoryLayout<Point>.offset(of: \Point.z) // 8

@inlinable

SE-0193 引入了两个新属性@inlinable@usableFromInline

这些不是应用程序代码所必需的。库作者可以将一些公共函数注释为@inlinable。这为编译器提供了跨模块边界优化通用代码的选项。

例如,提供一组集合算法的库可以将这些方法标记为@inlinable,以便编译器可以将使用这些算法的客户端代码专门化为在构建库时未知的类型。

示例(SE-0193中给出的示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension Sequence where Element: Equatable {
@inlinable
public func allEqual() -> Bool {
var iterator = makeIterator()
guard let first = iterator.next() else { return true }
while let next = iterator.next() {
if first != next {
return false
}
}
return true
}
}

[1,1,1,1,1].allEqual() // true
Array(repeating: 42, count: 1000).allEqual() // true
[1,1,2,1,1].allEqual() // false

在做一个函数之前仔细想一想。使用@inlinable有效地使库的公共接口的功能部分成为主体。如果稍后更改实现(例如修复一个错误),则针对旧版本编译的二进制文件可能会继续使用旧的(内联)代码,或者甚至是旧的和新的混合(因为@inlinable只是一个提示;优化程序决定每个呼叫站点是否内联代码)。

因为inlinable函数可以发送到客户端二进制文件中,所以不允许引用对客户端二进制文件不可见的声明。您可以使用@usableFromInline批注在您的库中创建特定的内部声明“ABI-public”,从而允许其在可引用函数中使用。

Immutable withUnsafePointer

这是一件小事,但如果你曾经使用withUnsafePointer(to:_:)withUnsafeBytes(of:_:)的顶层函数,你可能已经注意到他们需要他们的参数是一个可变值,因为参数是inout。

SE-0205 增加了与不变值一起工作的重载。

1
2
3
4
5
6
let x: UInt16 = 0xabcd
let (firstByte, secondByte) = withUnsafeBytes(of: x) { ptr in
(ptr[0], ptr[1])
}
String(firstByte, radix: 16) // "cd"
String(secondByte, radix: 16) // "ab"