使用 Dictionary(grouping:by:) 依索引鍵收集合成的 Swift 值,接著使用 mapValues 和 reduce 計算各組總和。這篇 Mac 教學也會示範直接累加,並檢查空輸入、總和為零及整數溢位。

這項工作是將記憶體中的中性形狀標籤與整數數量陣列,轉換成一個 [String: Int] 字典。所有輸入都是合成常值。計算不會讀取外部記錄、不會發出網路請求,也不會儲存結果。

定義每個索引鍵一個總和的意義

選擇集合方法之前,先定義結果的契約。此處每個輸入元素都會將其數量恰好一次加到對應標籤的總和中。標籤採用一般的 Swift 字串相等比較;程式不會修剪空白或轉換大小寫。字典索引鍵必須遵循 Hashable,而 Swift 支援以字串作為索引鍵。請參閱 Apple 的Dictionary 文件。

示範輸入包含兩個圓形值、兩個方形值及一個三角形值。預期總和很簡單,可以手動核對:

標籤 輸入數量 預期總和
circle 4、6 10
square 3、5 8
triangle 0 0

此範例採用以下規則:

  • 空輸入會產生空字典。
  • 若標籤出現時數量為零,結果中仍會保留該標籤,且總和為零。
  • 允許負數,並以一般加法計入總和。
  • 輸入中未出現的標籤不會出現在結果中。
  • 每個中間總和都必須能以 Int 表示;否則函式會丟出錯誤,不回傳任何總和。

標籤不存在與總和為零之間的差異很重要。標籤缺漏表示沒有符合的輸入;存在但總和為零,則可能表示明確輸入了零,或多個數量互相抵銷。除非結果的讀取者明確需要不同慣例,否則請保留這項區別。

在 Mac 上執行完整範例

使用已安裝的 Swift 工具鏈。Swift.org 的 macOS 安裝指南說明了工具鏈設定,以及建立和執行可執行套件的命令。工具鏈準備就緒後,開啟 Terminal 並建立一個小型本機專案:

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

開啟 Sources 下產生的可執行程式原始碼檔案,並以以下完整程式取代其內容。產生的檔案可能位於以目標命名的子資料夾中。保留產生的套件資訊清單;此程式不需要新增相依套件或匯入項目,因為其型別和計算方法都屬於 Swift 標準函式庫。

struct SyntheticValue: Sendable {
    let key: String
    let quantity: Int
}

enum TotalError: Error {
    case overflow(key: String)
}

func checkedAdd(_ total: Int, _ value: SyntheticValue) throws -> Int {
    let addition = total.addingReportingOverflow(value.quantity)
    guard !addition.overflow else {
        throw TotalError.overflow(key: value.key)
    }
    return addition.partialValue
}

func groupThenTotal(_ values: [SyntheticValue]) throws -> [String: Int] {
    let groups = Dictionary(grouping: values, by: { $0.key })
    return try groups.mapValues { group in
        try group.reduce(0) { total, value in
            try checkedAdd(total, value)
        }
    }
}

func totalDirectly(_ values: [SyntheticValue]) throws -> [String: Int] {
    return try values.reduce(into: [String: Int]()) { totals, value in
        let previous = totals[value.key, default: 0]
        totals[value.key] = try checkedAdd(previous, value)
    }
}

let sample: [SyntheticValue] = [
    SyntheticValue(key: "circle", quantity: 4),
    SyntheticValue(key: "square", quantity: 3),
    SyntheticValue(key: "circle", quantity: 6),
    SyntheticValue(key: "triangle", quantity: 0),
    SyntheticValue(key: "square", quantity: 5)
]

let expected = ["circle": 10, "square": 8, "triangle": 0]

 do {
    let groupedTotals = try groupThenTotal(sample)
    let directTotals = try totalDirectly(sample)
    precondition(groupedTotals == expected)
    precondition(directTotals == expected)

    let emptyGrouped = try groupThenTotal([])
    let emptyDirect = try totalDirectly([])
    precondition(emptyGrouped.isEmpty && emptyDirect.isEmpty)

    let cancellation = [
        SyntheticValue(key: "circle", quantity: 2),
        SyntheticValue(key: "circle", quantity: -2)
    ]
    let zeroGrouped = try groupThenTotal(cancellation)
    let zeroDirect = try totalDirectly(cancellation)
    precondition(zeroGrouped == ["circle": 0])
    precondition(zeroDirect == ["circle": 0])

    for key in groupedTotals.keys.sorted() {
        if let total = groupedTotals[key] {
            print("\(key): \(total)")
        }
    }

    let tooLarge = [
        SyntheticValue(key: "circle", quantity: Int.max),
        SyntheticValue(key: "circle", quantity: 1)
    ]
    do {
        _ = try totalDirectly(tooLarge)
        preconditionFailure("Expected overflow to be reported")
    } catch TotalError.overflow(let key) {
        print("Overflow check: rejected \(key)")
    }
} catch {
    fatalError("Unexpected calculation failure: \(error)")
}

在套件目錄中,使用 Swift.org macOS 指南所記載的命令執行程式:

swift run SyntheticTotals

預期程式輸出如下,不含建置訊息:

circle: 10
square: 8
triangle: 0
Overflow check: rejected circle

程式包含對範例總和、空輸入、相消為零及刻意觸發溢位的檢查。這些預期結果是根據範例和已記載的 API 推導而來。程式碼已經檢視,但準備期間因可用環境沒有 Swift 編譯器,未實際執行。

分組後再歸約是如何運作的?

第一個函式刻意將工作拆分成清楚可見的步驟。Dictionary(grouping: values, by: { $0.key }) 會建立一個字典,其值是符合條件元素的陣列。Apple 文件指出,每個產生的群組都非空,且會保留元素在來源序列中的順序。請參閱 init(grouping:by:)。

在此範例中,中間型別是 [String: [SyntheticValue]]。圓形群組包含數量 4 和 6;方形群組包含 3 和 5;三角形群組則包含 0。如果本機計算的其他部分也需要檢視每個群組的成員,這種中間表示法就很實用。

接著,mapValues 會保留字典的索引鍵,並將每個群組陣列轉換成單一整數。這項保留索引鍵的行為,是 mapValues(_:) 文件所述的功能。這也說明了為什麼三角形項目即使總和為零,仍會保留下來。

在這項轉換中,reduce(0) 會從零開始,並將每個後續元素傳入合併閉包。閉包會回傳下一個累計總和。Apple 文件記載了循序歸約的行為,並指出歸約空序列時會回傳初始值。請參閱 reduce(::)。

此處有兩種不同的空情況。若以零為起始值歸約空的數量陣列,結果會是零。然而,對空輸入進行分組不會建立任何群組,因此完整函式會回傳空字典,而不會憑空建立總和為零的標籤。

何時應直接累加總和?

若只需要最終總和,請使用 totalDirectly。其累加器從空的 [String: Int] 字典開始。對每個輸入元素,它會查找該標籤先前的總和、加上數量,並儲存更新後的總和。

帶預設值的查找會在索引鍵不存在時提供零。單純讀取預設值不會建立項目;下一行的指定運算才會儲存計算結果。Apple 的預設值下標文件說明了回退查找,以及對具有值語意的值進行原地更新。

reduce(into:) 會讓閉包以可變方式存取累加器。Apple 建議在建構具有寫時複製行為的結果(例如字典)時,使用此形式,而非一般的 reduce。請參閱 reduce(into:_:)。

下一步需要的內容 選擇方式 原因
群組成員及其總和 分組後再歸約 可檢視或重複使用中間陣列。
每個標籤只需要一個整數 直接累加 可避免建立中間群組陣列。
可重複輸出的文字 任一方式,再排序索引鍵 輸出順序是彙總之外的另一項決策。

中間儲存的差異可從程式碼看出:分組會將所有輸入元素保留在群組陣列中,而直接累加只會為每個不同索引鍵保留一個累計總和。這是實作方式的比較,並非經過測量的效能主張。此範例沒有提供計時基準測試。

如何處理溢位並確保輸出穩定?

checkedAdd 會呼叫 addingReportingOverflow,後者會回傳部分結果和溢位旗標。Apple 指出,只有在旗標為 false 時,部分值才包含完整總和;發生溢位時,回傳的部分值會被截斷。因此,程式會在儲存前丟出錯誤。請參閱 addingReportingOverflow(_:)。

此契約會依照每個索引鍵中輸入的順序,檢查每次加法。例如,同一標籤的數量若依序為 Int.max、1 和 -1,會在第二次加法時遭到拒絕,即使其數學上的最終總和能以 Int 表示。若要支援這類輸入,必須採用不同的算術策略;接受遭截斷的中間結果會改變契約。

如果多個群組都發生溢位,分組實作不保證哪個標籤的錯誤會先出現:字典的迭代順序不可預測。直接累加實作則會依來源順序處理元素。兩者都會拒絕溢位計算,但最先回報的索引鍵可能不同。

排序結果的索引鍵可讓顯示的總和順序固定。Swift 字典沒有順序,因此不應直接列印字典來建立展示順序。Apple 在 Dictionary 文件中說明了這項順序特性。範例只在彙總完成後排序索引鍵,不會改變計算方式。

Foundation 或 Swift 並行處理會改變這項計算嗎?

此值契約不需要 Foundation。Apple 文件記載,若需要參考語意,可以使用 NSDictionary。此處的具型別 Swift 字典已能表示標籤到整數總和的對應關係。請參閱 Foundation 的 NSDictionary 文件。

這項計算是同步的,並將可變累加器保留在函式內。範例中的 SyntheticValue 明確遵循 Sendable;這表達的是與並行處理相關的值契約,並不代表要求以並行方式執行歸約。Swift 的並行處理指南說明了可傳送值,以及其儲存屬性都可傳送的結構所需符合的要求。

對這項小型桌面計算,直接呼叫任一函式並檢視其回傳字典即可。如果群組成員仍有用途,請選擇分組;如果總和已足夠,則選擇直接累加。保留空輸入與總和為零的規則,明確拒絕算術溢位,並只在展示結果時排序。

來源