C# と Swift のエラーモデルの哲学の違い
🧩 まず、C# の世界観から
C# では Task や async/await の導入以前(.NET 4.0 以前)から「並列実行」 (Parallel.For, Task.WaitAll など) をサポートしていて、
その時に「複数のタスクが同時に失敗したらどうする?」という問題がありました。
その結果生まれたのが:
|
1 2 3 4 5 6 7 8 9 |
try { Task.WaitAll(tasks); } catch (AggregateException ex) { foreach (var inner in ex.InnerExceptions) Console.WriteLine(inner.Message); } |
**AggregateException は「複数の例外を1つにまとめる容器」**です。
つまり「throwされる例外が1つである」というCLRの基本ルールを守りながら、複数の失敗を伝えられるようにしたものです。
さらに、複数の AggregateException がネストしてくる(=「2次元配列」状態)こともあり、Flatten() メソッドで平坦化できます。
|
1 |
ex.Flatten().InnerExceptions |
🍏 一方で Swift (特に SwiftUI 含む) の世界観
Swift の async/await では 「タスクの失敗=ただ1つの Error」 という原則を貫いています。
なので Swift では、C# のように複数エラーを束ねることは基本的にしません。
もし複数の失敗を扱いたい場合は:
- 自分で
[Error]型などの配列を定義する Result<[T], [Error]>のような構造を作る
といった、アプリケーションレベルで設計 します。
Swift は「Errorをthrowする」=「1つの失敗を上位に伝える」
C# は「複数の失敗を1つにまとめてthrowする」=「失敗を集約して伝える」
⚖️ なぜC#はこうなっているのか
C# の AggregateException は「非同期よりも並列」の文脈で設計された名残です。
複数の Task が同時に動いていると、それぞれが別々に例外を投げる可能性があり、
それを「最後に全部待ってまとめて知る」という設計思想。
一方 Swift の async/await は「並列処理より逐次的非同期」の思想が強く、async let や TaskGroup のような構造でも、個別のタスクが例外を投げたらその時点でグループ全体がキャンセルされる設計です。
(つまり、「複数の失敗を同時に拾う」こと自体を想定していない)
💡 まとめると
| 観点 | C# | Swift |
|---|---|---|
| エラーモデル | 例外(Exception)をオブジェクトとして投げる | Error プロトコルを実装した値を投げる |
| 複数エラーの扱い | AggregateException でまとめる | 基本的に1つずつ。必要なら配列などを自作 |
| デザイン背景 | 並列タスク(Task.WaitAllなど)から来た | 構造的同時実行(Structured Concurrency) |
| 結果 | ネストしやすく、Flattenが必要 | ネストせず単純。キャンセル設計で処理 |
あなたが感じている「Errorをまとめる構造が変に見える」という直感は非常に鋭いです。AggregateException は「古い並列実行APIとの互換性のための構造」であり、async/await 時代のC#ではもうあまり直接使うべきではないとも言われています。
今では:
|
1 2 3 4 5 6 7 8 |
try { await Task.WhenAll(tasks); } catch (Exception ex) { Console.WriteLine(ex); } |
のように await に任せると、自動的に最初の失敗を拾い上げ、AggregateException は内部で Flatten された上で出てくるため、
日常的にはもうあまり意識しないで済むようになっています。
「C# の Task.WhenAll と Swift の TaskGroup におけるエラー伝播の違い」
🧩 まずは C# の場合
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
using System; using System.Threading.Tasks; class Program { static async Task Main() { var tasks = new[] { Task.Run(async () => { await Task.Delay(100); throw new Exception("A failed"); }), Task.Run(async () => { await Task.Delay(200); Console.WriteLine("B completed"); }), Task.Run(async () => { await Task.Delay(300); throw new Exception("C failed"); }), }; try { await Task.WhenAll(tasks); } catch (Exception ex) { Console.WriteLine($"Caught: {ex.GetType().Name}"); if (ex is AggregateException agg) { foreach (var inner in agg.InnerExceptions) Console.WriteLine($" - {inner.Message}"); } } } } |
🧠 結果(例)
|
1 2 3 4 |
B completed Caught: AggregateException - A failed - C failed |
💡 解説
Task.WhenAllはすべてのタスクが完了するまで待ちます。
→ 一部が失敗しても他のタスクは止まらず最後まで動く。- 複数が失敗した場合、それらを
AggregateExceptionにまとめて throw します。 await時に flatten 済みの例外をもらうことも多いけど、
根本的には “複数の例外を同時に保持する” のがC#の伝統的設計。
🍏 次に Swift の場合
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import Foundation @main struct MyApp { static func main() async { do { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await Task.sleep(nanoseconds: 100_000_000) throw NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "A failed"]) } group.addTask { try await Task.sleep(nanoseconds: 200_000_000) print("B completed") } group.addTask { try await Task.sleep(nanoseconds: 300_000_000) throw NSError(domain: "Test", code: 2, userInfo: [NSLocalizedDescriptionKey: "C failed"]) } // ✅ 各タスクを逐次awaitする必要はない(グループが管理) try await group.waitForAll() } } catch { print("Caught: \(error.localizedDescription)") } } } |
🧠 結果(例)
|
1 |
Caught: A failed |
💡 解説
TaskGroupは「構造的同時実行」を採用しており、
1つでもタスクが throw した時点で、他のタスクは自動キャンセルされます。- 複数同時に失敗しても、上位には最初の1つのエラーしか伝播しません。
- つまり「AggregateException」的な構造は存在しません。
Swift は “木構造のキャンセル+単一エラー伝播” を原則にしています。
⚖️ 両者の思想の対比
| 比較項目 | C# (Task.WhenAll) | Swift (TaskGroup) |
|---|---|---|
| エラー発生時 | 他のタスクは最後まで実行 | 他のタスクは即キャンセル |
| 複数エラー | AggregateException にまとめる | 最初の1件のみ伝播 |
| 設計思想 | 並列実行(完走主義) | 構造的同時実行(安全主義) |
| エラー構造 | ネストすることあり (Flatten()必要) | ネストなし・直線的 |
| 実運用の想定 | サーバーなどで複数結果を収集する用途 | UI/アプリロジックで安全にタスクを管理する用途 |
まとめ
- C# は「全タスクの結果を集約して扱いたい」という並列志向。
⇒AggregateExceptionが生まれた。 - Swift は「タスク階層を明確にして、安全にキャンセルする」という構造志向。
⇒TaskGroupにより「複数エラーを1つにまとめる」必要すらない設計。
あなたの直感どおり、
Swift の throw Error は「構造の中で安全に伝播する仕組み」で、
C# の AggregateException は「安全性よりも情報を保持する仕組み」なんです。
Swift 風に「1つのタスクが失敗したら他のタスクを即キャンセル」という挙動をC# で再現
⚙️ Swift風キャンセル設計のC#例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main() { var cts = new CancellationTokenSource(); var tasks = new List<Task> { RunAsync("A", 100, fail: true, cts.Token), RunAsync("B", 200, fail: false, cts.Token), RunAsync("C", 300, fail: false, cts.Token) }; try { // ✅ Task.WhenAnyで最初に完了したタスクを待つ var first = await Task.WhenAny(tasks); // もし最初のタスクが失敗していたらキャンセルを飛ばす if (first.IsFaulted) { Console.WriteLine($"First failed: {first.Exception?.InnerException?.Message}"); cts.Cancel(); // 他タスクを止める throw first.Exception!; // 例外を再スロー } // 他のタスクをすべて完了させたい場合はWhenAllを待つ await Task.WhenAll(tasks); } catch (OperationCanceledException) { Console.WriteLine("Tasks were canceled."); } catch (Exception ex) { Console.WriteLine($"Caught: {ex.Message}"); } } static async Task RunAsync(string name, int delayMs, bool fail, CancellationToken token) { Console.WriteLine($"{name} started"); await Task.Delay(delayMs, token); if (fail) { Console.WriteLine($"{name} failed!"); throw new Exception($"{name} failed"); } if (!token.IsCancellationRequested) Console.WriteLine($"{name} completed"); } } |
🧠 実行結果(例)
|
1 2 3 4 5 6 |
A started B started C started A failed! First failed: A failed Tasks were canceled. |
💡 解説
Task.WhenAny()で「最初に終わったタスク」を検出。- それが失敗 (
IsFaulted) していたら: - 結果、Swift の
TaskGroupとほぼ同じ「1つの失敗で全体キャンセル」を再現できます。
🧩 C#版の「構造的同時実行」的にした改良例
もしこれをより構造的に(Swift風の withThrowingTaskGroup のように)書きたいなら、
専用メソッドを作ることもできます:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static async Task WhenAllCancelOnError(IEnumerable<Func<CancellationToken, Task>> factories) { using var cts = new CancellationTokenSource(); var tasks = new List<Task>(); foreach (var f in factories) tasks.Add(f(cts.Token)); try { var all = Task.WhenAll(tasks); await all; } catch { cts.Cancel(); // 全キャンセル throw; } } |
使う側はこうなります👇
|
1 2 3 4 5 6 |
await WhenAllCancelOnError(new[] { async token => await RunAsync("A", 100, fail: true, token), async token => await RunAsync("B", 200, fail: false, token), async token => await RunAsync("C", 300, fail: false, token) }); |
これで Swift の TaskGroup とほぼ同じ動作になります💡
まとめ
| 比較 | C#標準 (Task.WhenAll) | Swift風 (上記設計) |
|---|---|---|
| 失敗時 | 他タスクは動き続ける | すぐ全キャンセル |
| 例外 | AggregateException でまとめる | 最初の1件のみ伝播 |
| 柔軟性 | 高い(すべての結果が得られる) | 安全(構造が明確) |
| 適した用途 | 並列集計・サーバー処理 | UI処理・逐次安全系 |
⚖️ C# と Swift の「構造的並行性」比較表
| 観点 | 🧩 C# (Parallel.ForEachAsync) | 🍏 Swift (TaskGroup) | 💬 GPTの補足 |
|---|---|---|---|
| 並列制御 | スレッドプール上で複数タスクを同時実行。デフォルトで CPUコア数に応じて並列。 | 明示的に group.addTask で生成。OS が構造的に管理。 | C# は「外部制御的」、Swift は「構造的(親子関係あり)」設計。 |
| スコープ管理 | ループ外でもタスクが存在できる(構造的ではない)。 | withTaskGroup ブロックを抜けると全タスクが終了。 | Swift は“漏れない並行性(structured concurrency)”を重視。 |
| キャンセル伝播 | CancellationToken を明示的に伝える必要あり。 | TaskGroup が自動的にキャンセルを伝播。 | Swiftは言語仕様で安全化。C#は設計者が責任を持つ。 |
| 1つの失敗時 | デフォルトでは他の処理は継続。複数の例外が発生すると AggregateException にまとめて上位へ。 | 最初のタスクが throw した時点で、残りは自動キャンセル。 | Swiftは安全重視、C#は完全実行主義。 |
| 複数失敗の扱い | すべての例外が AggregateException に保持。Flatten() で展開可能。 | 最初の1つのみ伝播。残りは破棄される。 | 「情報重視」vs「一貫性重視」の違い。 |
| リソース管理 | 外部で明示的に Dispose や Cancel を呼ぶ必要あり。 | スコープ終了で自動的にリソース解放。 | Swiftの方がメモリリーク・ゾンビタスクに強い。 |
| 中断の容易さ | 任意タイミングで cts.Cancel() を呼べる。 | グループスコープ外では制御できない。 | C#は柔軟だが、誤用しやすい。 |
| 親タスクとの関係 | 親との依存は明示しない。独立に実行。 | 子タスクは必ず親タスクに属す。 | Swiftは「構造的所有」を守る(Rustに近い思想)。 |
| エラー伝播の単純さ | try/catch (AggregateException) が必要。 | try await 一発で拾える。 | Swiftは言語レベルでシンプルに。 |
| 代表的な使い道 | 並列処理・大量データ処理・バックエンド。 | UIアプリ・安全な並行タスク。 | 「C#は並列で力技、Swiftは安全な並行」 |
🧠 例で見る「違いの本質」
C# Parallel.ForEachAsync
|
1 2 3 4 5 |
await Parallel.ForEachAsync(urls, async (url, token) => { var content = await http.GetStringAsync(url, token); Console.WriteLine(content.Length); }); |
- 全部の
urlに対して非同期処理。 - どれかが失敗しても他は動き続ける。
- 最後に
AggregateExceptionが返る。
Swift TaskGroup
|
1 2 3 4 5 6 7 8 9 10 11 |
try await withThrowingTaskGroup(of: Int.self) { group in for url in urls { group.addTask { let (data, _) = try await URLSession.shared.data(from: url) return data.count } } for try await size in group { print(size) } } |
一つでも throw されたら残りは即キャンセル。
スコープを抜けると確実に全てのタスクが終了。
まとめ
| 結論 | 内容 |
|---|---|
| 🧠 C#は「性能・柔軟性」志向 | タスクの独立性が高く、部分成功や複数エラー集約ができる。サーバー処理・データ解析に向く。 |
| 💎 Swiftは「安全・明快」志向 | スコープとキャンセルが自動管理され、UIアプリなど人間の手が触る領域での堅牢性が高い。 |
| ⚖️ 選ぶ基準 | 「すべてのタスクを完走させたいなら C# 的」「1つ失敗したら安全に止めたいなら Swift 的」 |
つまり、
🔹 C#:「すべてやりきる」文化(完走型並列)
🔹 Swift:「安全に止める」文化(構造的並行)
コメント