Swift 4.2 更新指北(译)

一、概述

好消息,Swift 4.2 在 Xcode 10 beta 版上可以使用了,在 Swift 4.1 的基础上更新了很多语言特性,为 Swift 5 中 ABI 稳定做好准备。

这篇文章包含了 Swift 4.2 中的重大的改变。因为 Swift 4.2 需要 Xcode 10,所以请下载安装最新的 Xcode 测试版本。

二、准备

Swift 4.2 和 Swift 4.1 源码兼容,但是和其他发布版本的二进制不兼容。Swift 4.2 是 Swift 5 实现 ABI 稳定(不同的 Swift 版本编译的应用程序和库之间实现兼容)的一个中间阶段。ABI 的特性在集成进最终的 ABI 之前会接收社区的大量反馈。

三、语言演进

在这个版本中有很多新的语言特性。例如,随机数生成,动态成员查找等等

3.1 随机数生成

3.1.1 随机数生成

arc4random_uniform(_:) 返回一个 0 - 9 之间的随机数字。这种实现方式有两个问题:

  • 需要引入 Foundation 框架,在 Linux 下无法工作。
  • Linux 上的随机数生成会产生模偏差(有取模的过程,更容易随机到小的数)。
// Swift 4.1
let digit = Int(arc4random_uniform(10))

Swift 4.2 在标准库中添加了随机数的 API SE-0202

// Swift 4.2
/ 1  
let digit = Int.random(in: 0..<10)
​
// 2
if let anotherDigit = (0..<10).randomElement() {
 print(anotherDigit)
} else {
 print("Empty range.")
}
​
// 3
let double = Double.random(in: 0..<1)
let float = Float.random(in: 0..<1)
let cgFloat = CGFloat.random(in: 0..<1)
let bool = Bool.random()

注:randomElement() 如果 range 是空,返回 nil

3.1.2 数组随机

Swift 4.1 数组随机也是采用 C 函数的形式,这种方式会存在上面提到的问题,而且会存在 Int 和 Int32 转换的问题。

let playlist = ["Nothing Else Matters", "Stairway to Heaven", "I Want to Break Free", "Yesterday"]
let index = Int(arc4random_uniform(UInt32(playlist.count)))
let song = playlist[index]

Swift 4.2 采用了更加简单直接的方式。

if let song = playlist.randomElement() {
 print(song)
} else {
 print("Empty playlist.")
}
3.1.3 洗牌算法

Swift 4.1 不包含任何集合的洗牌算法,所以要采用比较曲折的方式来实现。

// 1
let shuffledPlaylist = playlist.sorted{ _, _ in arc4random_uniform(2) == 0 }
​
// 2
var names = ["Cosmin", "Oana", "Sclip", "Nori"]
names.sort { _, _ in arc4random_uniform(2) == 0 }

Swift 4.2 提供了更加高效更加优雅的实现 Shuffling Algorithms

let shuffledPlaylist = playlist.shuffled()
names.shuffle()

注:使用 shuffled() 来创建一个洗牌后的数组。使用 shuffle) 来将数组洗牌。

3.2 动态成员查找

Swift 4.1 使用下面的方式实现自定义下标操作。

class Person {
  let name: String
  let age: Int
  private let details: [String: String]
  
  init(name: String, age: Int, details: [String: String]) {
    self.name = name
    self.age = age
    self.details = details
  }
  
  subscript(key: String) -> String {
    switch key {
      case "info":
        return "\(name) is \(age) years old."
      default:
        return details[key] ?? ""
    }
  }
}

let details = ["title": "Author", "instrument": "Guitar"]
let me = Person(name: "Cosmin", age: 32, details: details)
me["info"]   // "Cosmin is 32 years old."
me["title"]  // "Author"

Swift 4.2 使用动态成员查找来提供点语法来实现下标调用 Dynamic Member Lookup

// 1
@dynamicMemberLookup
class Person {
  let name: String
  let age: Int
  private let details: [String: String]
  
  init(name: String, age: Int, details: [String: String]) {
    self.name = name
    self.age = age
    self.details = details
  }
  
  // 2
  subscript(dynamicMember key: String) -> String {
    switch key {
      case "info":
        return "\(name) is \(age) years old."
      default:
        return details[key] ?? ""
    }
  }
}

// 3
me.info   // "Cosmin is 32 years old." 
me.title  // "Author"

使用步骤:

  • 标记 Person 为 @dynamicMemberLookup 使下标可以使用点语法
  • 遵守 @dynamicMemberLookup 实现 subscript(dynamicMember:) 方法
  • 使用点语法调用之前定义的下标

注:编译器会在运行时动态评估下标的调用,这样就可以写出像 Python 或者 Ruby 等脚本语言一样类型安全的代码。

动态成员查找不会和类的属性混淆。

me.name // "Cosmin"
me.age // 32

可以使用点语法而非下标来调用 name 和 age。而且派生类可以继承基类的动态成员查找。

@dynamicMemberLookup
class Vehicle {
  let brand: String
  let year: Int
  
  init(brand: String, year: Int) {
    self.brand = brand
    self.year = year
  }
  
  subscript(dynamicMember key: String) -> String {
    return "\(brand) made in \(year)."
  }
}

class Car: Vehicle {}

let car = Car(brand: "BMW", year: 2018)
car.info  // "BMW made in 2018."

可以通过协议拓展给已有类型添加动态成员查找

// 1
@dynamicMemberLookup
protocol Random {}

// 2
extension Random {
  subscript(dynamicMember key: String) -> Int {
    return Int.random(in: 0..<10)
  }
}

// 3
extension Int: Random {}

// 4
let number = 10
let randomDigit = String(number.digit)
let noRandomDigit = String(number).filter { String($0) != randomDigit }

3.3 枚举实例集合

Swift 4.1 默认没有提供访问枚举实例集合的方式,所以实现方式不是很优雅。

enum Seasons: String {
  case spring = "Spring", summer = "Summer", autumn = "Autumn", winter = "Winter"
}

enum SeasonType {
  case equinox
  case solstice
}

let seasons = [Seasons.spring, .summer, .autumn, .winter]
for (index, season) in seasons.enumerated() {
  let seasonType = index % 2 == 0 ? SeasonType.equinox : .solstice
  print("\(season.rawValue) \(seasonType).")
}

为了解决这个问题,Swift 4.2 给枚举类型添加了实例数组。

// 1
enum Seasons: String, CaseIterable {
  case spring = "Spring", summer = "Summer", autumn = "Autumn", winter = "Winter"
}

enum SeasonType {
  case equinox
  case solstice
}

// 2
for (index, season) in Seasons.allCases.enumerated() {
  let seasonType = index % 2 == 0 ? SeasonType.equinox : .solstice
  print("\(season.rawValue) \(seasonType).")
}

如果枚举中包含 unavailable,需要将 availablecase 手动维护协议中的 allCases

enum Days: CaseIterable {
  case monday, tuesday, wednesday, thursday, friday
  
  @available(*, unavailable)
  case saturday, sunday
  
  static var allCases: [Days] {
    return [.monday, .tuesday, .wednesday, .thursday, .friday]
  }
}

allCases 中只能添加 weekdays,因为 saturdaysunday 被标记为各个平台不可用。
枚举实例数组中也可以添加有关联值的实例。

enum BlogPost: CaseIterable {
  case article
  case tutorial(updated: Bool)
  
  static var allCases: [BlogPost] {
    return [.article, .tutorial(updated: true), .tutorial(updated: false)]
  }
}

3.4 新的序列方法

Swift 4.1 中的 Sequence 定义了查找指定元素的第一个索引位置或者满足指定条件的第一个元素的方法。

let ages = ["ten", "twelve", "thirteen", "nineteen", "eighteen", "seventeen", "fourteen",  "eighteen", "fifteen", "sixteen", "eleven"]

if let firstTeen = ages.first(where: { $0.hasSuffix("teen") }), 
   let firstIndex = ages.index(where: { $0.hasSuffix("teen") }), 
   let firstMajorIndex = ages.index(of: "eighteen") {
  print("Teenager number \(firstIndex + 1) is \(firstTeen) years old.")
  print("Teenager number \(firstMajorIndex + 1) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

Swift 4.2 为了实现一致性重构了方法名

if let firstTeen = ages.first(where: { $0.hasSuffix("teen") }), 
   let firstIndex = ages.firstIndex(where: { $0.hasSuffix("teen") }), 
   let firstMajorIndex = ages.firstIndex(of:  "eighteen") {
  print("Teenager number \(firstIndex + 1) is \(firstTeen) years old.")
  print("Teenager number \(firstMajorIndex + 1) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

Swift 4.1 也没有定义查找指定元素的最后一个索引的位置和满足指定条件的的最后一个元素等方法。在 Swift 4.1 中我们可能采用下面的方法来处理。

// 1
let reversedAges = ages.reversed()

// 2
if let lastTeen = reversedAges.first(where: { $0.hasSuffix("teen") }), 
   let lastIndex = reversedAges.index(where: { $0.hasSuffix("teen") })?.base, 
   let lastMajorIndex = reversedAges.index(of: "eighteen")?.base {
  print("Teenager number \(lastIndex) is \(lastTeen) years old.")
  print("Teenager number \(lastMajorIndex) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

Swift 4.2 添加了相应的方法,使用方式如下

if let lastTeen = ages.last(where: { $0.hasSuffix("teen") }), 
   let lastIndex = ages.lastIndex(where: { $0.hasSuffix("teen") }), 
   let lastMajorIndex = ages.lastIndex(of: "eighteen") {
  print("Teenager number \(lastIndex + 1) is \(lastTeen) years old.")
  print("Teenager number \(lastMajorIndex + 1) isn't a minor anymore.")
} else {
  print("No teenagers around here.")
}

3.5 检测序列元素

Swift 4.1 中没有检查序列中所有元素是否满足某个指定条件的方法。不过你可以实现你自己的方法,例如下面检测集合中的元素是否都是偶数。

let values = [10, 8, 12, 20]
let allEven = !values.contains { $0 % 2 == 1 }

Swift 4.2 添加了新的方法,很好的简化了代码,提升了可读性。

let allEven = values.allSatisfy { $0 % 2 == 0 }

3.6 条件遵守更新

Swift 4.2 给拓展和标准库中添加一些条件遵守方面的改进。

3.6.1 拓展中的条件遵守

Swift 4.1 不能在拓展中自动合成 Equatable 的协议实现。例子如下:

// 1
struct Tutorial : Equatable {
  let title: String
  let author: String
}

// 2
struct Screencast {
  let author: String
  let tutorial: Tutorial
}

// 3 
extension Screencast: Equatable where Tutorial: Equatable {
  // 必须自己实现 == 方法,Swift 4.1 不会自动合成
  static func ==(lhs: Screencast, rhs: Screencast) -> Bool {
    return lhs.author == rhs.author && lhs.tutorial == rhs.tutorial
  }
}

// 4
let swift41Tutorial = Tutorial(title: "What's New in Swift 4.1?", author: "Cosmin Pupăză")
let swift42Tutorial = Tutorial(title: "What's New In Swift 4.2?", author: "Cosmin Pupăză")
let swift41Screencast = Screencast(author: "Jessy Catterwaul", tutorial: swift41Tutorial)
let swift42Screencast = Screencast(author: "Jessy Catterwaul", tutorial: swift42Tutorial)
let sameScreencast = swift41Screencast == swift42Screencast

Swift 4.2 只需要遵守协议,不需要实现。因为编译器会添加一个默认的Equatable协议实现。

extension Screencast: Equatable where Tutorial: Equatable {}

这个特性也同样支持 HashableCodable

// 1
struct Tutorial: Hashable, Codable {
  let title: String
  let author: String
}

struct Screencast {
  let author: String
  let tutorial: Tutorial
}

// 2
extension Screencast: Hashable where Tutorial: Hashable {}
extension Screencast: Codable where Tutorial: Codable {}

// 3
let screencastsSet: Set = [swift41Screencast, swift42Screencast]
let screencastsDictionary = [swift41Screencast: "Swift 4.1", swift42Screencast: "Swift 4.2"]

let screencasts = [swift41Screencast, swift42Screencast]
let encoder = JSONEncoder()
do {
  try encoder.encode(screencasts)
} catch {
  print("\(error)")
}
3.6.2 条件遵守运行时查询

Swift 4.2 实现条件遵守的动态查询。可以从下面的例子看出。

// 1
class Instrument {
  let brand: String
  
  init(brand: String = "") {
    self.brand = brand
  }
}

// 2
protocol Tuneable {
  func tune()
}

// 3
class Keyboard: Instrument, Tuneable {
  func tune() {
    print("\(brand) keyboard tuning.")
  }
}

// 4
extension Array: Tuneable where Element: Tuneable {
  func tune() {
    forEach { $0.tune() }
  }
}

// 5
let instrument = Instrument()
let keyboard = Keyboard(brand: "Roland")
let instruments = [instrument, keyboard]

// 6
if let keyboards = instruments as? Tuneable {
  keyboards.tune()
} else {
  print("Can't tune instrument.")
}

注:上面在条件遵循的运行时检测中,会输出 "Can't tune instrument.",因为 Instrument类型不遵守 Tuneable 协议,如果是两个 Keyboard 类型就可以。
更多关于 Conditional Conformance 的内容,参考 Swift 4.1 更新指北(译)

3.6.3 Hashable 在标准库中条件遵守增强

在 Swift 4.2 中可选值、数组、字典和区间当他们的元素是 Hashable 的话,他们也是 Hashable

struct Chord: Hashable {
  let name: String
  let description: String?
  let notes: [String]
  let signature: [String: [String]?]
  let frequency: CountableClosedRange
}

let cMajor = Chord(name: "C", description: "C major", notes: ["C", "E",  "G"], 
                   signature: ["sharp": nil,  "flat": nil], frequency: 432...446)
let aMinor = Chord(name: "Am", description: "A minor", notes: ["A", "C", "E"], 
                   signature: ["sharp": nil, "flat": nil], frequency: 440...446)
let chords: Set = [cMajor, aMinor]
let versions = [cMajor: "major", aMinor: "minor"]

3.7 Hashable 增强

Swift 4.1 中一般会像下面这样实现自定义哈希函数:

class Country: Hashable {
  let name: String
  let capital: String
  
  init(name: String, capital: String) {
    self.name = name
    self.capital = capital
  }
  
  static func ==(lhs: Country, rhs: Country) -> Bool {
    return lhs.name == rhs.name && lhs.capital == rhs.capital
  }
  
  var hashValue: Int {
    return name.hashValue ^ capital.hashValue &* 16777619
  }
}

let france = Country(name: "France", capital: "Paris")
let germany = Country(name: "Germany", capital: "Berlin")
let countries: Set = [france, germany]
let countryGreetings = [france: "Bonjour", germany: "Guten Tag"]

因为countriesHashable ,所以可以添加到集合或者字典中。但是 hashValue 的实现很难理解并且也不高效。Swift 4.2 通过定义了一个通用的哈希函数来解决这个问题。

class Country: Hashable {
  let name: String
  let capital: String
  
  init(name: String, capital: String) {
    self.name = name
    self.capital = capital
  }
  
  static func ==(lhs: Country, rhs: Country) -> Bool {
    return lhs.name == rhs.name && lhs.capital == rhs.capital
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(name)
    hasher.combine(capital)
  }
}

Country 中使用 hash(into:) 来替代 hashValue。这个函数使用 combine() 将属性注入到 hasher中。
注:现在实现上很容易,并且性能要比之前的版本高。

3.8 集合中移除元素

在 Swift 4.1 中,想要从集合中移除一个指定的元素,通常会使用 filter(_:) 的实现方式,在 Swift 4.2 添加了 removeAll(_:)

// Swift 4.1
var greetings = ["Hello", "Hi", "Goodbye", "Bye"]
greetings = greetings.filter { $0.count <= 3 }
// Swift 4.2
greetings.removeAll { $0.count > 3 }

3.9 更改布尔值

在 Swift 4.1 中,我们通常会这样实现

extension Bool {
  mutating func toggle() {
    self = !self
  }
}

var isOn = true
isOn.toggle()

Swift 4.2 给 Bool 增加了 toggle()方法

3.10 新的编译器指令

Swift 4.2 定义了表述代码问题的编译器指令

// 1
#warning("There are shorter implementations out there.")

let numbers = [1, 2, 3, 4, 5]
var sum = 0
for number in numbers {
  sum += number
}
print(sum)

// 2
#error("Please fill in your credentials.")

let username = ""
let password = ""
switch (username.filter { $0 != " " }, password.filter { $0 != " " }) {
  case ("", ""):
    print("Invalid username and password.")
  case ("", _):
    print("Invalid username.")
  case (_, ""):
    print("Invalid password.")
  case (_, _):
    print("Logged in succesfully.")
}
  • #warning 用来输出警告信息,表示实现未完全完成
  • #error 强制其他开发者填入 usernamepassword

3.11 新的指针函数

withUnsafeBytes(of:_:)withUnsafePointer(to:_:) 在 Swift 4.1 中只能用于变量,所以必须拷贝一份。Swift 4.2 中该函数支持常量,不需要再保存值。

// Swift 4.1
let value = 10
var copy = value
withUnsafeBytes(of: ©) { pointer in print(pointer.count) }
withUnsafePointer(to: ©) { pointer in print(pointer.hashValue) }
// Swift 4.2
withUnsafeBytes(of: value) { pointer in print(pointer.count) }
withUnsafePointer(to: value) { pointer in print(pointer.hashValue) }

3.12 Memory Layout 更新

Swift 4.2 使用 keypath 查找存储属性的内存布局 [SE-0210],具体做法如下:

// 1
struct Point {
  var x, y: Double
}

// 2
struct Circle {
  var center: Point
  var radius: Double
  
  var circumference: Double {
    return 2 * .pi * radius
  }
  
  var area: Double {
    return .pi * radius * radius
  }
}

// 3
if let xOffset = MemoryLayout.offset(of: \Circle.center.x), 
   let yOffset = MemoryLayout.offset(of: \Circle.center.y), 
   let radiusOffset = MemoryLayout.offset(of: \Circle.radius) {
  print("\(xOffset) \(yOffset) \(radiusOffset)")
} else {
  print("Nil offset values.")
}

// 4
if let circumferenceOffset = MemoryLayout.offset(of: \Circle.circumference), 
   let areaOffset = MemoryLayout.offset(of: \Circle.area) {
  print("\(circumferenceOffset) \(areaOffset)")
} else {
  print("Nil offset values.")

注:可以通过 keypath 返回存储属性的内存偏移。计算属性返回 nil,因为没有存储关联。

3.13 模块中的内联函数

在 Swift 4.1 中,不允许在自己的模块中定义内联函数。依次选择 View ▸ Navigators ▸ Show Project Navigator, 右键单击 Sources and 选择 New File。重命名文件为 FactorialKit.swift 并且替换为下面代码块中的代码。

public class CustomFactorial {
  private let customDecrement: Bool
  
  public init(_ customDecrement: Bool = false) {
    self.customDecrement = customDecrement
  }
  
  private var randomDecrement: Int {
    return arc4random_uniform(2) == 0 ? 2 : 3
  }
  
  public func factorial(_ n: Int) -> Int {
    guard n > 1 else {
      return 1
    }
    let decrement = customDecrement ? randomDecrement : 1
    return n * factorial(n - decrement)
  }
}

在 Swift 4.2 中定义为内联的函数会更加高效,所以将 FactorialKit.swift 的代码替换如下。

public class CustomFactorial {
  @usableFromInline let customDecrement: Bool
  
  public init(_ customDecrement: Bool = false) {
    self.customDecrement = customDecrement
  }
  
  @usableFromInline var randomDecrement: Int {
    return Bool.random() ? 2 : 3
  }
  
  @inlinable public func factorial(_ n: Int) -> Int {
    guard n > 1 else {
      return 1
    }
    let decrement = customDecrement ? randomDecrement : 1
    return n * factorial(n - decrement)
  }
}

四、其他更新

下面是 Swift 4.2 中的一些其他改变

4.1 Swift Package Manager 更新
4.1.1 定义 Package 的 Swift 版本

Swift 4.1 在 Package.swift 中定义了swiftLanguageVersions,所以可以在 packages 中定义主版本。

let package = Package(name: "Package", swiftLanguageVersions: [4])

Swift 4.2 中也能通过SwiftVersion定义小版本 [SE-0209]

let package = Package(name: "Package", swiftLanguageVersions: [.v4_2])

能够通过.version(_:)定义之后的版本

let package = Package(name: "Package", swiftLanguageVersions: [.version("5")])
4.1.2 Packages 定义本地版本

在 Swift 4.1 中,可以使用仓库链接为 Package 定义依赖。如果有相互关联的 Package,就会产生额外的问题,所以 Swift 4.2 中引入了本地路径而提案[SE-0201]。

4.1.3 给 Package 添加系统库 Target
4.1.4 Swift 4.1 中系统模块包需要分仓库,这样包管理很难用 ,所以 Swift 4.2 使用系统库 Target 来实现 [SE-0208]
4.2 移除隐式解包可选值

在 Swift 4.1 中,你可以在嵌套类型中使用隐式解包可选值。

let favoriteNumbers: [Int!] = [10, nil, 7, nil]
let favoriteSongs: [String: [String]!] = ["Cosmin": ["Nothing Else Matters", "Stairway to Heaven"], "Oana": nil] 
let credentials: (usermame: String!, password: String!) = ("Cosmin", nil)

Swift 4.2 从数组、字典和元祖中移除了隐式解包可选值 SE-0054

let favoriteNumbers: [Int?] = [10, nil, 7, nil]
let favoriteSongs: [String: [String]?] = ["Cosmin": ["Nothing Else Matters", "Stairway to Heaven"], "Oana": nil] 
let credentials: (usermame: String?, password: String?) = ("Cosmin", nil)

五、未来愿景

可以从这个教程中下载最终的 Playground。Swift 4.2 在 Swift 4.1 诸多特性的基础上进一步做了改进,而且为了2019 年初 Swift 5 的 ABI 稳定做好准备。可以从 Swift CHANGELOG 或者 Swift standard library diffs 中了解更多关于这个版本的改变。也可以从 Swift Evolution 中看出 Swift 5 的改变。你可以给正在审查的提案提交反馈或者自己提交一个提案。到目前为止对 Swift 4.2 有什么喜欢或者不喜欢的地方。可以在论坛中参与讨论。

六、感谢原作者 Cosmin Pupăză

  • 原文地址:https://www.raywenderlich.com/5357-what-s-new-in-swift-4-2

你可能感兴趣的:(Swift 4.2 更新指北(译))