使用 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 的並行處理指南說明了可傳送值,以及其儲存屬性都可傳送的結構所需符合的要求。
對這項小型桌面計算,直接呼叫任一函式並檢視其回傳字典即可。如果群組成員仍有用途,請選擇分組;如果總和已足夠,則選擇直接累加。保留空輸入與總和為零的規則,明確拒絕算術溢位,並只在展示結果時排序。
來源
- Install Swift — macOS — Swift.org。
- Dictionary — Apple 開發者文件。
- init(grouping:by:) — Apple 開發者文件。
- mapValues(_:) — Apple 開發者文件。
- reduce(::) — Apple 開發者文件。
- reduce(into:_:) — Apple 開發者文件。
- subscript(_:default:) — Apple 開發者文件。
- addingReportingOverflow(_:) — Apple 開發者文件。
- NSDictionary — Apple 開發者文件。
- Concurrency — Swift 程式語言。