Swift 4.1 中的新特性

原文:What’s New in Swift 4.1?
作者:Cosmin Pupăză
译者:kmyhy

Xcode 9.3 和 Swift 4.1 终于不再是 beta 版!本次发布包含了对标准库和语言自身的一些期待已久的改进。如果你没有跟进 Swift 进化过程,请继续阅读。

在本教程中,你将学习Swift 4.1 的最为重大的一些改变。

本文需要 Xcode 9.3,请却你已经安装它。

开始

Swift 4.1 与 Swift 4 在源码上是兼容的,因此如果你的项目已经用 Xcode 迁移助手升级到 Swift 4 ,新特性不会被破坏。

在下一节,你会看到一些标签,比如 [SE-0001]。这是 Swift 进化的提议号。我用这些编号以便你能详细了解每个改变的完整细节。我建议你在 playground 中测试这些特性,以便更好地理解它们。

让我们开始吧,点开 Xcode 9.3,选择 File ▸ New ▸ Playground。选择 iOS 平台以及 Blank 模板。随便取个名字,然后保存。要在本教程中获得最大收益,请在新的 playground 中测试每个新特性。

注:想了解 Swift 4 的重点功能吗?没问题!请阅读本教程的前姊妹篇,Swift 4:What’s New in Swift 4

语法改进

本次发布有大量语法改进。包括条件一致性、协议中关联类型的递归约束等。

条件一致性

条件一致性为类型参数满足特定条件的泛型类型启用协议一致性[SE-0143]。这是一个很强大的特性,能让你的代码更加灵活。你将在几个例子中看到如何使用它。

在标准库中的条件一致性

在 Swift 4 中,你可以比较数组、字典和 optional,只要它们的元素是 Equatable 的。在一些场景下这是非常有用的:

// Arrays of Int
let firstArray = [1, 2, 3]
let secondArray = [1, 2, 3]
let sameArray = firstArray == secondArray

// Dictionaries with Int values
let firstDictionary = ["Cosmin": 10, "George": 9]
let secondDictionary = ["Cosmin": 10, "George": 9]
let sameDictionary = firstDictionary == secondDictionary

// Comparing Int?
let firstOptional = firstDictionary["Cosmin"]
let secondOptional = secondDictionary["Cosmin"]
let sameOptional = firstOptional == secondOptional

在这些例子中,用 == 操作符进行等于比较,因为从 Swift 4 开始 Int 就是 Equatable 的了。但是,对 optional 集合进行比较会有些问题,因为 Swift 4 中 optional 并没有遵循 Equatable 协议。Swift 4.1 用条件一致性解决了这个问题,允许对底层为 Equatable 类型的 optional 类型进行比较:

// Array of Int?
let firstArray = [1, nil, 2, nil, 3, nil]
let secondArray = [1, nil, 2, nil, 3, nil]
let sameArray = firstArray == secondArray

// Dictionary with Int? values
let firstDictionary = ["Cosmin": 10, "George": nil]
let secondDictionary = ["Cosmin": 10, "George": nil]
let sameDictionary = firstDictionary == secondDictionary

// Comparing Int?? (Optional of Optional)
let firstOptional = firstDictionary["Cosmin"]
let secondOptional = secondDictionary["Cosmin"]
let sameOptional = firstOptional == secondOptional

Int? 在 Swift 4.1 中是 Equatable 的,因此可以在 [Int?]、[String:Int?] 和 Int?? 上使用 == 操作符。

在对数组的数组(例如[[Int]])进行比较时,类似的问题得到了解决。在 Swift 4 中,你只能对 Set 的数组(例如 [Set])进行比较,因为 Set 遵循了 Equatable 协议。Swift 4.1 解决了这个问题,因为数组(和字典)和它们的元素一样也是 Equatable 的了。

let firstArrayOfSets = [Set([1, 2, 3]), Set([1, 2, 3])]
let secondArrayOfSets = [Set([1, 2, 3]), Set([1, 2, 3])]

// Will work in Swift 4 and Swift 4.1
// since Set<Int> is Equatable
firstArrayOfSets == secondArrayOfSets

let firstArrayOfArrays = [[1, 2, 3], [3, 4, 5]]
let secondArrayOfArrays = [[1, 2, 3], [3, 4, 5]]

// Caused an error in Swift 4, but works in Swift 4.1
// since Arrays are Equatable in Swift 4.1
firstArrayOfArrays == secondArrayOfArrays

总之,Swift 4.1 的 Optional、数组和字典现在都实现了 Equatable 和 Hashable,无论它们包含的元素和值有没有实现这些协议。

这是条件一致性在标准库中的情况。接下来,将介绍如何在自己的代码中实现它。

在代码中实现条件一致性

你将用条件一致性来创建自己的乐队。首先在 playground 底部添加如下代码块:

// 1 
class LeadInstrument: Equatable {
  let brand: String

  init(brand: String) {
    self.brand = brand
  }

  func tune() -> String {
    return "Standard tuning."
  }

  static func ==(lhs: LeadInstrument, rhs: LeadInstrument) -> Bool {
    return lhs.brand == rhs.brand
  }
}

// 2
class Keyboard: LeadInstrument {
  override func tune() -> String {
    return "Keyboard standard tuning."
  }
}

// 3
class Guitar: LeadInstrument {
  override func tune() -> String {
    return "Guitar standard tuning."
  }
}

代码分成以下几个步骤:

  1. LeadInstrument (主乐器)声明遵循 Equatable 协议。它有一个 brand 属性和用于给乐器调音的 tune() 方法。
  2. 在 Keyboard (键盘)中覆盖 tune() 方法,返回键盘的标准音调。
  3. 然后在 Guitar(吉他)中做同样的事情。

然后,定义乐队:

// 1  
class Band<LeadInstrument> {
  let name: String
  let lead: LeadInstrument

  init(name: String, lead: LeadInstrument) {
    self.name = name
    self.lead = lead
  }
}

// 2
extension Band: Equatable where LeadInstrument: Equatable {
  static func ==(lhs: Band<LeadInstrument>, rhs: Band<LeadInstrument>) -> Bool {
    return lhs.name == rhs.name && lhs.lead == rhs.lead
  }
}

代码分成以下几个步骤:

  1. 创建一个 Band (乐队)类,带泛型参数 LeadInstrument。每个乐队都有一个唯一的名字和主乐器。
  2. 用 where 关键字将 Band 约束为遵循 Equatable 协议,只要 LeadInstrument 遵循 Equatable 协议。你可以让 Band 的泛型 LeadInstrument 去实现 Equatable,这就是所谓的条件一致性。
  3. 然后,就可以声明你想要的乐队并对它们进行比较了:
// 1
let rolandKeyboard = Keyboard(brand: "Roland")
let rolandBand = Band(name: "Keys", lead: rolandKeyboard)
let yamahaKeyboard = Keyboard(brand: "Yamaha")
let yamahaBand = Band(name: "Keys", lead: yamahaKeyboard)
let sameBand = rolandBand == yamahaBand

// 2
let fenderGuitar = Guitar(brand: "Fender")
let fenderBand = Band(name: "Strings", lead: fenderGuitar)
let ibanezGuitar = Guitar(brand: "Ibanez")
let ibanezBand = Band(name: "Strings", lead: ibanezGuitar)
let sameBands = fenderBand == ibanezBand

在这里,创建了两个 Keyboard(键盘) 和 Guitar (吉他)以及对应的 Band(乐队)。然后直接比较 Band(乐队),幸亏我们早先定义的条件一致性。

JSON 解析时的条件一致性

在 Swift 4.1 中,数组、字典、Set 和 Optional 如果其元素实现了 Codable 协议的话,则它们也自动实现 Codable 协议。在 playground 文件中继续编写代码:

struct Student: Codable, Hashable {
  let firstName: String
  let averageGrade: Int
}

let cosmin = Student(firstName: "Cosmin", averageGrade: 10)
let george = Student(firstName: "George", averageGrade: 9)
let encoder = JSONEncoder()

// Encode 学生数组
let students = [cosmin, george]
do {
  try encoder.encode(students)
} catch {
  print("Failed encoding students array: \(error)")
}

// Encode 存储学生的字典
let studentsDictionary = ["Cosmin": cosmin, "George": george]
do {
  try encoder.encode(studentsDictionary)
} catch {
  print("Failed encoding students dictionary: \(error)")
}

// Encode 学生 Set 集合
let studentsSet: Set = [cosmin, george]
do {
  try encoder.encode(studentsSet)
} catch {
  print("Failed encoding students set: \(error)")
}

// Encode 可空的学生
let optionalStudent: Student? = cosmin
do {
  try encoder.encode(optionalStudent)
} catch {
  print("Failed encoding optional student: \(error)")
}

我们分别 encode 了 [Student]、[String: Student]、Set 和 Student?。这在 Swift 4.1 中是毫无困难的,因为 Student 是一个 Codable,因此这个类型的集合也都实现了 Codable。

在 JSON 解析时可以采用驼峰命名和下划线命名法

在 Swift 4.1 中,对 JSON 进行 encode 时,可以将 key 由 CamelCase(驼峰命名法) 转换为 snake_case(下划线命名法)。

var jsonData = Data()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted

do {
  jsonData = try encoder.encode(students)
} catch {
  print(error)
}

if let jsonString = String(data: jsonData, encoding: .utf8) {
  print(jsonString)
}

在实例化 encoder 时,将 keyEcodingStrategy 设置为 .convertToSnakeCase。看控制台,你会看到:

[
  {
    "first_name" : "Cosmin",
    "average_grade" : 10
  },
  {
    "first_name" : "George",
    "average_grade" : 9
  }
]

也可以将 snake_case 转换为驼峰命名法:

var studentsInfo: [Student] = []
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
  studentsInfo = try decoder.decode([Student].self, from: jsonData)
} catch {
  print(error)
}

for studentInfo in studentsInfo {
  print("\(studentInfo.firstName) \(studentInfo.averageGrade)")
} 

这次,将 keyDecodingStrategy 设为 .convertFromSnakeCase。

Equatable 和 Hashable 的协议一致性

在 Swift 4,要让结构体实现 Equatable 和 Hashable 协议,必须写这样的代码:

struct Country: Hashable {
  let name: String
  let capital: String

  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
  }
}

在这段代码中,要支持 Equatable 和 Hashable 接口,需要实现 ==(lhs:rhs:) 和 hashValue 方法。你可以对 Country 对象进行比较,将它们添加到 Set 甚至将它们作为字典中的 Key:

let france = Country(name: "France", capital: "Paris")
let germany = Country(name: "Germany", capital: "Berlin")
let sameCountry = france == germany

let countries: Set = [france, germany]
let greetings = [france: "Bonjour", germany: "Guten Tag"]

在 Swift 4.1 中,为结构体添加了默认的 Equatable 和 Hashable 实现,只要它们的所有属性都是 Equatable 和 Hashable 的 [SE-0185]。

这极度简化了你的代码,可以简单地这样写:

struct Country: Hashable {
  let name: String
  let capital: String
}

在 Swift 4 中,枚举在实现 Equatable 和 Hashable 时还需要更多的代码:

enum BlogPost: Hashable {
  case tutorial(String, String)
  case article(String, String)

  static func ==(lhs: BlogPost, rhs: BlogPost) -> Bool {
    switch (lhs, rhs) {
    case let (.tutorial(lhsTutorialTitle, lhsTutorialAuthor), .tutorial(rhsTutorialTitle, 
               rhsTutorialAuthor)):
      return lhsTutorialTitle == rhsTutorialTitle && lhsTutorialAuthor == rhsTutorialAuthor
    case let (.article(lhsArticleTitle, lhsArticleAuthor), .article(rhsArticleTitle, rhsArticleAuthor)):
      return lhsArticleTitle == rhsArticleTitle && lhsArticleAuthor == rhsArticleAuthor
    default:
      return false
    }
  }

  var hashValue: Int {
    switch self {
    case let .tutorial(tutorialTitle, tutorialAuthor):
      return tutorialTitle.hashValue ^ tutorialAuthor.hashValue &* 16777619
    case let .article(articleTitle, articleAuthor):
      return articleTitle.hashValue ^ articleAuthor.hashValue &* 16777619
    }
  }
}

在书写 ==(lhs:rhs:) 和 hashValue 方法时,用到了枚举的各种情况。这样,你就可以对 BlogPost 进行比较了,并且在 Set 或字典中使用它们了:

let swift3Article = BlogPost.article("What's New in Swift 3.1?", "Cosmin Pupăză")
let swift4Article = BlogPost.article("What's New in Swift 4.1?", "Cosmin Pupăză")
let sameArticle = swift3Article == swift4Article

let swiftArticlesSet: Set = [swift3Article, swift4Article]
let swiftArticlesDictionary = [swift3Article: "Swift 3.1 article", swift4Article: "Swift 4.1 article"]

和 Hashable 一样,因为有默认的 Equatable 和 Hashable 实现,代码在 Swift 4.1 中被极度缩减了:

enum BlogPost: Hashable {
  case tutorial(String, String)
  case article(String, String)
}

你可以省略 20 行公式化的代码!

可哈希的索引类型

在 Swift 4 中,如果下标参数的类型是可哈希的,那么可以用 key path 作为下标。例如,可以在一个 double 数组中使用 key path:

let swiftVersions = [3, 3.1, 4, 4.1]
let path = \[Double].[swiftVersions.count - 1]
let latestVersion = swiftVersions[keyPath: path]

通过 keyPath 从 SwiftVersion 中获取当前 Swift 版本号。

Swift 4.1 为标准库的所有索引类型添加了 Hashable 协议 [SE-0188]:

let me = "Cosmin"
let newPath = \String.[me.startIndex]
let myInitial = me[keyPath: newPath]

这个下标返回了字符串的第一个字母。因为在 Swift 4.1 中字符串的 index 类型是 Hashable 的。

协议中关联类型的递归约束

Swift 4 不支持协议中定义关联类型的递归约束:

protocol Phone {
  associatedtype Version
  associatedtype SmartPhone
}

class IPhone: Phone {
  typealias Version = String
  typealias SmartPhone = IPhone
}

这里,定义了一个 SmartPhone 的关联类型,但有时候需要把它限制为 Phone,因为所有的 smartphone 都是 phone。现在,在 Swift 4.1 中已经实现了 [SE-0157]:

protocol Phone {
  associatedtype Version
  associatedtype SmartPhone: Phone where SmartPhone.Version == Version, SmartPhone.SmartPhone == SmartPhone
}

用 where 关键字限制了 Version 和 SmartPhone 应当和 phone 相同。

协议中的 Weak 和 unowned

Swift 4 中支持将协议的属性修饰为 weak 和 unowned:

class Key {}
class Pitch {}

protocol Tune {
  unowned var key: Key { get set }
  weak var pitch: Pitch? { get set }
}

class Instrument: Tune {
  var key: Key
  var pitch: Pitch?

  init(key: Key, pitch: Pitch?) {
    self.key = key
    self.pitch = pitch
  }
}

用特定音调(key)和音高(pitch)来给乐器调音。pitch 可以为 nil,因此在 Tune 协议中将它修饰为 weak。

但在协议自身中定义 weak 和 unowned 并没有实际意义,因此 Swift 4.1 将它们去掉了,如果你在协议中使用这些关键字会得到警告 [SE-0186]:

protocol Tune {
  var key: Key { get set }
  var pitch: Pitch? { get set }
}

集合中的索引距离

Swift 4 使用 IndexDistance 来声明集合中的元素个数:

func typeOfCollection<C: Collection>(_ collection: C) -> (String, C.IndexDistance) {
  let collectionType: String

  switch collection.count {
  case 0...100:
    collectionType = "small"
  case 101...1000:
    collectionType = "medium"
  case 1001...:
    collectionType = "big"
  default:
    collectionType = "unknown"
  }

  return (collectionType, collection.count)
}

typeOfCollection(_:) 返回的元组中包含了集合类型和数目。你可以在任何类型的集合中使用,比如数组、字典、Set:

typeOfCollection(1...800) // ("medium", 800)
typeOfCollection(greetings) // ("small", 2)

你可以用 where 子句将返回类型中的 IndexDistance 约束为 Int:

func typeOfCollection<C: Collection>(_ collection: C) -> (String, Int) where C.IndexDistance == Int {
  // 和上面一样的代码
}

Swift 4.1 在标准库中将 IndexDistance 替换成了 Int,因此你不需要 where 子句了 [SE-0191]:

func typeOfCollection<C: Collection>(_ collection: C) -> (String, Int) {
  // 和之前的代码一样
}

模块中的结构体初始化

在 Swift 4 中,在公有的结构体中增加属性会导致源码破坏性修改。教程中有个例子,首先确保项目导航器是打开的,你可以用 View\Navigators\Show Project Navigator 来显示它。然后,右键点击 Sources,选择 New File。文件命名为 DiceKit.swift。将它的内容修改为:

public struct Dice {
  public let firstDie: Int
  public let secondDie: Int

  public init(_ value: Int) {
    let finalValue: Int

    switch value {
    case ..<1:
      finalValue = 1
    case 6...:
      finalValue = 6
    default:
      finalValue = value
    }

    firstDie = finalValue
    secondDie = 7 - finalValue
  }
}

在结构的初始化方法中确保两个 dice(骰子)的有效值在 1-6 之间。回到 playground 文件,继续编写下列代码:

// 1
let dice = Dice(0)
dice.firstDie
dice.secondDie

// 2
extension Dice {
  init(_ firstValue: Int, _ secondValue: Int) {
    firstDie = firstValue
    secondDie = secondValue
  }
}

// 3
let newDice = Dice(0, 7)
newDice.firstDie
newDice.secondDie

在代码中你做了这些事情:

  1. 创建了一对有效的骰子。
  2. 用另一个初始化方法扩展 Dice,在方法中直接访问它的属性。
  3. 定义一对无效的骰子,用新的初始化方法。

在 Swift 4.1 中,跨目标的初始化方法会调用默认的那个。将你的 Dice 扩展改成:

extension Dice {
  init(_ firstValue: Int, _ secondValue: Int) {
    self.init(abs(firstValue - secondValue))
  }
}

这种修改是的结构体和类的行为一致:在 Swift 4.1 中跨模块的初始化方法必须是便利初始化方法 [SE-0189]。

平台设置和 Build 配置改变

在 Swift 4.1 中加入了一些针对代码测试非常有用的平台和编译特性:

Build Imports

在 Swift 4 中,如果你要检查某个 module 是否在特定平台下有效,以检查操作系统自身,例如:

#if os(iOS) || os(tvOS)
  import UIKit
  print("UIKit is available on this platform.")
#else
  print("UIKit is not available on this platform.")
#endif

UIKit 在 iOS 和 tvOS 上有效,因此测试为真时可以导入它。Swift 4.1 中进一步简化了这个,允许你用检查 module 自身来代替:

#if canImport(UIKit)
print("UIKit is available if this is printed!")
#endif

在 Swift 4.1 中,用 #if canImport(UIKit) 来确认某个框架是不是能够导入 [SE-0075]。

Target Environments

在编写 Swift 4 代码时,检查当前运行的是模拟器还是真机,最常见的方法是检查架构和操作系统:

#if (arch(i386) || arch(x86_64)) && (os(iOS) || os(tvOS) || os(watchOS))
  print("Testing in the simulator.")
#else
  print("Testing on the device.")
#endif

如果你的架构是 Intel 架构,而操作系统是 iOS、tvOS 或 watchOS,则你运行环境是模拟器。否则,就是真机。

这种判断十分麻烦,而且对当前问题描述不清晰。Swift 4.1 让这个判断变得更简单,只需要用 targetEnvironment(simulator) [SE-0190] :

#if targetEnvironment(simulator)
  print("Testing in the simulator.")
#endif

杂项

Swift 4.1 还有其它几个值得一提的改变:

Compacting Sequences

In Swift 4 中用 flatMap(_:) 过滤序列中的空值用的非常多:

let pets = ["Sclip", nil, "Nori", nil]
let petNames = pets.flatMap { $0 } // ["Sclip", "Nori"]

不幸的是,flatMap(_:) 在许多时候是笨重的,在某些特殊情况下,flatMap(_:) 的名字对实际的任务来说也不具有描述性。

因此,Swift 4.1 引入了一个别名 compactMap(_:),以使它的意义更清晰和可识别 [SE-0187]:

let petNames = pets.compactMap { $0 }

不安全的指针

Swift 4 用临时的不安全的可变的指针去创建和修改不安全的可变的缓存指针:

let buffer = UnsafeMutableBufferPointer<Int>(start: UnsafeMutablePointer<Int>.allocate(capacity: 10), 
                                             count: 10)
let mutableBuffer = UnsafeMutableBufferPointer(start: UnsafeMutablePointer(mutating: buffer.baseAddress), 
                                               count: buffer.count)

Swift 4.1 允许你直接使用不安全的可变缓存指针,用法和不安全的可变指针一样 [SE-0184]:

let buffer = UnsafeMutableBufferPointer<Int>.allocate(capacity: 10)
let mutableBuffer = UnsafeMutableBufferPointer(mutating: UnsafeBufferPointer(buffer))

playground 的新功能

Swift 4 允许你在 Xcode playground 中自定义类型描述:

class Tutorial {}
extension Tutorial: CustomPlaygroundQuickLookable {
  var customPlaygroundQuickLook: PlaygroundQuickLook {
    return .text("raywenderlich.com tutorial")
  }
}
let tutorial = Tutorial()

让 Tutorial 实现 CustomPlaygroundQuickLookable 协议,返回一个自定义的快速浏览的 playground 描述。该描述类性在 customPlaygroundQuickLook 被限制为 PlaygroundQuickLook。在 Swift 4.1 中不再是这样了:

extension Tutorial: CustomPlaygroundDisplayConvertible {
  var playgroundDescription: Any {
    return "raywenderlich.com tutorial"
  }
}

这次实现的是 CustomPlaygroundDisplayConvertible。这种描述类型选择是 Any,因此你可以在 playgroundDescription 中返回任何东西。 这就简化了代码并增加了灵活性 [SE-0198]。

接下来去哪里?

在顶部或底部的 Dowload Materials 链接处下载最终的 playground。

Swift 4.1 在 Swift 4 基础上新增了一些特性,更重大的改变将延至年末的 Swift 5 中。包括 ABI 稳定性,改进的泛型和字符串、新的内存所有权和并发模型等。

如果你还意犹未尽,去看看 Swift 标准库的差异或者官方的 Swift 更新列表,在这里你可以阅读更多关于这个版本中所有更改的信息。你也可以用它来跟进即将出来的 Swift 5!

如果你还想了解 Swift 5 及更高版本中的改变,我们建议你看一下 Swift 进化计划,在那里可以看到正在提议的新功能、更改和新增功能有哪些。如果你针对很感兴趣,请对正在审查的建议进行反馈,甚至自己提出建议!

喜欢/不喜欢 Swift 4.1 的哪些地方,请在论坛中留言!

Download Materials

猜你喜欢

转载自blog.csdn.net/kmyhy/article/details/81082328