3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#: override される可能性があるメソッドは、すべて Task を返すべし。

Last updated at Posted at 2025-06-26

表題のような宗派(常識?)があることを今更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 したくなった場合、その時点で voidXXX 型、もしくは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 ができず詰む可能性がある。

image.png

  • この際、メソッド名は慣例に従い 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> を返すべし」

この宗派の根本理念はこうです:

  1. await 可能にしなさい(後で await したくなるから)

  2. 実装側が非同期になる余地を与えなさい(OCP に従うため)

  3. 呼び出し元に制御を戻しなさい(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. デリゲート型(例: ActionFunc<T>)の登録先

  • ActionTask を返す 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 or Dispatcher.InvokeAsync を使う


🧠 結論:WPFの「public API」は Task と縁遠い(設計レベルで)

よって WPF においては:

🚫 「すべての interface/public method は Task を返すべし」教は通用しない
🙏 「UI層は Dispatcher の支配下にある。そなたに Task を許さぬ」by INotifyPropertyChanged


✨裏教典:「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君、それ一か月前に言ってよ~~" 案件のご報告でした。

おしまい

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?