1. 程式人生 > >Swift 4.2 新特性(譯)

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 轉換的問題。

/// swift 4.1
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 採用了更加簡單直接的方式。

/// 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 }
let shuffledPlaylist = playlist.shuffled()
names.shuffle()

注:使用 shuffled() 來建立一個洗牌後的陣列。使用 shuffle() 來將陣列洗牌。

3.2 動態成員查詢

Swift 4.1 使用下面的方式實現自定義下標操作。

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

/// swift 4.2
// 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 定義了查詢指定元素的第一個索引位置或者滿足指定條件的第一個元素的方法。

/// swift 4.1
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 為了實現一致性重構了方法名

/// 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 中我們可能採用下面的方法來處理。

/// 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<Tutorial> {
  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<Tutorial> {
  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 {