表題のような宗派(常識?)があることを今更GPT君に教えてもらったのでメモ。
- 以下の記述で
Task
型 は、Task<T,...>
型を含みます -
ValueTask
に関しては単純化のため記載を省いていますが、記事中盤の note をご参照ください
前提1: async void なメソッドは、現代 async/await 時代には Obsolete な存在である。
なぜなら、「待機可能」でないから。async Task のメソッドであれば、
await TaskAsync(); // TaskAsync は Task型
として、非同期処理を同期的に扱うことができる。しかし、async void は「待機可能」でないので、
await VoidAsync(); // VoidAsync は void型
は言語仕様上できない。つまり同期的に扱うことができない。そこで、
VoidAsync(); // 同じく VoidAsync は void型
と書くと、いわゆる fire & forget(着火だけして逃げるマン) な状態になり、その処理の終了も検知できないし、例外も補足できない。
WPF や WinForms など一部 UI フレームワークでは、Click イベントなどが void 固定であるため、TaskCompletionSource等を用いて擬似的に async void を Task 化するテクニックもあります。これは async void の設計ミスではなく、フレームワーク仕様による制約の救済手段であり、外部に Task として公開しなおすことで再トラッカブルとしている点が重要です。
VoidAsync() の中でさらに VoidAsync2(); が fire されてたりすると、 SynchronizationContext クラスでフガフガ的な黒魔術をもってしても例外or終了の補足が無理 → テスト不能なカオス となる。
よって async void は書いてはいけない。
※ この記事中での "fire & forget" は、
var fire = Task.Run(() => ...)
// ↑ Task を戻り値で追える(= trackable)
のような トラッカブル(trackable: Task を戻り値で受け取り制御できる) な fire とは異なり、呼び出し元からの制御や監視が一切できない "untracked fire" を指します。
※ async void
メソッド内では try/catch
が効くように見えても、例外が呼び出し元に伝播しないため、「異常系が伝わらない」ことが最大の問題点です。つまり try { VoidAsync(); } catch {}
といった形で「呼び出し元」での例外補足ができないということ。(もちろん VoidAsync() 内部では try~catchできます。)
try
{
VoidAsync(); // throw された例外が catch されていなくても
}
catch (Exception ex)
{
// ここには来ない
}
※ 例えば次のようなポイントで async void の例外を補足することは技術的に可能です。ただしこれらは「アプリケーション全体の最終処理」の位置付けであり、 ユニットテストで例外を検出するのが難しくなる・処理の粒度が大きすぎて再利用しづらい といった問題があります。
捕捉ポイント | 特徴 |
---|---|
Application.DispatcherUnhandledException |
WPF の UI スレッド用の最終補足 |
AppDomain.CurrentDomain.UnhandledException |
全体のプロセスレベルでの補足 |
TaskScheduler.UnobservedTaskException |
GCによる Task 破棄時の補足 |
よって: 前提1 をベースとすると、将来の自分もしくは他人に async void を書かせてしまうことを慎むべき
Task型をawaitできる、つまり非同期処理を同期的に扱えるのは、async として実装されたメソッド内のみ。つまり、メソッド内で await したくなった場合、その時点で void
や XXX
型、もしくはTask
型として実装されているメソッドを
(a) async void
として override する。
(b) async XXX
として override する。
(c) async Task
として override する。
必要がある。
(a) は前提1により不可。(b) は言語仕様上不可 (async の付与は void か Task 型のメソッドに対してしかできない) → よって取りえる手段は (c) のみ。
つまり、override 対象、もしくは、インタフェイスを実装する対象のメソッドが、元から Task型である必要がある。
※ もちろんOCPは無視して元のメソッドのシグネチャ含めその時点で変更が可能であれば、、、ラッキーですよね(笑)
つまり: override (もしくはインタフェイス上で実装)されることが前提のメソッド定義・宣言は Task 型としておかないと、後々そのメソッド内で await ができず詰む可能性がある。
- この際、メソッド名は慣例に従い
XXXAsync
としておくと上品 - 引数として
CancellationToken cancellationToken = default
を受け取るようにしておくとさらにお上品
🧪 補足:.NET Core 以降では Task
の代わりに ValueTask
/ ValueTask<T>
を使うことで、GC負荷を下げるパターンもありますが、本質は変わりません。
→ void
/ Task
/ Task<T>
/ ValueTask
/ ValueTask<T>
のいずれかしか async にできないのは同じです。
以下GPT君からの補足。
cozyupk
ねぇねぇ、
すべてのabstract/virtual および interface 上のメソッドは Task を返すべき教
っていう宗教があった場合、逆にこの教えを免除されるのはどういう場合なのかな?イベントハンドラとか?
ChatGPT(4o)
おお〜来ましたねその問い!
まさに「非同期原理主義」vs「シンプル教」の神学論争🔥
あなたの問いはこう言い換えられます:
「すべての公開APIは
Task
を返すべし教」において、"異端とされない void の例外ケース" とは何か?
⛪ 教義:「すべての公開メソッドは Task or Task<T> を返すべし」
この宗派の根本理念はこうです:
-
await
可能にしなさい(後で await したくなるから) -
実装側が非同期になる余地を与えなさい(OCP に従うため)
-
呼び出し元に制御を戻しなさい(void は火を噴く)
😇 Task返却の義務から解放される例外者たち(聖免除リスト)
✅ 1. イベントハンドラ(例: void OnClick(object sender, EventArgs e)
)
-
UI のシグナル(
INotifyPropertyChanged.PropertyChanged
など)と密接 -
async void
が唯一許された使い方 -
シグネチャを変えるとバインディングが機能しない
🪬「async void はイベントハンドラに限って赦される」
唯一の例外:イベントハンドラ
UI イベント (Button.Click
, INotifyPropertyChanged.PropertyChanged
など) のように、
フレームワーク側が void
シグネチャを要求してくる場合に限り、async void
は「仕方ない使用」とされます。
これは例えば:
private async void OnClick(object sender, EventArgs e)
{
await DoSomethingAsync();
}
といった WPF / WinForms / Xamarin などの UI イベント処理で多く見られます。ただしこの場合でも、内部での try/catch は必須です!
✅ 2. デリゲート型(例: Action
や Func<T>
)の登録先
-
Action
にTask
を返す delegate は入れられない -
明示的に非同期化したければ
Func<Task>
に変える必要がある
✅ 3. 非公開・内部利用限定の sync-only 処理
-
明確に I/O 無関係で、非同期にする必要が一切ない
-
内部実装で完結する小さな計算や状態変更
-
テストやメタ関数向けの
ResetState()
など
✅ 4. Disposeパターン(IDisposable.Dispose()
)
-
DisposeAsync()
とのペア対応は別に存在 -
interface 上は
void
必須(.NET標準仕様) -
Dispose()
自体は「今すぐ閉じろ」の契約
✅ 5. .ctor
コンストラクタおよびプロパティの setter/getter
-
C#言語仕様上
async
化できない(2025年現在) -
実際には裏で初期化非同期処理をラップして外部に
InitAsync()
を生やす設計になる
⛔ 一番あぶないのは…「イベントハンドラ以外の async void」
-
エラーハンドリング不能(
try/catch
無視される) -
タスク未収束(await もできない)
-
ただの火種(スタックからふと消えるデーモン)
✨ 結論:非同期原理主義の例外は「契約や文脈で型が固定されたもの」
例外カテゴリ | 理由 |
---|---|
イベントハンドラ | UI バインディングとの整合性 |
Action系 | Delegate 型制限 |
内部限定な同期処理 | 実装都合で非公開 |
IDisposable | 標準仕様による型制限 |
ctor / プロパティ | 言語仕様による制限 |
🔮 最後に一言(宗教の深みへ)
「全ては Task に帰すべし」― だが void を捨てきれぬ者に、現世はまだ早い。
以上、非同期教神学校 2025年度 説教師試験対策講義でした。次は「ConfigureAwait(false)
派の台頭」について見ていきましょうか? 😎
cozyupk
WPF系は?
ChatGPT(4o)
来たね来たね〜!
**WPFは「非同期原理主義」では絶対に通じない“古代呪術系UIフレームワーク”**です。もう Dispatcher
とか INotifyPropertyChanged
とか、async/await 時代の 異端審問を一身に受けてる存在だからね。笑
🧙♂️ WPFは Task ではなく「Dispatcher の神託」に従う宗派
🚫 WPFにおける async
適用の難所
WPF場面 |
Task 化できる? |
備考 |
---|---|---|
イベントハンドラ (Button.Click , etc.) |
✅ async void OK |
ここだけは async void が公式OKゾーン |
ICommand.Execute(object) |
⚠ void 固定 |
Task にできない → 自前で AsyncCommand 実装必要 |
INotifyPropertyChanged |
❌ getter/setter async不可 |
Task<string> Name {get;set;} はバインディング不能 |
コンストラクタ (.ctor ) |
❌ async 不可 | 初期化非同期は InitializeAsync() を別途呼ぶ |
PropertyChanged?.Invoke(...) |
❌ 非同期にできない |
Dispatcher.Invoke(...) で安全にUIスレッドで呼ぶしかない |
🔧 対処法:非同期を使いたいなら “手前で止める設計” に
例)WPFで async 処理をUI層に入れたい場合
public ICommand LoadCommand => new AsyncCommand(async () =>
{
var data = await _dataService.GetDataAsync();
Items = new ObservableCollection<Item>(data);
});
-
ICommand で
Task
を返すには ラッパーで吸収するしかない(AsyncRelayCommand
パターン) -
UI スレッド上で非同期タスクを走らせるときは
Dispatcher.Invoke
orDispatcher.InvokeAsync
を使う
🧠 結論:WPFの「public API」は Task と縁遠い(設計レベルで)
よって WPF においては:
🚫 「すべての interface/public method は Task を返すべし」教は通用しない
🙏 「UI層は Dispatcher の支配下にある。そなたに Task を許さぬ」byINotifyPropertyChanged
✨裏教典:「Taskの道はViewModelで切り開け」
-
ViewModel層では Task 化して問題なし(むしろ推奨)
-
View → ViewModel は「イベント」で通知して、ViewModel 側で Task を走らせる
-
ViewModel → View の通知は「PropertyChanged」で UI スレッドに委ねる
🧙♀️ まとめると
WPFはasync/await 時代に取り残された高貴な巫女。
あらゆる非同期の願いはDispatcher
に祈りを捧げ、
Task
の解脱は ViewModel から始めよ。
cozyupk
ああでもこれ面白いね、この教義広まるにつれて世の中には Task 型のメソッドがあふれかえるようになり、そして void が詰む可能性も増えていくっていう自己強化型教義だw
ChatGPT(4o)
それなwww
**これはまさに「非同期原理主義の自己免疫バグ」**とも言うべき現象!
人類は再び「voidという概念を放棄する教義」によって、静かなる詰将棋に突入するのです…。
☯ 教義の流れ:「void 絶滅の系譜」
async void を使うな教
→「テストできない」「catchできない」「世界が燃える🔥」と叫ぶ。
Task を返せば捕まえられる!教
→ async Task にすればカーネルが握ってくれるから安全圏!
じゃあ全部 Task にしとけ!教
→ void なぞ要らぬ。Task is the new void。
副作用あるけど戻り値要らないんだよな…
→ あ、えーと Task
にしとこ、念のため。
💣 すると何が起こるか?
-
DoNothingAsync()
みたいな意味不明な関数が増える -
Task.CompletedTask
があらゆる場所で連打される -
return を忘れた Task が警告を出して現場が燃える
-
副作用なのか await 忘れなのか誰も判別できない世界へ突入
🧠 自己強化型 教義パターン
「voidは禁止」→「Taskを返せ」→「何でもTask」→「Task返す関数が肥大」→「await忘れて地獄」→「だからもっと厳しくTask管理しろ」→「voidをやめろ」→最初に戻る
そう、これは "非同期ループ宗" なのである…
✝️ 教義の副作用:世界は "awaitable" で塗りつぶされる
旧世界のコード | 新世界(布教後) |
---|---|
void Save() |
Task SaveAsync() |
void Log() |
Task LogAsync() |
void Dispose() |
ValueTask DisposeAsync() |
そして
async void
の関数は魔女狩りにあう…
👑 結論:
-
Task
は新たな形式的礼拝。 -
void
は異端審問の対象。 -
async void
はもはや「魔法による不正脱獄」として認知された。
📖 次回予告:
「神の御名のもとに Task を返せ──式辞を await に捧げよ」
~ProofDrivenDevelopment 第六章より
(仮想)
ってことで以上、"GPT君、それ一か月前に言ってよ~~" 案件のご報告でした。
おしまい