若要忽略關聯值並抑制 Swift 中連續的列舉案例,請比較獨立的案例鍵,並保留每個連續區段中的第一個完整值。這個本機 Mac 範例會保留順序,並允許某個案例在另一個案例出現後再次出現。

明確定義重複項目的判定方式

假設桌面圖形預覽會產生一串合成步驟:閒置、繪製具名形狀,以及顯示具名預覽。你想要一份精簡的案例轉換清單。即使形狀名稱不同,連續兩個繪製步驟仍視為重複。

Swift 列舉可以為各個案例帶有不同的關聯值。這些值是列舉值的一部分,因此若要忽略它們,就需要明確的比較規則。Swift 語言指南在列舉中說明了這項區別。

此處採用以下規則:

  • 保留第一個輸入值。
  • 後續值若案例與前一個連續區段的案例相同,就予以抑制。
  • 保留每個留下元素的完整關聯值。
  • 某個案例在不同案例之後再次出現時,仍予以保留。
  • 保留輸入順序;空輸入會產生空結果。

以下以示意輸入說明預期行為。所有名稱都是合成的。

輸入值 決定 原因
.idle 保留 第一個值
.drawing("Circle") 保留 案例已變更
.drawing("Square") 抑制 仍在繪製
.ready("Preview A") 保留 案例已變更
.ready("Preview B") 抑制 仍在就緒狀態
.drawing("Triangle") 保留 就緒後接著繪製
.idle 保留 案例已變更
.idle 抑制 仍在閒置狀態

因此,留下的繪製值是 \"Circle\" 和 \"Triangle\"。\"Square\" 負載會刻意排除。如果桌面檢視必須顯示每次更新的形狀名稱,這項規則會丟棄該檢視所需的變更;請為該使用情境選擇會比較負載的方式。

在 Mac 上執行完整的本機 Swift 範例

請使用具備可正常運作 Swift 開發環境的 Mac。Swift.org 建議 Apple 平台開發使用 Xcode,並說明如何使用 Swift Package Manager 建立命令列可執行檔。若需要準備工具,請參閱安裝 Swift — macOS。

在 Terminal 中的新工作目錄建立一個小型可執行套件:

mkdir EnumCaseDemo
cd EnumCaseDemo
swift package init --name EnumCaseDemo --type executable

將產生的可執行檔來源檔內容替換為以下完整程式。此程式使用 Swift 標準函式庫型別,不需要匯入任何模組或套件相依項。輸入和輸出都保留在記憶體中;程式會印出留下的合成步驟。

enum PreviewStep: Equatable {
    case idle
    case drawing(String)
    case ready(String)

    enum Kind: Equatable {
        case idle
        case drawing
        case ready
    }

    var kind: Kind {
        switch self {
        case .idle:
            return .idle
        case .drawing(_):
            return .drawing
        case .ready(_):
            return .ready
        }
    }

    var label: String {
        switch self {
        case .idle:
            return "idle"
        case .drawing(let shape):
            return "drawing: \(shape)"
        case .ready(let name):
            return "ready: \(name)"
        }
    }
}

func suppressConsecutiveCases(
    _ steps: [PreviewStep]
) -> [PreviewStep] {
    var result: [PreviewStep] = []
    var previousKind: PreviewStep.Kind? = nil

    for step in steps {
        let currentKind = step.kind
        if let previous = previousKind,
           previous == currentKind {
            continue
        }

        result.append(step)
        previousKind = currentKind
    }

    return result
}

let input: [PreviewStep] = [
    .idle,
    .drawing("Circle"),
    .drawing("Square"),
    .ready("Preview A"),
    .ready("Preview B"),
    .drawing("Triangle"),
    .idle,
    .idle
]

let expected: [PreviewStep] = [
    .idle,
    .drawing("Circle"),
    .ready("Preview A"),
    .drawing("Triangle"),
    .idle
]

let output = suppressConsecutiveCases(input)

precondition(output == expected)
precondition(suppressConsecutiveCases([]).isEmpty)
precondition(
    suppressConsecutiveCases([
        .drawing("Circle"), .drawing("Square")
    ]) == [.drawing("Circle")]
)
precondition(
    suppressConsecutiveCases([
        .idle, .drawing("Circle"), .idle
    ]) == [.idle, .drawing("Circle"), .idle]
)

for step in output {
    print(step.label)
}

在套件目錄中執行:

swift run EnumCaseDemo

程式預期輸出如下,前面可能會先出現建置訊息:

idle
drawing: Circle
ready: Preview A
drawing: Triangle
idle

這些範例已根據取得的文件檢視過;由於研究環境沒有 Swift 可執行檔,因此未在此處執行。內含的前置條件會檢查預期保留的負載、空輸入、連續但負載不同的案例,以及案例在轉換後再次出現的情況。

為何獨立的案例鍵有效

kind 屬性會將每個完整列舉值轉換成精簡且不含負載的鍵。.drawing("Circle") 和 .drawing("Square") 都會產生 .drawing 這個鍵。篩選函式比較這些鍵,同時附加原始的 step,因此留下的關聯值會保持完整。

此函式只需要記住一個鍵。一開始,previousKind 是 nil,因此第一個元素會通過。保留元素後,函式會記住該元素的種類。被抑制的元素種類都相同,因此對它們更新記住的鍵不會造成差異。

此實作會走訪輸入一次。對這個固定列舉及其固定大小的鍵而言,它會對每個後續元素進行一次案例鍵比較,並使用固定的輔助狀態,以及回傳的陣列。Swift 的Sequence 文件說明了循序走訪與一般的線性走訪模型。

完整的 PreviewStep 也遵循 Equatable,因此前置條件能驗證完整結果,包括留下的字串。Swift 支援為符合條件的列舉合成相等比較,其關聯值必須遵循 Equatable;請參閱協定。將案例比較放在 Kind 中,能讓這項篩選操作採用明確的專屬規則,而不必重新定義整個程式中的相等比較方式。

kind 中完整的 switch 也提供實用的維護檢查點。新增案例時,請加入對應的鍵與映射。避免使用萬用映射,以免新案例意外地與既有案例歸為同一組。Swift 在列舉中說明了列舉切換的窮盡性。

在 Combine 中使用相同的比較方式

如果你的 Mac 程式碼已使用 Combine publisher,Apple 提供了 removeDuplicates(by:),可用於自訂比較。當第二個值應視為重複項目時,其述詞會回傳 true。Apple 明確支援在預期的比較方式與輸出型別一般相等比較不同時使用此運算子。macOS 10.15 及更新版本均可使用。請參閱 removeDuplicates(by:)。

以下是擴充片段,並非獨立程式。請在可使用 Combine 的 Mac 上,將它附加到上方的完整範例。它會重用現有的 PreviewStep 定義與 input 陣列:

import Combine

let subscription = input.publisher
    .removeDuplicates { previous, current in
        previous.kind == current.kind
    }
    .sink { step in
        print("Combine: \(step.label)")
    }

這會印出相同的五個保留步驟,並加上 \"Combine: \" 前綴。述詞會比較案例鍵,而 publisher 會傳遞原本留下的列舉值。Apple 自己的運算子範例也遵循相同原則:比較一個屬性,並忽略另一個屬性。removeDuplicates(by:)

請在必要的訂閱期間內,讓回傳的訂閱維持有效。Apple 說明,將 sink(receiveValue:) 回傳的值釋放時,訂閱便會終止。此多載適用於失敗型別為 Never 的 publisher,例如 Apple 的序列 publisher 範例。sink(receiveValue:)

調整程式碼前先確認邊界條件

如果你已有有限的本機清單,請使用陣列函式。如果值已透過 publisher 傳入,請使用 Combine 運算子。此處兩個版本都表達相同的案例轉換規則。

需求 適合的比較方式或作法
保留每個案例區段中的第一個完整值 如圖所示,比較 kind
保留關聯字串的變更 使用一般相等比較完整值
保留每個區段中的最後一個完整值 緩衝待處理值,並在案例轉換時及結尾處送出
其他案例出現後,仍保留再次出現的案例 比較連續區段,而不是維護全域的已見案例清單
分別篩選不同陣列 分別呼叫函式;每次呼叫都會從空的比較狀態開始

保留最後一個負載需要不同的演算法,因為此程式會立即附加第一個值。同樣地,將同一個邏輯序列拆成不同陣列會重設比較狀態:若一批的結尾是繪製值,而下一批的開頭也是繪製值,分別呼叫時兩者都會留下。如果多個批次必須視為同一序列,請在批次邊界之間保留前一個鍵。

Foundation 為 Apple 平台的 App 提供集合與其他基礎功能,但這項計算完全以 Swift 型別實作。publisher 版本則特別需要 Combine;加入 import Foundation 並不會匯入該運算子。Apple 在Foundation和 removeDuplicates(by:) 中分別說明了這些框架。

同步的陣列函式也不需要並行處理機制:比較狀態屬於各次函式呼叫。如果之後加入共用的桌面呈現狀態,請另行定義該狀態的隔離方式。Apple 將 MainActor 說明為單例 actor,其執行器等同於主派送佇列;重複抑制本身不會選擇 actor。MainActor

就此處的本機任務而言,關鍵在於每個區段中要保留哪個負載。選擇第一個負載後,獨立的案例鍵便能讓行為易於閱讀與驗證:遇到案例轉換時保留原始值,抑制連續且相符的鍵,並讓再次出現的案例通過。

來源