[譯] Swift 程式碼格式化

iWeslie發表於2019-05-05

我剛離開了一家時髦的咖啡館。裡面有很多 iOS 開發者,他們互相竊竊私語,討論他們是多麼得等不及蘋果公司為 Swift 釋出的官方風格指南和格式化程式。

在過去的幾天裡,社群一直在討論 Tony AllevatoDave Abrahams 採用官方版的 Swift 格式化工具。

數十名社群成員已經對 提案草案 進行了權衡。與所有樣式問題一樣,每個人都有不同的意見。但幸運的是,來自社群的話語通常具有代表性和洞察力,其中清晰表達了各式各樣的觀點、用例以及關注點。

在撰寫本文時,似乎很多但不是所有受訪者都對官方格式化表示贊同。那些贊成給程式碼格式化約束的人也希望有一個工具可以自動診斷和修復不符合這些約束的程式碼。但是,其他一些人對這格式化的適用性和可配置性表示擔憂。

在本週的 NSHipster 上,我們將討論一下關於當前可用的 Swift 格式化程式,包括作為提案的一部分發布的 swift-format 工具,同時看看它們是如何進行處理的。然後我們再權衡其中的利弊。

首先,讓我們從下面的一個問題開始:

什麼是程式碼格式化?

我們將程式碼格式化定義為對程式碼所做的任何更改,以便在不改變其行為的情況下更容易理解程式碼的內容。雖然這個定義延伸到等價形式的差異(例如 [Int]Array <Int>),我們暫且將這裡的討論內容限制為空格和標點符號。

與許多其他程式語言一樣,Swift 在接受換行符,製表符和空格時都非常自由。大多數空格都是無關緊要的,從編譯器的角度來看對程式碼沒有任何影響。

當我們使用空格來使程式碼更易於理解而不改變其行為時,此時空格就作為一個 輔助符號。當然,主要符號是程式碼本身。

另一種輔助表示法是語法高亮,這在我們以前的 NSHipster 文章 中討論過.

雖然你可以通過分號在一行程式碼中寫幾乎任何東西,但是在其他條件相同的情況下,使用空格與換行來對齊程式碼難道不是一種更易於理解並且直觀的方式嗎?

不幸的是,因編譯器接受空格的性質而產生的歧義往往會引起程式設計師之間對程式碼的混淆和分歧:“我到底應不應該在大括號之前新增換行符?如何分解超出編輯器寬度的語句?”

一個公司通常會有自己的一套程式碼規範,但它們通常不夠明確,執行力不強,而且可能已經過時。程式碼格式化程式的作用是自動執行一組約定,以便程式設計師可以拋開差異來把重心放到解決實際問題上。

格式化工具比較

Swift 社群從一開始就考慮過程式碼格式問題,程式碼書寫規範從 Swift 誕生之初就已經存在,各種開源工具也可以通過規範來自動化格式化程式碼。

你可以看看以下四個工具來了解目前 Swift 程式碼格式化程式的狀態:

專案 倉庫連結
SwiftFormat github.com/nicklockwoo…
SwiftLint github.com/realm/Swift…
Prettier with Swift Plugin github.com/prettier/pr…
swift-format(已提議) github.com/google/swif…

為簡潔起見,本文僅討論一些可用的 Swift 格式化工具。如果你有興趣可以瞭解以下更多內容:SwimatSwiftRewriterswiftfmt

為了方便比較,我們設計了以下程式碼來評估每個工具,工具都使用它們的預設配置:

struct ShippingAddress : Codable  {
    var recipient: String
    var streetAddress : String
    var locality :String
    var region   :String;var postalCode:String
    var country:String

    init(recipient: String,        streetAddress: String,
         locality: String,region: String,postalCode: String,country:String)
    {
        self.recipient = recipient
        self.streetAddress = streetAddress
        self.locality  = locality
        self.region        = region;self.postalCode=postalCode
        guard country.count == 2, country == country.uppercased() else { fatalError("invalid country code") }
        self.country=country}}

let applePark = ShippingAddress(recipient:"Apple, Inc.", streetAddress:"1 Apple Park Way", locality:"Cupertino", region:"CA", postalCode:"95014", country:"US")
複製程式碼

儘管程式碼格式化包含各種可能的語法和語義轉換,但我們將專注於換行和縮排的問題,我們認為這是任何程式碼格式化程式的基礎要求。

誠然,本文中的效能基準並不是非常嚴格。但是它們應該可以作為參考。我們把秒作為測量時間的單位,採用 2017 款配備 2.9 GHz Intel Core i7 處理器和 16 GB 2133 MHz LPDDR3 記憶體的 MacBook Pro。

SwiftFormat

首先是 SwiftFormat,它的簡介是一個很有用的工具。

安裝

SwiftFormat 可以通過 HomebrewMint 以及 CocoaPods 來安裝。

你可以通過以下命令來安裝它:

$ brew install swiftformat
複製程式碼

此外,SwiftFormat 還提供了一個 Xcode 擴充,你可以通過在 Xcode 裡使用它進行格式化。或者如果你是 VSCode 使用者,你可以使用 此外掛

使用

swiftformat 命令會把在指定檔案和目錄路徑中找到的所有 Swift 檔案進行格式化。

$ swiftformat Example.swift
複製程式碼

SwiftFormat 有很多規則,你可以通過命令列選項或使用配置檔案單獨配置。

樣例輸出

使用預設配置執行 swiftformat 後,你會得到以下輸出:

// swiftformat version 0.39.5
struct ShippingAddress: Codable {
    var recipient: String
    var streetAddress: String
    var locality: String
    var region: String; var postalCode: String
    var country: String

    init(recipient: String, streetAddress: String,
         locality: String, region: String, postalCode: String, country: String) {
        self.recipient = recipient
        self.streetAddress = streetAddress
        self.locality = locality
        self.region = region; self.postalCode = postalCode
        guard country.count == 2, country == country.uppercased() else { fatalError("invalid country code") }
        self.country = country
    }
}

let applePark = ShippingAddress(recipient: "Apple, Inc.", streetAddress: "1 Apple Park Way", locality: "Cupertino", region: "CA", postalCode: "95014", country: "US")
複製程式碼

正如你所見,格式化明顯改進了原始版本。每一行都根據其範圍縮排,標點符號間的宣告都有一致的間距。屬性宣告中的分號和初始化引數中的換行都被保留。但是,大括號沒有像預期的那樣被移動到單獨一行,Nick0.39.5 修復了它。

效能

SwiftFormat 在本文中測試工具中始終是最快的,它能在幾毫秒內完成處理。

$ time swiftformat Example.swift
        0.03 real         0.01 user         0.01 sys
複製程式碼

SwiftLint

接下來是 SwiftLint,它是 Swift 開源社群的支柱。其中包含了超過 100 個規則,SwiftLint 可以對你的程式碼執行各種各樣的檢查,從將 AnyObject 優於 class(僅用於類的協議)到所謂的“尤達條件式”,其中規定變數應放在比較運算子的左側(即 if n == 42 而非 if 42 == n)。

顧名思義,SwiftLint 主要不是程式碼格式化程式,它確實是一種識別濫用慣例和誤用 API 的診斷工具。正是由於具備自動校正功能,它常常被用於格式化程式碼。

安裝

你可以通過以下命令使用 Homebrew 來安裝它:

$ brew install swiftlint
複製程式碼

或者你也可以通過 CocoaPodsMint 或者 獨立 pkg 安裝包 來安裝。

使用

要用 SwiftLint 來進行程式碼格式化,請執行 autocorrect 子命令,--format 引數為需要執行的檔案或目錄。

$ swiftlint autocorrect --format --path Example.swift
複製程式碼

樣例輸出

執行上述命令,你將得到以下輸出:

// swiftlint version 0.31.0
struct ShippingAddress: Codable {
    var recipient: String
    var streetAddress: String
    var locality: String
    var region: String;var postalCode: String
    var country: String

    init(recipient: String, streetAddress: String,
         locality: String, region: String, postalCode: String, country: String) {
        self.recipient = recipient
        self.streetAddress = streetAddress
        self.locality  = locality
        self.region        = region;self.postalCode=postalCode
        guard country.count == 2, country == country.uppercased() else { fatalError("invalid country code") }
        self.country=country}}

let applePark = ShippingAddress(recipient: "Apple, Inc.", streetAddress: "1 Apple Park Way", locality: "Cupertino", region: "CA", postalCode: "95014", country: "US")
複製程式碼

SwiftLint 可以清除最糟糕的縮排和行內間距問題,同時保留其他無關的空格。值得注意的是,格式化不是 SwiftLint 的主要作用,一般情況下,它只是附帶提供可操作的程式碼診斷。從 “首先,沒有任何副作用” 的角度來看,你不必在這裡抱怨它處理的結果。

效能

SwiftLint 檢查的所有內容非常高效,在我們的示例中它只需幾分之一秒即可完成。

$ time swiftlint autocorrect --quiet --format --path Example.swift
        0.11 real         0.05 user         0.02 sys
複製程式碼

Swift Plugin 美化

如果你沒有使用過 JavaScript(如 上週文章 中所述),這可能是你聽說過的第一篇 Prettier 相關的文章。反之,如果你沉浸在 ES6,React 和 WebPack 的世界中,你幾乎肯定會特別依賴它。

Prettier 在程式碼格式化程式中是獨一無二的,因為它體現了程式碼的美學,用換行來分隔程式碼,就好像寫詩一樣。

多虧了一個在開發中的 外掛架構,你可以在其他語言上使用它,其中 包括了 Swift

Swift 的 Prettier 外掛非常重要,但是當它遇到一個沒有規則的語法標記時會崩潰(比如 EnumDecl ?)。然而,正如你將在下面看到的那樣,到目前為止它的功能還是很強大以至於我們不能忽視它,所以把它放在本文格式化工具 PK 中是值得的。

安裝

要使用 Prettier 及其 Swift 外掛,你將涉及到 Node。雖然有很多方法可以安裝它,但我們還是最喜歡 Yarn ?。

$ brew install yarn
$ yarn global add prettier prettier/plugin-swift
複製程式碼

使用

現在環境變數 $PATH 裡已經可以訪問 prettier 命令列工具,你可以傳入檔案或路徑來執行它。

$ prettier Example.swift
複製程式碼

樣例輸出

以下是在我們的前面的例子中使用 Prettier 執行最新版本的 Swift 外掛得到的輸出:

// prettier version 1.16.4
// prettier/plugin-swift version 0.0.0 (bdf8726)
struct ShippingAddress: Codable {
    var recipient: String
    var streetAddress: String
    var locality: String
    var region: String
    var postalCode: String
    var country: String

    init(
        recipient: String,
        streetAddress: String,
        locality: String,
        region: String,
        postalCode: String,
        country: String
    ) {
        self.recipient = recipient
        self.streetAddress = streetAddress
        self.locality = locality
        self.region = region;
        self.postalCode = postalCode
        guard country.count == 2, country == country.uppercased() else {
            fatalError("invalid country code")
        }
        self.country = country
    }
}

let applePark = ShippingAddress(
    recipient: "Apple, Inc.",
    streetAddress: "1 Apple Park Way",
    locality: "Cupertino",
    region: "CA",
    postalCode: "95014",
    country: "US"
)
複製程式碼

Prettier 將自己稱為 “一個自以為是的程式碼格式化程式”。實際上,它的配置方式不多,只有兩個選項:“常規程式碼” 或 “更漂亮的程式碼”。

現在,你可能會面對很多垂直的對齊,但你不得不承認這段程式碼看起來確實很棒。所有的語句都有均勻間隔、縮排以及對齊,很難相信這是自動實現的。

當然,我們之前的警告仍然適用:這仍然在開發中,並不適合生產環境使用,而且它還有一些效能問題。

效能

說到底,Prettier 比本文討論的其他工具都慢一到兩個數量級。

$ time prettier Example.swift
    1.14 real         0.56 user         0.38 sys
複製程式碼

目前還不清楚這到底是語言障礙還是優化不良的結果,Prettier 慢得足以帶來很大的問題。

目前,我們建議僅將 Prettier 用於一次性格式化任務,例如編寫文章和書籍的程式碼。

swift-format

在瞭解可用於 Swift 格式化程式的現狀後,我們現在有了一個合理的標準來評估 Tony Allevato 和 Dave Abrahams 提出的 swift-format 工具。

安裝

swift-format 的程式碼目前託管在 Google fork 的 Swift 的 format 分支 上。你可以通過執行以下命令來下載並且從原始碼編譯它:

$ git clone https://github.com/google/swift.git swift-format
$ cd swift-format
$ git submodule update --init
$ swift
複製程式碼

為了讓你方便使用,我們提供了一個自制的公式,該公式來自 我們自己 Google 倉庫上的 fork,你可以通過下面的命令來安裝:

$ brew install nshipster/formulae/swift-format
複製程式碼

使用

執行 swift-format 命令,傳入要格式化的 Swift 檔案或目錄。

$ swift-format Example.swift
複製程式碼

swift-format 命令還採用了 --configuration 選項,該引數需要傳入一個 JSON 檔案。目前,定製 swift-format 行為的最簡單方法就是將預設配置轉儲到檔案裡。

$ swift-format -m dump-configuration .swift-format.json
複製程式碼

建立如下的 JSON 檔案,在執行上述命令時傳入:

{
  "blankLineBetweenMembers": {
    "ignoreSingleLineProperties": true
  },
  "indentation": {
    "spaces": 2
  },
  "lineLength": 100,
  "maximumBlankLines": 1,
  "respectsExistingLineBreaks": true,
  "tabWidth": 8,
  "version": 1
}
複製程式碼

在配置完之後,你應該這樣使用:

$ swift-format Example.swift --configuration .swift-format.json
複製程式碼

樣例輸出

使用其預設配置,這裡是 swift-format 格式化後的輸出:

// swift-format version 0.0.1
struct ShippingAddress: Codable {
  var recipient: String
  var streetAddress: String
  var locality: String
  var region   :String;
  var postalCode: String
  var country: String

  init(
    recipient: String, streetAddress: String,
    locality: String, region: String, postalCode: String, country: String
  )
  {
    self.recipient = recipient
    self.streetAddress = streetAddress
    self.locality = locality
    self.region = region
    self.postalCode = postalCode
    guard country.count == 2, country == country.uppercased() else {
      fatalError("invalid country code")
    }
    self.country = country
  }
}

let applePark = ShippingAddress(
  recipient: "Apple, Inc.", streetAddress: "1 Apple Park Way", locality: "Cupertino", region: "CA",
  postalCode: "95014", country: "US")
複製程式碼

隨著 0.0.1 版本的釋出,這好像很有希望!我們可以在沒有原始分號的情況下實現,也不用太關心 region 屬性的冒號位置,但總的來說,這是無可非議的,它正是你想要的官方程式碼格式化工具。

效能

在效能方面,swift-format 目前處於中間位置,不快也不慢。

$ time swift-format Example.swift
        0.51 real         0.20 user         0.27 sys
複製程式碼

根據我們有限的初步調查,swift-format 似乎提供了一套合理的格式約定。希望未來它能建立出更加生動的例子來幫助我們理解其中的格式化規則。

無論如何,看到提案如何發展以及圍繞這些問題展開討論都將是一件很有趣的事。


NSMutableHipster

如果你有其他問題,歡迎給我們提 Issuespull requests

這篇文章使用 Swift 5.0.。你可以在 狀態頁面 上查詢所有文章的狀態資訊。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章