Swift 5 字串插值-簡介

SwiftGG翻譯組發表於2019-04-22

作者:Olivier Halligon,原文連結,原文日期:2018-12-15

譯者:Nemocdz;校對:numbbbbbYousanflics;定稿:Forelax

StringInterpolation 協議最初的設計效率低下又不易擴充套件,為了在後續的版本中能夠將其徹底重構,Swift 4 中將該協議標記為廢棄。即將在 Swift 5 中亮相的 SE-0228 提案介紹了一種新的 StringInterpolation 設計,使得 String 有了更大的潛能。

在 Swift 的 master 分支裡實現之後,就可以下載一個 快照 來安裝最新的 Swift 5 工具鏈到 Xcode 中,來嘗試全新的 StringInterpolation。讓我們來把玩一下。

全新的 StringInterpolation 設計

我強烈建議本篇文章的讀者閱讀一下 SE-0228 提案,感受一下新 API 的背後的設計思路和動機。

要讓一個型別遵循 ExpressibleByStringInterpolation,最基本的你需要:

  • 讓這個型別擁有一個型別為 StringInterpolation 的子型別,這個子型別遵循 StringInterpolationProtocol 並將負責解釋插值
  • 這個子型別僅需要實現 appendLiteral(_ literal: String) 方法,再選擇一個或多個你自己想要支援的 appendInterpolation(...) 簽名的方法
  • 這個 StringInterpolation 子型別會作為“構造器”服務於你的主型別,然後編譯器會呼叫那些 append… 方法一步一步地構造物件
  • 然後你的主型別需要實現 init(stringInterpolation: StringInterpolation) ,它會用上一步的結果來例項化它自己。

你可以實現任何你喜歡的 appenInterpolation(...) 方法,這意味著你可以任意選擇支援什麼插值。這是一個帶來巨大的可能性的超強功能。

舉個例子,如果你實現了 func appendInterpolation(_ string: String, pad: Int),那麼意味著你將可以用類似這樣的插值:"Hello \(name, pad: 10), how are you?" 來構造你的型別。插值只需要匹配你的 StringInterpolation 子型別其中一個支援的 appendInterpolation 方法簽名。

一個簡單的例子

讓我用一個簡單的例子來演示一下插值是如何運作的。一起來構造一個允許引用 issue 編號和使用者的 GitHubComment 型別吧。

這個例子的目標是做到類似下面的寫法:

let comment: GitHubComment = """
 See \(issue: 123) where \(user: "alisoftware") explains the steps to reproduce.
 """
複製程式碼

所以我們該怎麼實現它呢?

首先,讓我們宣告基本的結構體 struct GitHubComment 並讓它遵循 ExpressibleByStringLiteral(因為 ExpressibleByStringInterpolation 繼承自這個協議所以我們將它的實現抽離)和 CustomStringConvertible(為了 debug 時友好地在控制檯中列印)。

struct GitHubComment {
  let markdown: String
}
extension GitHubComment: ExpressibleByStringLiteral {
  init(stringLiteral value: String) {
    self.markdown = value
  }
}
extension GitHubComment: CustomStringConvertible {
  var description: String {
    return self.markdown
  }
}
複製程式碼

然後,我們讓 GitHubComment 遵循 ExpressibleByStringInterpolation。這意味著在剩下需要實現的功能,將由一個 StringInterpolation 子型別來完成:

  • 首先初始化它自己:init(literalCapacity: Int, interpolationCount: Int) 提供給你保留一部分資料到緩衝區的能力,在一步步構造型別時就會用到這個能力。在這個例子中,我們可以在構造例項的時候,簡單用一個 String 並往它上面追加片段,不過這裡我採用一個 parts: [String] 來代替,之後再將它組合起來

  • 實現 appendLiteral(_ string: String) 逐個追加文字到 parts

  • 實現 appendInterpolation(user: String) 在遇到 \(user: xxx) 時逐個追加 markdown 表示的使用者配置連結

  • 實現 appendInterpolation(issue: Int) 逐個追加用 markdown 表示的 issue 連結

  • 然後在 GitHubComment 上實現 init(stringInterpolation: StringInterpolation)parts 構造成一個評論

extension GitHubComment: ExpressibleByStringInterpolation {
  struct StringInterpolation: StringInterpolationProtocol {
    var parts: [String]
      init(literalCapacity: Int, interpolationCount: Int) {
      self.parts = []
      // - literalCapacity 文字片段的字元數 (L)
      // - interpolationCount 插值片段數 (I)
      // 我們預計通常結構會是像 "LILILIL"
      // — e.g. "Hello \(world, .color(.blue))!" — 因此是 2n+1
      self.parts.reserveCapacity(2*interpolationCount+1)
    }
      mutating func appendLiteral(_ literal: String) {
      self.parts.append(literal)
    }
    mutating func appendInterpolation(user name: String) {
      self.parts.append("[\(name)](https://github.com/\(name))")
    }
    mutating func appendInterpolation(issue number: Int) {
      self.parts.append("[#\(number)](issues/\(number))")
    }
  }
  init(stringInterpolation: StringInterpolation) {
    self.markdown = stringInterpolation.parts.joined()
  }
}
複製程式碼

這就完事了!我們成功了!

注意,因為那些我們實現了的 appendInterpolation 方法簽名,我們允許使用 Hello \(user: "alisoftware") 但不能使用 Hello \(user: 123),因為 appendInterpolation(user:) 期望一個 String 作為形參。類似的是,在你的字串中 \(issue: 123) 只能允許一個 Int 因為 appendInterpolation(issue:) 採用一個 Int 作為形參。

實際上,如果你嘗試在你的 StringInterpolation 子類中用不支援的插值,編譯器會給你提示報錯:

let comment: GitHubComment = """
See \(issue: "bob") where \(username: "alisoftware") explains the steps to reproduce.
"""
//             ^~~~~         ^~~~~~~~~
// 錯誤: 無法轉換 ‘String’ 型別的值到期望的形參型別 ‘Int’
// 錯誤: 呼叫 (have 'username:', expected 'user:')實參標籤不正確
複製程式碼

這僅僅只是個開始

這個新的設計開啟了一大串腦洞讓你去實現自己的 ExpressibleByStringInterpolation 型別。這些想法包括:

  • 建立一個 HTML 型別並遵循,你就可以用插值寫 HTML
  • 建立一個 SQLStatement 型別並遵循,你就可以寫更簡單的 SQL 語句
  • 用字串插值支援更多自定義格式,比如在你的插值字串中用格式化 Double 或者 Date
  • 建立一個 RegEX 型別並遵循,你就可以用花裡胡哨的語法寫正規表示式
  • 建立一個 AttributedString 型別並遵循,就可以用字串插值構建 NSAttributedString

帶來新的字串插值設計的 Brent Royal-GordonMichael Ilseman,提供了更多例子在這個 要點列表 中。

我個人嘗試了一下支援 NSAttributedString 的實現,並想 在專門的一篇文章裡分享它的初步實現,因為我發現它非常優雅。我們下一篇文章再見!

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

相關文章