若要在並行 Swift 計算中保留具型別的錯誤,請讓每個子任務回傳具型別的 Result,並在收集後解開結果。這個 Mac 範例會計算合成矩形的像素數,同時維持輸入順序、明確回報失敗,並採用整批成功或整批失敗的契約。
為什麼要在任務群組邊界使用具型別的結果?
假設你正在建置桌面圖形預覽功能,需要計算數個矩形的像素數。每次計算都可能因尺寸無效而拒絕處理,或回報整數溢位。呼叫端需要取得完整且有序的陣列,或特定的計算錯誤,才能明確處理所有支援的失敗情況。
Swift 在 Swift 6.0 引入具型別的 throws。像 throws(CalculationError) 這樣的宣告會將函式可能拋出的值限制為該錯誤型別。對於實作與呼叫端一同演進的小型封閉計算契約,這很有用。請參閱 SE-0413:具型別的 throws。
並行 API 的邊界很重要。Apple 文件中的 withThrowingTaskGroup 簽章會將 ThrowingTaskGroup<ChildTaskResult, any Error> 傳入其主體。這個看似泛型的群組型別,並未透過該工廠函式提供具體的領域錯誤參數。因此,單在計算函式加入具型別的 throws,並不能讓整個群組 API 都具備具體的拋出型別。請參閱 withThrowingTaskGroup。
一種受支援的做法是使用 withTaskGroup,並讓每個子任務回傳 Result<Int, CalculationError>。任務會正常完成,並以值表示成功或失敗。收集這些值後,批次函式會呼叫 get(),其文件所列的簽章是 throws(Failure)。這樣就能在不使用執行期轉型的情況下,恢復具型別的錯誤拋出。請參閱 Apple 的 Result 文件和 get() 文件。
撰寫並行程式碼前,先定義完整的批次契約
在此範例中,每筆輸入包含以像素為單位的整數寬度和高度。計算結果是兩者的乘積。所有資料都是合成的,並留在記憶體中;程式不會繪製影像,也不會載入或儲存應用程式資料。
以下政策是此範例的設計選擇。它們讓回傳值和選定的錯誤都可預期:
| 情況 | 批次行為 |
|---|---|
| 所有矩形皆有效 | 依輸入順序回傳像素數 |
| 寬度或高度為零或負數 | 將該矩形回報為無效 |
乘積無法放入 Int |
回報該矩形發生溢位 |
| 多個矩形失敗 | 回報輸入索引最低者的失敗 |
| 在批次檢查點觀察到取消 | 回報 .cancelled |
| 輸入陣列為空 | 回傳空陣列,除非觀察到取消 |
選擇輸入索引最低的項目,必須先收集所有子任務結果,再決定要回報哪個計算錯誤。Swift 不保證任務群組的排程順序,完成順序也可能與提交順序不同。為每個結果附上索引,就能讓收集器還原預期順序。請參閱 TaskGroup。
這項政策適合重視回饋一致性的小型預覽批次,也刻意等待所有計算完成。若改成回報執行期間最先觀察到的失敗,取捨就會不同,而且不同執行次數可能選到不同的矩形。
在 Mac 上建立完整的本機範例
使用已安裝的 Swift 6 或更新版本工具鏈。在 Terminal 中執行 swift --version 檢查。Swift.org 在開始使用中說明此檢查方式,並在安裝 Swift — macOS提供 Mac 安裝指示。
依照安裝頁面記載的流程,建立一個小型可執行套件:
mkdir TypedCalculations
cd TypedCalculations
swift package init --name TypedCalculations --type executable
將 Sources 下產生的 Swift 入口檔替換為以下三個程式碼區塊,並依序放在同一個檔案中。只保留一個可執行入口點。此範例使用套件的一般預設 actor 隔離;若專案設定為預設由主 actor 隔離,則必須將計算型別與函式移至該隔離範圍之外。
定義輸入與具體錯誤
錯誤列舉會保留結構化資訊:案例指出問題,關聯索引則指出輸入項目。Foundation 的 LocalizedError 協定提供可用於呈現的選用 errorDescription。下方的英文訊息是範例字串;遵循此協定不會自動翻譯它們。請參閱 LocalizedError。
import Foundation
struct Rectangle: Sendable {
let width: Int
let height: Int
}
enum CalculationError: Error, Sendable, LocalizedError {
case invalidDimensions(index: Int)
case overflow(index: Int)
case cancelled
var errorDescription: String? {
switch self {
case .invalidDimensions(let index):
return "Rectangle \(index + 1) needs positive dimensions."
case .overflow(let index):
return "Rectangle \(index + 1) has too many pixels for Int."
case .cancelled:
return "Calculation cancelled."
}
}
}
struct IndexedOutcome: Sendable {
let index: Int
let result: Result<Int, CalculationError>
}
計算、收集並恢復具型別的錯誤拋出
算術運算使用 multipliedReportingOverflow(by:)。Apple 文件指出,只有在 overflow 為 false 時,其 partialValue 才是完整乘積;否則該值會是截斷後的乘積。下方的檢查可避免將該截斷值當成成功的像素數。請參閱 multipliedReportingOverflow(by:)。
func pixelCount(
for rectangle: Rectangle,
index: Int
) throws(CalculationError) -> Int {
if Task.isCancelled {
throw .cancelled
}
guard rectangle.width > 0, rectangle.height > 0 else {
throw .invalidDimensions(index: index)
}
let product = rectangle.width.multipliedReportingOverflow(
by: rectangle.height
)
guard !product.overflow else {
throw .overflow(index: index)
}
return product.partialValue
}
func calculateBatch(
_ rectangles: [Rectangle]
) async throws(CalculationError) -> [Int] {
if Task.isCancelled {
throw .cancelled
}
let outcomes: [IndexedOutcome] = await withTaskGroup(
of: IndexedOutcome.self,
returning: [IndexedOutcome].self
) { group in
for (index, rectangle) in rectangles.enumerated() {
group.addTask {
let result = Result<Int, CalculationError> {
() throws(CalculationError) -> Int in
try pixelCount(for: rectangle, index: index)
}
return IndexedOutcome(index: index, result: result)
}
}
var collected: [IndexedOutcome] = []
for await outcome in group {
collected.append(outcome)
}
return collected.sorted { $0.index < $1.index }
}
if Task.isCancelled {
throw .cancelled
}
var counts: [Int] = []
counts.reserveCapacity(outcomes.count)
for outcome in outcomes {
counts.append(try outcome.result.get())
}
return counts
}
在 Result 閉包上明確標註 throws(CalculationError),可清楚呈現其錯誤契約,而不依賴閉包推斷。SE-0413 指出,原先規劃的完整具型別 throws 推斷變更並未在 Swift 6.0 中實作。請參閱具型別的 throws中的實作說明。
只有父層收集器會附加到 collected。每個子任務都會收到一個矩形和索引,並回傳自己的 Sendable 結果。這符合任務群組要求子任務結果值必須為 Sendable 的規定;非拋出錯誤的群組主體會使用 for await 收集結果。請參閱 withTaskGroup。
加入可執行入口點
最後這個區塊會提供所有範例輸入並呼叫批次函式。catch 內明確的指派,也會在編譯時檢查預期的 API 契約:捕捉到的值必須是 CalculationError。
@main
struct TypedCalculations {
static func main() async {
let examples: [(name: String, rectangles: [Rectangle])] = [
(
name: "Valid",
rectangles: [
Rectangle(width: 640, height: 480),
Rectangle(width: 320, height: 200)
]
),
(
name: "Multiple failures",
rectangles: [
Rectangle(width: 0, height: 20),
Rectangle(width: Int.max, height: 2)
]
),
(
name: "Overflow",
rectangles: [
Rectangle(width: Int.max, height: 2)
]
),
(name: "Empty", rectangles: [])
]
for example in examples {
do {
let counts = try await calculateBatch(example.rectangles)
print("\(example.name): \(counts)")
} catch {
let failure: CalculationError = error
print("\(example.name): \(failure.errorDescription ?? "Calculation failed.")")
}
}
}
}
在套件目錄中執行可執行檔:
swift run TypedCalculations
預期的應用程式輸出如下,不包含建置訊息。研究環境中沒有 Swift 編譯器,因此此程式是依據取得的 API 文件檢視,並未在該環境中編譯或執行。
Valid: [307200, 64000]
Multiple failures: Rectangle 1 needs positive dimensions.
Overflow: Rectangle 1 has too many pixels for Int.
Empty: []
有效計數可直接由範例輸入得出:640 乘以 480 等於 307,200,320 乘以 200 等於 64,000。在多重失敗範例中,第一個矩形會勝出,因為結果是依輸入順序檢查,即使第二個子任務較早完成也一樣。
取消與部分結果會如何處理?
Swift 的取消機制採合作式。任務必須觀察取消並做出回應;提出取消要求不會自動中斷其計算。結構化任務群組也會等待子任務完成後才回傳。請參閱 Swift 語言指南的並行章節和 Apple 的 withTaskGroup 文件。
此處會在提交前、每次計算內,以及收集完成後檢查 Task.isCancelled。檢查點觀察到取消時,程式會使用現有的 .cancelled 案例表示。讀取這個布林值可避免在封閉契約中引入另一種拋出錯誤型別。Apple 在 Task.isCancelled中說明了此屬性。
收集完成後的檢查會讓已觀察到的批次取消優先於矩形錯誤。這些檢查是檢查點,並非原子式保證:若在最後一次檢查後才提出取消要求,可能不會影響回傳結果。若較長的計算包含重複工作,請在該工作內加入檢查,讓程式能更早觀察到取消。
此實作中,子任務失敗不會觸發取消。子任務會回傳一般的 Result.failure 值,收集器則繼續執行。若所有子任務都成功完成,呼叫端會收到所有計數。若解開結果時遇到失敗,函式就會拋出錯誤,而區域變數中部分組成的 counts 陣列不會被回傳。
若桌面介面需要每個矩形都有成功或失敗狀態,就應將公開的批次契約改為回傳有序結果。每個失敗都要保留其索引。這與此處實作的整批成功或失敗陣列,對使用者呈現的行為不同。
診斷遺失的錯誤型別,同時保留失敗資訊
若改寫後的程式無法編譯,請依序追蹤每個邊界上的型別:
- 確認計算宣告為
throws(CalculationError)。 - 確認子任務值為
Result<Int, CalculationError>,而非失敗型別為any Error的結果。 - 若推斷擴大了錯誤型別,請保留閉包上明確的具型別 throws 註記。
- 使用非拋出錯誤的
withTaskGroup傳遞這些結果值。 - 在具型別的批次函式內使用
get()解開結果。 - 檢查任何新增的拋出錯誤操作是否引入了不同的錯誤型別。
強制轉型無法證明此契約。同樣地,捨棄失敗結果或以零取代也會改變計算意義。請將失敗保留為值,直到選定的批次政策決定如何回報為止。
對這些微小乘法而言,並行處理只是架構示範;此處不宣稱有任何效能提升。實用之處在於精確的契約:成功計數維持輸入順序、多重失敗有確定的選擇規則,而且呼叫端會收到可直接處理的具體錯誤列舉。
來源
具型別的 throws — SE-0413 — Swift 專案。
withThrowingTaskGroup(of:returning:isolation:body:) — Apple。
Result — Apple。
get() — Apple。
TaskGroup — Apple。
開始使用 — Swift.org。
安裝 Swift — macOS — Swift.org。
LocalizedError — Apple。
multipliedReportingOverflow(by:) — Apple。
並行 — Swift 程式語言 — Swift 專案。
isCancelled — Apple。