Um typisierte Fehler bei nebenläufigen Swift-Berechnungen zu bewahren, gib von jeder untergeordneten Aufgabe ein typisiertes Result zurück und entpacke es nach dem Sammeln. Dieses Mac-Beispiel berechnet synthetische Pixelzahlen für Rechtecke und bewahrt dabei die Eingabereihenfolge, explizite Fehler und einen Batch-Vertrag nach dem Alles-oder-Fehler-Prinzip.
Warum typisierte Ergebnisse an der Task-Group-Grenze verwenden?
Angenommen, du entwickelst eine Desktop-Grafikvorschau, die Pixelzahlen für mehrere Rechtecke berechnet. Jede Berechnung kann ungültige Maße zurückweisen oder einen Ganzzahlüberlauf melden. Der Aufrufer benötigt ein vollständiges, geordnetes Array oder einen bestimmten Berechnungsfehler, damit er jeden unterstützten Fehler explizit behandeln kann.
Swift führte in Swift 6.0 typisierte Throws ein. Eine Deklaration wie throws(CalculationError) beschränkt die von der Funktion geworfenen Werte auf diesen Fehlertyp. Das ist für einen kleinen, abgeschlossenen Berechnungsvertrag nützlich, bei dem sich Implementierung und Aufrufer gemeinsam weiterentwickeln. Siehe SE-0413: Typed throws.
Die Grenze der Nebenläufigkeits-API ist entscheidend. Apples dokumentierte Signatur von withThrowingTaskGroup übergibt dem Funktionskörper eine ThrowingTaskGroup<ChildTaskResult, any Error>. Der generisch wirkende Gruppentyp bietet über diese Factory keinen konkreten Typparameter für den Domänenfehler. Typisierte Throws nur zu deiner Berechnung hinzuzufügen, legt daher keinen konkreten Fehlertyp für die gesamte Gruppen-API fest. Siehe withThrowingTaskGroup.
Ein unterstützter Weg ist die Verwendung von withTaskGroup, sodass jedes untergeordnete Element Result<Int, CalculationError> zurückgibt. Die Aufgabe wird normal mit einem Wert abgeschlossen, der Erfolg oder Fehlschlag darstellt. Nachdem diese Werte gesammelt wurden, ruft die Batch-Funktion get() auf, dessen dokumentierte Signatur throws(Failure) lautet. So erhältst du typisiertes Werfen zurück, ohne einen Laufzeit-Cast. Siehe Apples Dokumentation zu Result und zu get().
Den Batch-Vertrag festlegen, bevor du Nebenläufigkeitscode schreibst
In diesem Beispiel enthält jede Eingabe eine ganzzahlige Breite und Höhe in Pixeln. Die Berechnung ist ihr Produkt. Alles ist synthetisch und bleibt im Arbeitsspeicher; das Programm rendert weder ein Bild noch lädt oder speichert es Anwendungsdaten.
Die folgenden Regeln sind Entwurfsentscheidungen für dieses Beispiel. Sie machen sowohl die zurückgegebenen Werte als auch den ausgewählten Fehler vorhersehbar:
| Situation | Verhalten des Batches |
|---|---|
| Alle Rechtecke sind gültig | Pixelzahlen in Eingabereihenfolge zurückgeben |
| Eine Breite oder Höhe ist null oder negativ | Dieses Rechteck als ungültig melden |
Ein Produkt passt nicht in Int |
Überlauf für dieses Rechteck melden |
| Mehrere Rechtecke schlagen fehl | Den Fehler mit dem niedrigsten Eingabeindex melden |
| An einem Batch-Prüfpunkt wird ein Abbruch erkannt | .cancelled melden |
| Das Eingabearray ist leer | Ein leeres Array zurückgeben, sofern kein Abbruch erkannt wird |
Die Auswahl des niedrigsten Eingabeindexes erfordert, dass alle Ergebnisse der untergeordneten Aufgaben gesammelt werden, bevor ein Berechnungsfehler ausgewählt wird. Swift garantiert keine Planungsreihenfolge für Task-Groups, und die Abschlussreihenfolge kann von der Übermittlungsreihenfolge abweichen. Wenn jedem Ergebnis ein Index zugeordnet wird, kann der Sammler die gewünschte Reihenfolge wiederherstellen. Siehe TaskGroup.
Diese Regel eignet sich für einen kleinen Vorschau-Batch, bei dem wiederholbares Feedback wichtig ist. Sie wartet bewusst auf alle Berechnungen. Ein Entwurf, der den ersten während der Ausführung beobachteten Fehler meldet, würde einen anderen Kompromiss eingehen und könnte je nach Lauf ein anderes Rechteck auswählen.
Ein vollständiges lokales Beispiel auf deinem Mac erstellen
Verwende eine installierte Swift-6-Toolchain oder neuer. Prüfe sie in Terminal mit swift --version. Swift.org dokumentiert diese Prüfung unter Getting Started und bietet Installationsanweisungen für den Mac unter Install Swift — macOS.
Erstelle ein kleines ausführbares Paket mit dem auf der Installationsseite dokumentierten Ablauf:
mkdir TypedCalculations
cd TypedCalculations
swift package init --name TypedCalculations --type executable
Ersetze die generierte Swift-Einstiegsdatei unter Sources durch die folgenden drei Codeblöcke in der angegebenen Reihenfolge und in einer einzigen Datei. Behalte nur einen ausführbaren Einstiegspunkt. Dieses Beispiel verwendet die normale Standardisolierung des Actors des Pakets; bei einem Projekt mit standardmäßiger Main-Actor-Isolierung müssen die Berechnungstypen und -funktionen außerhalb dieser Isolierung platziert werden.
Eingaben und konkrete Fehler definieren
Das Fehler-Enum bewahrt strukturierte Informationen: Der Fall kennzeichnet das Problem, und sein zugehöriger Index kennzeichnet die Eingabe. Das Foundation-Protokoll LocalizedError stellt für die Darstellung ein optionales errorDescription bereit. Die englischen Meldungen unten sind Beispieltexte; durch die Konformität mit dem Protokoll werden sie nicht automatisch übersetzt. Siehe 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>
}
Berechnen, sammeln und typisiertes Werfen wiederherstellen
Für die Arithmetik wird multipliedReportingOverflow(by:) verwendet. Apple dokumentiert, dass sein partialValue nur dann das vollständige Produkt ist, wenn overflow false ist; andernfalls enthält es ein abgeschnittenes Produkt. Die folgende Prüfung verhindert, dass dieser abgeschnittene Wert als erfolgreiche Pixelzahl verwendet wird. Siehe 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
}
Die explizite Annotation throws(CalculationError) für den Result-Closure macht den Fehlervertrag sichtbar, statt sich auf die Inferenz des Closures zu verlassen. SE-0413 merkt an, dass die ursprünglich geplanten umfassenden Änderungen an der Inferenz von Typed Throws in Swift 6.0 nicht umgesetzt wurden. Siehe den Implementierungshinweis unter Typed throws.
Nur der übergeordnete Sammler fügt Werte zu collected hinzu. Jedes untergeordnete Element erhält ein Rechteck und einen Index und gibt ein eigenes Sendable-Ergebnis zurück. Das entspricht der Anforderung der Task-Group, dass Werte untergeordneter Ergebnisse Sendable sein müssen; der nicht werfende Gruppen-Body sammelt sie mit for await. Siehe withTaskGroup.
Einen ausführbaren Einstiegspunkt hinzufügen
Dieser letzte Block stellt alle Beispiel-Eingaben bereit und ruft die Batch-Funktion auf. Die explizite Zuweisung innerhalb von catch prüft außerdem den vorgesehenen API-Vertrag zur Compile-Zeit: Der abgefangene Wert muss ein CalculationError sein.
@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.")")
}
}
}
}
Führe das Programm im Paketverzeichnis aus:
swift run TypedCalculations
Die erwartete Programmausgabe ohne Build-Meldungen ist unten zu sehen. In der Rechercheumgebung war kein Swift-Compiler verfügbar, daher wurde dieses Programm anhand der abgerufenen API-Dokumentation überprüft, dort aber weder kompiliert noch ausgeführt.
Valid: [307200, 64000]
Multiple failures: Rectangle 1 needs positive dimensions.
Overflow: Rectangle 1 has too many pixels for Int.
Empty: []
Die gültigen Zahlen ergeben sich direkt aus den beispielhaften Eingaben: 640 mal 480 ist 307.200, und 320 mal 200 ist 64.000. Im Beispiel mit mehreren Fehlern gewinnt das erste Rechteck, weil die Ergebnisse in Eingabereihenfolge geprüft werden, auch wenn die zweite untergeordnete Aufgabe früher abgeschlossen wird.
Wie verhalten sich Abbruch und Teilergebnisse?
Abbruch erfolgt in Swift kooperativ. Eine Aufgabe muss den Abbruch erkennen und darauf reagieren; eine Abbruchanforderung unterbricht ihre Berechnung nicht automatisch. Strukturierte Task-Groups warten außerdem auf den Abschluss ihrer untergeordneten Aufgaben, bevor sie zurückkehren. Siehe das Kapitel Concurrency im Swift-Sprachleitfaden und Apples Dokumentation zu withTaskGroup.
Hier wird Task.isCancelled vor dem Übermitteln, innerhalb jeder Berechnung und nach dem Sammeln geprüft. Wenn ein Prüfpunkt einen Abbruch erkennt, stellt der Code ihn mit dem bestehenden Fall .cancelled dar. Das Auslesen des Booleschen Werts verhindert, dass ein weiterer Fehlertyp in diesen abgeschlossenen Vertrag eingeführt wird. Apple dokumentiert die Eigenschaft unter Task.isCancelled.
Die Prüfung nach dem Sammeln gibt einem erkannten Batch-Abbruch Vorrang vor Rechteckfehlern. Diese Prüfungen sind Prüfpunkte und keine atomare Garantie: Ein nach der letzten Prüfung angeforderter Abbruch wirkt sich möglicherweise nicht auf das zurückgegebene Ergebnis aus. Füge bei einer längeren Berechnung mit wiederholten Arbeitsschritten Prüfungen innerhalb dieser Arbeitsschritte hinzu, damit ein Abbruch früher erkannt werden kann.
Ein fehlgeschlagenes untergeordnetes Element löst in dieser Implementierung keinen Abbruch aus. Es gibt einen normalen Wert vom Typ Result.failure zurück, und der Sammler fährt fort. Wenn alle untergeordneten Aufgaben erfolgreich abgeschlossen werden, erhält der Aufrufer alle Zahlen. Wenn beim Entpacken ein Fehler auftritt, wirft die Funktion ihn, und ihr lokal zusammengestelltes, unvollständiges Array counts wird nie zurückgegeben.
Wenn die Desktop-Oberfläche stattdessen für jedes Rechteck einen Erfolgs- oder Fehlerstatus benötigt, ändere den öffentlichen Batch-Vertrag so, dass er die geordneten Ergebnisse zurückgibt. Verknüpfe jeden Fehler weiterhin mit seinem Index. Das wäre ein anderes, für den Leser sichtbares Verhalten als das hier implementierte Alles-oder-Fehler-Array.
Einen verlorenen Fehlertyp diagnostizieren, ohne Fehler zu verbergen
Wenn dein angepasster Code nicht mehr kompiliert, verfolge den Typ über jede Grenze hinweg in dieser Reihenfolge:
- Prüfe, ob die Berechnung
throws(CalculationError)deklariert. - Prüfe, ob der Wert des untergeordneten Elements
Result<Int, CalculationError>ist und nicht ein Result, dessen Fehlerany Errorist. - Behalte die explizite Typed-Throws-Annotation des Closures bei, falls die Inferenz den Fehlertyp erweitert.
- Verwende das nicht werfende
withTaskGroup, um diese Result-Werte zu übertragen. - Entpacke mit
get()innerhalb der typisierten Batch-Funktion. - Prüfe, ob ein neu hinzugefügter werfender Vorgang einen anderen Fehlertyp einführt.
Ein erzwungener Cast kann diesen Vertrag nicht beweisen. Ebenso würde das Verwerfen fehlgeschlagener Ergebnisse oder das Ersetzen durch null die Bedeutung der Berechnung ändern. Bewahre Fehler als Werte, bis deine gewählte Batch-Regel entscheidet, wie sie gemeldet werden.
Bei diesen winzigen Multiplikationen ist Nebenläufigkeit eine architektonische Demonstration; eine Leistungssteigerung wird nicht behauptet. Das nützliche Ergebnis ist ein präziser Vertrag: Erfolgreiche Zahlen behalten die Eingabereihenfolge, für mehrere Fehler gilt eine deterministische Auswahlregel, und Aufrufer erhalten ein konkretes Fehler-Enum, das sie direkt behandeln können.
Quellen
Typed throws — SE-0413 — Swift Project.
withThrowingTaskGroup(of:returning:isolation:body:) — Apple.
Result — Apple.
get() — Apple.
TaskGroup — Apple.
Getting Started — Swift.org.
Install Swift — macOS — Swift.org.
LocalizedError — Apple.
multipliedReportingOverflow(by:) — Apple.
Concurrency — The Swift Programming Language — Swift Project.
isCancelled — Apple.