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"

枚举类型的 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

随机数

在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

取集合中的随机元素

集合有一个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
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

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"]更具可读性。在大多数情况下,我认为答案应该是“不”。

#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

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"