Swift 4的新特性

最近写项目时,发现自己对Swift 4的一些新特性有一些没有记得很清楚。就想着把Swift4的新特性总结一遍,一来加深印象,二来方便以后查询。但这件事其实已经很多人做过了,其中最为推荐Ole Begemann
大神的What’s new in Swift 4。我下面的总结主要就是翻译下Ole Begemann
大神的What’s new in Swift 4

单侧范围

Swift4引入了一个新的RangeExpression协议和一组前缀/后缀运算符来形成单侧范围,即未指定下限或上限的范围。

无限序列

你可以使用单侧范围来构造一个无限序列,例如 当你不希望编号从零开始时,可以用来替代enumerated()

1
2
3
let letters = ["a","b","c","d"]
let numberedLetters = zip(1..., letters)
let result = Array(numberedLetters) // [(1, "a"), (2, "b"), (3, "c"), (4, "d")]

集合下标

当您使用单侧范围将下标添加到Collection中时,集合的startIndexendIndex分别填充“隐藏的下限或上限”。

1
2
let numbers = [1,2,3,4,5,6,7,8,9,10]
numbers[5...] // [6, 7, 8, 9, 10], instead of numbers[5..<numbers.endIndex]

模式匹配

单侧范围可以用于模式匹配构造,例如, 在switch语句中的case表达式中。 不过,请注意,编译器无法确定switch是详尽的。

1
2
3
4
5
6
7
8
9
10
11
let value = 5
switch value {
case 1...:
print("greater than zero")
case 0:
print("zero")
case ..<0:
print("less than zero")
default:
fatalError("unreachable")
}

Strings

多行字符串文字

在Swift 4中,你使用三个双引号(“”“)来定义多行字符串文本。在多行字符串文本中,您不需要转义单个双引号,这意味着大多数文本 格式(例如JSON或HTML)可以被粘贴,而不会有任何转义。关闭分隔符的缩进决定了每行的开头有多少空白。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let multilineString = """
This is a multi-line string.
You don't have to escape "quotes" in here.
String interpolation works as expected: 2 + 3 = \(2 + 3)
The position of the closing delimiter
controls whitespace stripping.
"""
print(multilineString)
print("---")
/*
This is a multi-line string.
You don't have to escape "quotes" in here.
String interpolation works as expected: 2 + 3 = 5
The position of the closing delimiter
controls whitespace stripping.
---
*/

在字符串文字中规避换行功能

Swift 4中你可以通过在行末加反斜杠\,来规避换行功能。

1
2
3
4
5
6
7
8
9
10
let escapedNewline = """
To omit a line break, \
add a backslash at the end of a line.
"""
print(escapedNewline)
print("---")
/*
To omit a line break, add a backslash at the end of a line.
---
*/

字符串又是一个集合了

Swift 4中最大的变化是String再次成为一个Collection(因为它曾经在Swift 1.x中是),即String.CharacterView的功能已经被废弃了。(UnicodeScalarViewUTF8ViewUTF16View仍然存在。)

另一个与字符串相关的更改是对String.Index类型的重新设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let greeting = "Hello, 😜!"
// No need to drill down to .characters
greeting.count
for char in greeting {
print(char)
}
print("---")
/*
H
e
l
l
o
,

😜
!
---
*/

子字符串(Substring)是字符串切片的新类型

字符串切片现在是Substring类型的实例。 StringSubstring都遵守StringProtocol协议。 几乎整个字符串API将存在于StringProtocol中,以便StringSubstring的行为基本相同。

1
2
3
4
5
6
let greeting = "Hello, 😜!"
let comma = greeting.index(of: ",")!
let substring = greeting[..<comma]
type(of: substring) // Substring
// Most String APIs can be called on Substring
print(substring.uppercased()) // HELLO

一个Substring保持完整字符串值。 这可能会导致意外的高内存使用情况,当您传递一个看起来很小的Substring时,可能将一个很大的String保存到其他API中。 由于这个原因,大多数将字符串作为参数的函数应该只继续接受String类型的参数; 你通常不应该让这样的函数通用接受任何符合StringProtocol的值。

要将Substring转换回String,请使用String()初始化方法。这会将子字符串复制到新的缓冲区中:

1
2
let newString = String(substring)
type(of: newString) // String

Swift 4将一些标准库API更改为使用String而不是StringProtocol。目的就是避免因为使用字符串切片而导致的不必要的内存开销。

Unicode 9

Swift 4支持Unicode 9,下面所有的字符数都是正确的(而在Swift 3中并不是):

1
2
3
4
5
"👧🏽".count // person + skin tone;  1 (in Swift 3: 2)
"👨‍👩‍👧‍👦".count // family with four members; 1 (in Swift 3: 4)
"👱🏾\u{200D}👩🏽\u{200D}👧🏿\u{200D}👦🏻".count // family + skin tones; 1 (in Swift 3: 8)
"👩🏻‍🚒".count // person + skin tone + profession; 1 (in Swift 3: 3)
"🇨🇺🇬🇫🇱🇨".count // multiple flags; 3 (in Swift 3: 1)

Character.unicodeScalars属性

您现在可以直接访问字符的unicode标量值,而无需首先将其转换为字符串:

1
2
let c: Character = "🇪🇺"
Array(c.unicodeScalars) // [127466, 127482]

在Range和NSRange之间转换

Foundation在NSRangeRange<String.Index>上都添加了新的初始化方法,以便在两者之间进行转换,无需手动计算UTF-16偏移量。这样可以更容易地使用含有NSRanges的API,例如NSRegularExpressionNSAttributedString

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
// Given a String range
let string = "Hello 👩🏽‍🌾👨🏼‍🚒💃🏾"
let index = string.index(of: Character("👩🏽‍🌾"))!
let range = index...

// Convert the String range to an NSRange
let nsRange = NSRange(range, in: string) // {6, 18}
nsRange.length // length in UTF-16 code units // 18
string[range].count // length in Characters // 3
assert(nsRange.length == string[range].utf16.count)

// Use the NSRange to format an attributed string
let formatted = NSMutableAttributedString(string: string, attributes: [.font: UIFont.systemFont(ofSize: 14)])
formatted.addAttribute(.font, value: UIFont.systemFont(ofSize: 48), range: nsRange)

// NSAttributedString APIs return NSRange
let lastCharacterIndex = string.index(before: string.endIndex)
let lastCharacterNSRange = NSRange(lastCharacterIndex..., in: string) // {20, 4}
var attributesNSRange = NSRange()
_ = formatted.attributes(at: lastCharacterNSRange.location, longestEffectiveRange: &attributesNSRange, in: nsRange)
attributesNSRange // {6, 18}

// Convert the NSRange back to Range<String.Index> to use it with String
let attributesRange = Range(attributesNSRange, in: string)!
string[attributesRange] // 👩🏽‍🌾👨🏼‍🚒💃🏾

私有声明在相同文件的扩展中可见

Swift 4更改了访问控制规则,以便私有声明在同一文件中相同类型的扩展内部也可见。这样可以将您的类型定义分配到多个扩展中,并且仍然可以对大多数“私有”成员使用private,从而减少对不受欢迎的fileprivate关键字的需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct SortedArray<Element: Comparable> {
private var storage: [Element] = []
init(unsorted: [Element]) {
storage = unsorted.sorted()
}
}

extension SortedArray {
mutating func insert(_ element: Element) {
// storage is visible here
storage.append(element)
storage.sort()
}
}

let array = SortedArray(unsorted: [3,1,2])

// storage is _not_ visible here. It would be if it were fileprivate.
array.storage // error: 'storage' is inaccessible due to 'private' protection level

智能key paths

Swift 4的主要功能之一是新的key paths模型。与Cocoa中基于字符串的key paths不同,Swift中的key paths是强类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Person {
var name: String
}

struct Book {
var title: String
var authors: [Person]
var primaryAuthor: Person {
return authors.first!
}
}

let abelson = Person(name: "Harold Abelson")
let sussman = Person(name: "Gerald Jay Sussman")
let book = Book(title: "Structure and Interpretation of Computer Programs", authors: [abelson, sussman])

Key paths是通过从根类型开始,并逐层向下由属性和下标名称的任意组合来形成的。

你可以用一个反斜杠\开始写一个key path\Book.title。每种类型都会自动获得[keyPath: ...]下标方法,用来获取或设置指定key path的值。

1
2
3
4
book[keyPath: \Book.title]  // "Structure and Interpretation of Computer Programs"
// Key paths支持多层
// 同时Key paths也适用于计算属性
book[keyPath: \Book.primaryAuthor.name] // "Harold Abelson"

Key paths是可以存储和操作的对象。例如,您可以直接向key path添加信息来生成新的key path

1
2
3
let authorKeyPath = \Book.primaryAuthor  //  type: KeyPath<Book, Person>
let nameKeyPath = authorKeyPath.appending(path: \.name) // type: KeyPath<Book, String>
book[keyPath: nameKeyPath] // "Harold Abelson"

key paths支持下标访问

Key paths可以包含下标。这将使他们非常方便操作Collection

1
book[keyPath: \Book.authors[0].name]  // "Harold Abelson"

用key paths实现的类型安全KVO

Foundation中的key-value observing的API已经改进,充分利用了新的类型安全key paths。它比旧的API更容易使用。

请注意,KVO依赖于Objective-C runtime机制。它只能在NSObject的子类中使用,并且必须使用@objc dynamic声明属性。

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
34
35
36
37
38
39
40
class Child: NSObject {
let name: String
// KVO-enabled properties must be @objc dynamic
@objc dynamic var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
super.init()
}

func celebrateBirthday() {
age += 1
}
}

// Set up KVO
let mia = Child(name: "Mia", age: 5)
let observation = mia.observe(\.age, options: [.initial, .old]) { (child, change) in
if let oldValue = change.oldValue {
print("\(child.name)’s age changed from \(oldValue) to \(child.age)")
} else {
print("\(child.name)’s age is now \(child.age)")
}
}


// Trigger KVO (see output in the console)
mia.celebrateBirthday()

// Deiniting or invalidating the observation token ends the observation
observation.invalidate()

// This doesn't trigger the KVO handler anymore
mia.celebrateBirthday()

/*
Mia’s age is now 5
Mia’s age changed from 5 to 6
*/

存档和序列化

Swift 4为Swift类型(类,结构和枚举)的存档和序列化定义了一种方式,来描述如何存档和序列化自身。类型可以通过是否遵守Codable协议使自己(不可)可存档。

在许多情况下,你只需要添加遵守Codable协议即可,因为如果所有类型的成员都是Codable,编译器可以生成默认实现。如果您需要自定义您的类型如何编码自己,您还可以覆盖默认行为。这个主题有很多,请务必阅读提案以了解详情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Make a custom type archivable by conforming it (and all its members) to Codable
struct Card: Codable, Equatable {
enum Suit: String, Codable {
case clubs, spades, hearts, diamonds
}

enum Rank: Int, Codable {
case two = 2, three, four, five, six, seven, eight, nine, ten, jack, queen, king, ace
}

var suit: Suit
var rank: Rank

static func ==(lhs: Card, rhs: Card) -> Bool {
return lhs.suit == rhs.suit && lhs.rank == rhs.rank
}
}

let hand = [Card(suit: .clubs, rank: .ace), Card(suit: .hearts, rank: .queen)]

编码(encoding)

一旦你有了一个Codable值,你需要将它传递给一个编码器来存档它。

您可以编写自己的编码器和解码器,它们可以使用Codable基础结构,但Swift为JSON(JSONEncoderJSONDecoder)以及属性列表(PropertyListEncoderPropertyListDecoder)提供一组内置的编码器和解码器。NSKeyedArchiver也将支持所有的Codable类型。

最简单的情况,编码只是两行代码:创建一个编码器并要求它对您的值进行编码。大多数编码器都包含可以设置用于自定义输出的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var encoder = JSONEncoder()

// Optional properties offered by JSONEncoder for customizing output
encoder.outputFormatting = [.prettyPrinted] // another option: .sortedKeys
encoder.dataEncodingStrategy // base64
encoder.dateEncodingStrategy // deferredToDate
encoder.nonConformingFloatEncodingStrategy // throw

// Every encoder and decoder has a userInfo property to pass custom configuration down the chain. The supported keys depend on the specific encode/decoder.
encoder.userInfo // [:]

let jsonData = try encoder.encode(hand)
String(data: jsonData, encoding: .utf8)
// "[\n {\n \"rank\" : 14,\n \"suit\" : \"clubs\"\n },\n {\n \"rank\" : 12,\n \"suit\" : \"hearts\"\n }\n]"

解码(decoding)

与编码一样,解码包含两个步骤:创建一个解码器并将其传递给一个数据值进行解码。请注意,在本例中,您还传递了解码值的预期类型([Card]Array <Card>)。不要忘记处理生产代码中的解码错误。

1
2
3
4
let decoder = JSONDecoder()
let decodedHand = try decoder.decode([Card].self, from: jsonData)
type(of: decodedHand) // Array<Card>
assert(decodedHand == hand)

关联类型约束

协议中的关联类型现在可以用where子句来约束。这个看似微小的改变使得类型系统更具表现力,并且有助显著简化标准库。特别是在Swift 4中,使用SequenceCollection更直观。

Sequence.Element

Sequence现在有它自己的Element关联类型。这可以通过新的泛型特性实现,因为现在可以在类型系统中这么表示:associatedType Element where Element == Iterator.Element

Swift 3中写Iterator.Element,你现在都可以编写Element

1
2
3
4
5
6
7
8
9
10
11
extension Sequence where Element: Numeric {
var sum: Element {
var result: Element = 0
for element in self {
result += element
}
return result
}
}

[1,2,3,4].sum

另一个例子:在Swift 3中,这个扩展需要更多的约束,因为类型系统不能表达Collection的关联类型Indices元素与Collection.Index具有相同类型的想法:

Example
// Required in Swift 3
extension MutableCollection where Index == Indices.Iterator.Element {

1
2
3
4
5
6
7
8
9
extension MutableCollection {
/// Maps over the elements in the collection in place, replacing the existing
/// elements with their transformed values.
mutating func mapInPlace(_ transform: (Element) throws -> Element) rethrows {
for index in indices {
self[index] = try transform(self[index])
}
}
}

字典(Dictionary)和集合(Set)功能的加强

在Swift 4中,DictionarySet增加了一些不错的功能。

基于序列值的初始化方法

使用键值对创建一个字典。

1
2
3
let names = ["Cagney", "Lacey", "Bensen"]
let dict = Dictionary(uniqueKeysWithValues: zip(1..., names)) // [2: "Lacey", 3: "Bensen", 1: "Cagney"]
dict[2] // "Lacey"

合并的初始化方法和合并方法

指定从序列创建字典或将序列合并到现有字典时应如何处理重复键。

合并的初始化方法:

1
2
3
let duplicates = [("a", 1), ("b", 2), ("a", 3), ("b", 4)]
let letters = Dictionary(duplicates, uniquingKeysWith: { (first, _) in first })
letters // ["b": 2, "a": 1]

合并方法:

1
2
3
4
let defaults = ["darkUI": false, "energySaving": false, "smoothScrolling": false]
var options = ["darkUI": true, "energySaving": false]
options.merge(defaults) { (old, _) in old }
options // ["darkUI": true, "energySaving": false, "smoothScrolling": false]

具有默认值的下标方法

您可以提供一个默认值作为下标方法key不存在的返回值,从而使返回类型non-optional

1
dict[4, default: "(unknown)"]

当你想通过下标方法改变一个值时,这是特别有用的:

1
2
3
4
5
6
let source = "how now brown cow"
var frequencies: [Character: Int] = [:]
for c in source {
frequencies[c, default: 0] += 1
}
frequencies // ["b": 1, "w": 4, "r": 1, "c": 1, "n": 2, "o": 4, " ": 3, "h": 1]

字典的map和filter

filter返回一个Dictionary而不是Array。同样,新的mapValues方法在保留字典结构的同时转换值。

1
2
3
4
5
6
7
8
9
let dict: [Int: String] = [2: "Lacey", 3: "Bensen", 1: "Cagney"]
let filtered = dict.filter {
$0.key % 2 == 0
}
filtered // [2: "Lacey"]
let mapped = dict.mapValues { value in
value.uppercased()
}
mapped // [2: "LACEY", 3: "BENSEN", 1: "CAGNEY"]

集合的filter也是返回Set类型而不是Array

1
2
3
4
let set: Set = [1,2,3,4,5]
let filteredSet = set.filter { $0 % 2 == 0 }
type(of: filteredSet) // Set<Int>
filteredSet // {2, 4}

组合序列值

将一系列值组合成新的数据,例如 按照首字母在单词列表中划分单词。这是我的最爱例子之一。

1
2
3
let contacts = ["Julia", "Susan", "John", "Alice", "Alex"]
let grouped = Dictionary(grouping: contacts, by: { $0.first! })
grouped // ["J": ["Julia", "John"], "S": ["Susan"], "A": ["Alice", "Alex"]]

MutableCollection.swapAt的方法

Swift 4引入了一种新的方法,用于交换集合中的两个元素。与之前的swap(_:_:)函数不同,swapAt(_:_:)取得要交换的元素的索引,而不是元素本身(通过inout参数)。

现有的swap(_:_:)函数将不再用于交换同一集合中的两个元素。

1
2
3
4
5
6
7
8
9
var numbers = [1,2,3,4,5]

// Illegal in Swift 4:
// error: simultaneous accesses to var 'numbers', but modification requires exclusive access; consider copying to a local variable
// swap(&numbers[3], &numbers[4])

// This is the new way to do this:
numbers.swapAt(0,1)
numbers // [2, 1, 3, 4, 5]

reduce with inout

Swift 4添加了reduce的变体,其中部分结果被传递给组合函数。对于使用reduce通过消除中间结果的副本逐步构建序列的算法,这可能会显着提高性能。

1
2
3
4
5
6
7
8
9
10
11
extension Sequence where Element: Equatable {
func removingConsecutiveDuplicates() -> [Element] {
return reduce(into: []) { (result: inout [Element], element) in
if result.last != element {
result.append(element)
}
}
}
}

[1,1,1,2,3,3,4].removingConsecutiveDuplicates() // [1, 2, 3, 4]

通用的下标

Swift 4中,下标可以具有通用参数和/或返回类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct JSON {
fileprivayte var storage: [String : Any]

init(dictionary: [String: Any]) {
self.storage = dictionary
}

subscript<T>(key: String) -> T? {
return storage[key] as? T
}
}

let json = JSON(dictionary: [
"name": "Berlin",
"country": "de",
"population": 3_500_500
])

// 不需要使用 as? Int
let population: Int? = json["population"]
1
2
3
4
5
6
7
8
9
10
11
extension Collection {
subscript<Indices: Sequence>(indices indices: Indices) -> [Element] where Indices.Element == Index {
var result: [Element] = []
for index in indices {
result.append(self[index])
}
}
}

let words = "I love Swift.".split(separator: " ")
words[indices: [0, 2]] // ["I", "Swift."]

New integer protocols

现在,您可以将Int与UInt进行比较而无需进行显式转换,这要归功于标准库中这些函数的新的通用重载方法。

1
2
3
4
5
let a: Int = 5
let b: UInt = 5
let c: Int8 = -10
a == b // 在Swift 3 中编译不通过
a > c // 在Swift 3 中编译不通过

请注意,混合类型算术任然是不允许的。
a + b // 在Swift 3 和 4都是不可用正常编译的

获取有关位模式的信息

1
2
3
4
0xFFFF.words
0b11001000.trailingZeroBitCount // 3
(0b00001100 as UInt8).leadingZeroBitCount // 4
0b110001.nonzeroBitCount // 3

算术溢出报告

1
2
3
let (partialValue, overflow) = Int32.max.addingReportingOverflow(1)
partialValue // -2147483648
overflow // true

计算可能会溢出的乘法的完整结果

1
2
3
4
5
let x: UInt8 = 100
let y: UInt8 = 20
let result = x.multipliedFullWidth(by: y)
result.high // 7
result.low // 208

NSNumber bridging

1
2
3
4
5
6
7
import Foundation

let n = NSNumber(value: UInt32(543))
let v = n as? Int8 // nil in Swift 4. 31 in Swift 3.

NSNumber(value: 0.1) as? Double // 0.1
NSNumber(value: 0.1) as? Float // nil

Limiting @objc inference

在很多Swift 3 默认为@objc(对Objective-C可见)声明的地方,Swift 4不再这样做。最重要的是,NSObject子类不再为其成员默认添加@objc声明。

1
2
3
4
5
6
import Foundation

class MyClass: NSObject {
func foo() { } // 在Swift 4中,对Objective-C不可见
@objc func bar() { } // 对Objective-C可见
}

你可以使用@objcMembers来声明类中的所有成员是@objc

1
2
3
4
5
@objcMembers
class MyClass2: NSObject {
func foo() { } // 隐式添加@objc
func bar() -> (Int, Int) { return (0, 0) } // 不会添加@objc,因为Objective-C不支持tuples
}

在扩展中使用@nonobjc可禁用扩展中的所有声明的@objc推断(通过@objcMembers获得的@objc)。

1
2
3
4
@nonobjc extension MyClass2 {
func wobble() { } // 尽管MyClass2是@objcMembers,但是wobble方法不是@objc

}

Composing classes and protocols

您现在可以在Swift中编写类似Objective-C代码UIViewController <SomeProtocol> *的代码,即声明具体类型的变量并将其同时约束为一个或多个协议。语法是let variable: SomeClass &SomeProtocol 1 &SomeProtocol2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import UIKit

protocol HeaderView {}

class ViewController: UIViewController {
let header: UIView & HeaderView

init(header: UIView & HeaderView) {
self.header = header
super.init(nibName: nil, bundle: nil)
}

required init(coder decoder: NSCoder) {
fatalError("not implemented")
}
}

ViewController(header: UIView()) // error: 'UIView' does not conform to expected type 'UIView & HeaderView'

extension UIImageView: HeaderView {}
ViewController(header: UIIImageView()) // works