2036年の日常を想像してみた ─ 変わったこと、変わらなかったこと
AIが当たり前になった10年後、それでも人間にしかできないこと
AIの進化が凄くてワクワクする反面、最近ちょっと怖い。
10年後、自分の仕事ってどうなってるんだろう。 別にAI否定派とかそういう話じゃなくて、純粋に、自分が今やっていることの価値が10年後にどうなるか、よくわからないというか。 たぶん同じことを考えているエンジニアは多いんじゃないかな?
なので、2036年の日常を少し想像してみました。想像しながら、自分なりに考えたことを書き留めておきます。
朝 ─ 「最適化された一日」がもう始まっている
目が覚めたら、今日のスケジュールがもう調整されているーーー
昨夜どれだけ寝れたか、今日の天気、午後の会議の相手の予定。「この順番でやると一番うまくいく」という一日が勝手に組まれている。 会議の準備資料は寝てる間にできあがっていて、コーヒーを飲みながら昨日の議事録を開いたら3行にまとまっている。決まったこととまだ決まってないことが色分けされていて。
誰も作ってない。でも誰かが作ったより、たぶん良い。そういえば、昔は人が書いていたんだよな。。。とか懐かしむ。
バスが2つ前の停留所を発ったという通知が届いた。さて、出かけるとしよう。
仕事 ─ 「作ること」じゃなくて「決めること」が仕事になった
「これ作っといて」と話しかけると、数分後に動くものができている。
エンジニアの仕事が「コードを書くこと」じゃなくなったのはいつからだろう。今では、「何を作るべきか決めること」が本業だ。 コードを一行も書かない開発者が普通にいるし、それで普通に活躍している。
じゃあ何をやってるかというと、「何を作るか」「なぜ作るか」「誰が使うか」を考えること。 AIが出してきたものを見て「これ合ってるか」を判断すること。 「この設計、3ヶ月後に絶対詰まるな」って気づくこと。
「楽になった」というより「仕事の種類が変わった」という感じで、頭の使い方が全然違う。
コードを書かなくなっても「読める人」と「読めない人」の差はけっこう残っていて、 それどころか読む力が判断の質に直接効いてくる。そこだけは手放さない方がいい、と自分も思っている。
それでもAIは優秀なので自分では考えつかないソースコードを書いてくる。 ・・・でも、欲しいのはこれじゃないんだよな、、、と修正プロンプトを入力する日々。
生活 ─ 家電が先に教えてくれる
「そろそろ買い替え時です」 「コンプレッサーがあと3ヶ月くらいで限界です」
突然壊れて「なんで今日」ってなる体験がだいぶ減っている。これは素直に助かる。
医療も変わっていて、病院に行く前にだいたい診断がついている。 スマートウォッチがずっと測っていて、「この症状の組み合わせは循環器科の方がよさそう」みたいな示唆が出てくる。
「先生、AIがこう言ってたんですけど」って患者が診察室に入ってくるのが普通の光景になっている。 お医者さんとしてはどんな気持ちなんだろう?
教育 ─ 同じクラスで、みんな違う授業を受けている
その子の理解するスピード、どこで詰まるか、どういう説明が刺さるか。 そういうのを学習した専属AIが最適なペースで教えてくれる。 算数が苦手な子には料理のレシピで分数を説明して、歴史が頭に入らない子にはゲームのストーリー仕立てで。
「クラス全員が同じ教科書を同じペースで読む」って、昭和の話になっている。
でも一方で「この子、本当に理解してるのか、AIのペースに乗っかってるだけじゃないか」という悩みが、 親の新しいあるあるになっている。テストで点が取れても何かが違う気がする、みたいな。 自分も子どもがいるので、これは他人事じゃない。どう解決されるのか、まだよくわからない。
ここまで書いてきて、なんとなく便利で豊かな未来の話に見えるかもしれないけど、全部がそうじゃないと思いますし、暗い話も出てきます。
「考えるのが苦手なエンジニア」はどうなったか
「言われたものを作る」のが得意で、それでずっとキャリアを積んできた人が、 「何を作るか決めて」という仕事に放り込まれたとき、かなりしんどくなる。
コードは書ける。でも「なぜ作るか」は昔から考えてこなかった。 そういう人の居場所が、静かに無くなってきている。
個人の話だけじゃなくて、会社単位でも同じことが起きている。
SIerは、ざっくり言うと「人をたくさん投入してシステムを作る」ビジネスモデルじゃないですか。 人月で売る、というやつ。でもAIがコードをガンガン書けるようになったとき、「人をたくさん投入する」こと自体の価値が下がっていく。
コードを書く人が100人必要だったところが10人でできるようになったとして、 でも売上は10分の1になっていい、とはならない。でも実際にはそうなっていく。
適応できたSIerは「作る会社」から「何を作るか考える会社」に変わっている。 要件を整理して、AIに渡して、出てきたものを品質チェックして、客先と調整する。
でもそれって今までと全然違う仕事で、何百人もエンジニアを抱えていた会社が「考える人」「判断する人」中心の組織に変わるって、相当しんどい転換だ。
AIリテラシーで広がる格差
個人も会社も、という話をしてきたけど、社会全体で見ても格差の形が変わっている。
「お金がある・ない」はもちろん残っている。でもそこに「AIを使いこなせる・使いこなせない」という軸が加わっていて、 これが思ったよりしんどい。 使いこなせる人は加速し続けて、使いこなせない人は何が起きているかすらわからないまま取り残されていく。
1990年代後半のインターネット普及期に似てるな、と感じている。 あのとき、慣れた人と慣れていない人の間に気づいたら巨大な溝ができていた。 自分はちょうどあの時代にプログラムを覚え始めた世代なので、あの溝がどれだけ静かに広がっていったか、なんとなく覚えている。
「使えない人が悪い」という話をしたいわけじゃない。でも、テクノロジーの変化は待ってくれない。
自分の仕事は、2036年どうなっているんだろう
エンジニアだけの話じゃないと思っている。
医療、教育、法律、会計、デザイン、営業。 どの仕事にも「AIに一部を任せたとき、残るのは何?」という問いがついてくる。
「AIに奪われる」という言い方をよく聞くけど、自分は少し違う解釈をしています。 「AIが得意なことはAIがやって、人間は人間にしかできないことに集中せざるを得なくなる」という感じの方が近い。 奪われるというより、選び直される、というか。取捨選択が変わってくる。
「自分にしかできないことって何?」という問いを今から考えておく価値があると思っている。 答えが出なくていいんだ。出なくて当然だし。 でも問いを持っているかどうかで、10年後の見え方がかなり変わってくる気がする。
自分もまだ答えは出ていない。でもこれを考え続けることが、今できる一番大事なことなのかな、と思っている。
10年は、思ったより早く来ます。
Windows 10サポート終了後でも古いPCをWindows 11にする方法【2026年最新・自己責任】
この記事は2021年に書いた記事を2026年向けに全面リライトしたものです。
元記事からの主な変更点:①Windows 10のサポートがすでに終了、②手順を一般的なPCに汎用化、③新たなリスク(透かし・SSE4.2要件)を追記 shin21.hatenablog.com
- まず確認:あなたのPCは今どんな状況?
- 【重要】2026年時点でのWindows 10の状況
- Windows 11の公式システム要件おさらい
- 「要件が緩和された」という情報はデマ
- 2026年時点での新しいリスク2つ
- 実際の手順【25H2対応・2026年版】
- インストール後の注意点
- 非対応PCの「現実的な選択肢」まとめ
- さいごに
まず確認:あなたのPCは今どんな状況?
この記事を読んでいるということは、おそらくこんな状況ではないでしょうか。
- 「PC正常性チェック」を実行したら「このPCはWindows 11の最小要件を満たしていません」と表示された
- Windows 10のサポートが終わったことは知っているが、PCを買い替えるほどの予算がない
- 性能的にはまだ十分使えるのに、CPUの世代が古いだけで弾かれるのが腑に落ちない
気持ちはよく分かります。私もかつてSurface Pro5を使用していて同じ思いをしました。
この記事では、2026年時点での実態と手順を正確にお伝えします。
【重要】2026年時点でのWindows 10の状況
2021年にこの記事を最初に書いたとき、「試してダメならWindows 10に戻せばいい」という逃げ道がありました。しかし今は状況が変わっています。
Windows 10のサポートは2025年10月14日に終了しています。
サポート終了後はセキュリティ更新プログラムもバグ修正も提供されなくなっているため、「Windows 10に戻す」という選択肢は技術的には可能でも、セキュリティ上は危険な選択になりました。
ただし、MicrosoftはWindows 10 Home/Proユーザーを対象に、拡張セキュリティ更新プログラム(ESU)を2026年10月13日まで無料提供しています。まだ何もしていない方はこちらへの登録を先に検討してください。
Windows 11の公式システム要件おさらい

公式のインストールに必要なシステム要件は下表の通りです。
| 項目 | 要件 |
|---|---|
| CPU | Intel第8世代以降 / AMD Ryzen 2000以降(64bit・1GHz以上・2コア以上) |
| RAM | 4GB以上 |
| ストレージ | 64GB以上 |
| ファームウェア | UEFI + セキュアブート対応 |
| TPM | TPM 2.0 |
| GPU | DirectX 12対応 |
| ディスプレイ | 720p以上・9インチ以上 |
引っかかりやすい2大ポイントは「CPUの世代」と「TPM 2.0」です。
Intelの第7世代(Kaby Lake)以前、AMDのRyzen 1000シリーズ以前は、TPMの有無にかかわらず対象外とされています。「TPM 2.0はあるのに弾かれる」というケースはCPU世代の問題です。
「要件が緩和された」という情報はデマ
2024〜2025年にかけて、SNSやYouTubeで「もう非対応PCでも普通にインストールできる」「Microsoftが水面下で要件を緩和した」という情報が拡散しました。
これは事実ではありません。
2025年10月時点でMicrosoftが公式にシステム要件を緩和した事実は確認されていません。インストールに成功したように見えるケースは、インストーラーのバグ・ハードウェア判定のグレーゾーン・非公式ツールの使用によるものです。
2026年時点での新しいリスク2つ
2021年当時と比べて、非対応PCへのインストールリスクが増えています。
リスク①:デスクトップに透かし(ウォーターマーク)が表示される
非対応ハードウェアにWindows 11をインストールすると、デスクトップ右下に
「システム要件を満たしていません」 という透かしが常時表示されます。
これはMicrosoftの公式仕様です。レジストリ操作で消すことはできますが、あくまで表示を隠すだけで状況は変わりません。
リスク②:Windows 11 24H2以降はSSE4.2命令セットが必須
Windows 11の24H2アップデート(2024年秋)以降、SSE4.2命令セットへの対応が必須になりました。Intel Core 2世代など非常に古いCPUではこの要件を満たせず、24H2以降のバージョンはインストールできません。
目安として:Intel Core iシリーズ第1世代(Nehalem)以降であればSSE4.2に対応しています。
Core 2世代(2006〜2009年頃)は対象外です。
実際の手順【25H2対応・2026年版】
ここから先は自己責任での作業です。万一のためにあらかじめデータのバックアップを取ってから進めてください。
ステップ1:自分のPCがどこで弾かれているか確認する
まずMicrosoftの「PC正常性チェックアプリ」をインストールして何が原因かを確認します。

- TPM 2.0が原因の場合 → BIOSでTPMを有効化できる場合があります(特にIntel第6〜7世代)
- CPUの世代が原因の場合 → 以下の手順(レジストリ回避)が必要です
BIOSでのTPM有効化だけで要件を満たせる場合は、それが最もリスクの低い方法です。
ステップ2:ISOファイルをダウンロードする
MicrosoftのWindows 11ダウンロードページから 「x64デバイス用Windows 11ディスクイメージ(ISO)をダウンロードする」 を選択してISOを入手します。
⚠️ 「Windows 11インストールアシスタント」はブロックされます。ISOからのインストールを使ってください。
ステップ3:レジストリキーを設定する(CPU世代・TPM回避)
コマンドプロンプトを管理者として実行し、以下のコマンドを入力してEnterを押します。
reg add HKEY_LOCAL_MACHINE\SYSTEM\Setup\MoSetup /v AllowUpgradesWithUnsupportedTPMOrCPU /t REG_DWORD /d 1 /f
これで2021年記事で紹介した「MoSetupキーを手動作成してREG_DWORDを設定する」操作がコマンド一発で完了します。
成功すると 操作は正常に終了しました。 と表示されます。
ステップ4:ISOをマウントしてインストールを開始する
ダウンロードしたISOファイルをダブルクリックして仮想ドライブとしてマウントし、setup.exe を実行します。
インストールの途中で 「このPCはWindows 11の最小システム要件を満たしていません」 という警告画面が出ますが、「承諾する」を押して続行します。
インストールには30〜60分程度かかります。完了まで電源を切らないでください。
インストール後の注意点
Windowsアップデートは届く?
非対応PCでもWindows Updateは届きます。セキュリティ更新プログラムは受け取れますが、将来的に制限が強化される可能性はゼロではありません。
ロールバック(Windows 10に戻す)について
Windows 11へのアップグレード後10日以内であれば、設定 → システム → 回復 → 「前のバージョンに戻す」から元に戻せます。ただし前述の通り、戻り先のWindows 10はすでにサポート終了済みです。
非対応PCの「現実的な選択肢」まとめ
最後に、このページを読んでいる方が検討すべき選択肢を整理します。
| 選択肢 | メリット | デメリット |
|---|---|---|
| 本記事の手順でWindows 11をインストール | 費用ゼロ・既存環境を維持 | 自己責任・将来的なサポート不確実 |
| Windows 10 ESUに登録して様子を見る | 2026年10月まで無料延命 | 2026年10月以降は詰む |
| Linuxに乗り換える(Ubuntu・Zorin OS等) | 古いPCが快適に動く・無料 | 使い慣れるまで時間がかかる |
| 新しいPCを購入する | 最も確実・長期的に安心 | 費用がかかる |
古いPCを使い続けること自体は悪いことではありませんが、特にインターネットバンキングやオンライン決済に使うPCについては、セキュリティリスクを真剣に検討してください。
さいごに
2021年に初めてこの記事を書いたとき、「Microsoftよ、なぜ使えるのに弾くんだ」という憤りがありました。4年経った今も気持ちは変わりませんが、セキュリティの観点ではTPM 2.0やセキュアブートが重要な役割を果たすことも理解しています。
古いPCを大切に使いたい気持ちはよく分かります。本記事の手順が参考になれば幸いです。ただ、インストールができたとしてもサポート外であることに変わりはありません。リスクを理解した上で判断してください。
【.NET 8対応】C# 非同期プログラミング完全ガイド2026 ― async/await・Task.Run・ConfigureAwait・CancellationTokenをビギナーからの落とし穴まで一気に解説
async/await の「なぜ」がわかると、非同期コードはもう怖くない

C# で非同期コードを書いていると、こんな経験はないでしょうか。
async/awaitを付けたら動いた。なぜ動くのかはよくわからないTask.Runを使えばとりあえず「非同期っぽくなる」と思っている.Resultや.Wait()を使ったら画面がフリーズしたが、原因がわからなかったConfigureAwait(false)をコピペしているが、何をしているのか説明できない
これらはすべて、非同期の仕組みを「点」で理解しているが「線」でつながっていない状態から来ています。
本記事では、C# 非同期プログラミングの核心を 2026年現在の .NET 8 環境 に合わせて体系的に解説します。コードの動作原理から、現場でよく踏まれる地雷まで、一本の記事で完結する「地図」を提供することを目標にしました。
この記事で扱うこと
- 非同期の仕組みとスレッドの関係
- 非async関数からasync関数を呼ぶときの正しい対処(ビギナーが最初に詰まる壁)
Task.Runの正しい使いどころと誤用パターン- デッドロックの発生メカニズムと根本的な解決策
ConfigureAwait(false)についての正しい理解(よくある誤解を訂正する)CancellationTokenによるキャンセル処理TaskvsValueTaskの使い分けTask.WhenAllによる並列実行async voidの危険性
- async/await の「なぜ」がわかると、非同期コードはもう怖くない
- 1. 非同期とスレッド ― 基礎を正しく理解する
- 2. 非同期でない関数から async 関数を呼ぶとき ― ビギナーが最初に詰まる壁
- 3. Task.Run の2つの顔を整理する
- 4. デッドロック ― 「なぜフリーズするのか」を理解する
- 5. ConfigureAwait(false) の正しい理解
- 6. CancellationToken ― 正しいキャンセル処理
- 7. Task vs ValueTask
- 8. Task.WhenAll / Task.WhenAny ― 複数タスクの並列実行
- 9. async void の危険性
- 10. まとめ ― 2026年の非同期ベストプラクティス早見表
1. 非同期とスレッド ― 基礎を正しく理解する
「非同期 = マルチスレッド」は誤りである
非同期プログラミングを学び始めた人が最初に持ちやすい誤解があります。それは 「非同期にするとバックグラウンドスレッドで実行される」 というものです。
これは半分正しくて、半分間違いです。
非同期の本質は「待っている間、スレッドを解放する」ことにあります。
たとえばデータベースへのクエリを発行する場面を考えてみましょう。
// 同期コード(ブロッキング) var users = dbContext.Users.ToList(); // DBの応答まで、このスレッドは何もできない // 非同期コード(ノンブロッキング) var users = await dbContext.Users.ToListAsync(); // DBの応答を待つ間、スレッドは他の仕事ができる
同期コードでは、DBが応答するまでスレッドは何もせずに待ち続けます。これが Thread.Sleep と本質的に同じ問題です。
await を使った非同期コードでは、DBへリクエストを投げた後にスレッドを解放し、応答が来たときに処理を再開します。UIアプリケーションであれば、その間もボタンのクリックなどに反応できます。
I/OバウンドとCPUバウンドを区別する
非同期処理を正しく使うために、最も大切な概念の一つが I/Oバウンド と CPUバウンド の区別です。この判断を誤ると、後述する Task.Run の使い方がすべて狂います。
| 種類 | 内容 | 例 |
|---|---|---|
| I/Oバウンド | 外部の応答を待つ処理 | DB クエリ、HTTP リクエスト、ファイル読み書き |
| CPUバウンド | 計算そのものが重い処理 | 画像処理、データ集計、暗号化 |
正しい対処方法はそれぞれ異なります。I/Oは await に任せ、CPUバウンドは別スレッドに逃がします。具体的な使い方は3章でまとめて解説します。
コンパイラがステートマシンに変換している
async/await はシンタックスシュガーの一つです。コンパイル時に、await を含むメソッドはステートマシンクラスに変換されます。
📖 用語補足:シンタックスシュガー(Syntactic Sugar) 「糖衣構文」とも呼びます。複雑な処理を、人間が読み書きしやすい簡潔な書き方で表現できるようにした構文のことです。内部では別のコードに変換されていますが、書く側はそれを意識しなくてよいのが特徴です。C# では
async/awaitのほか、foreach・プロパティ・usingなども代表的なシンタックスシュガーです。📖 用語補足:ステートマシン(State Machine) 「状態機械」とも呼びます。「今どの状態にあるか」を管理しながら動くプログラムの構造のことです。async メソッドの場合、「まだ実行前」「最初の await で中断中」「2番目の await で中断中」「完了」といった状態を管理することで、処理の中断・再開を実現しています。
// 書いたコード
public async Task<string> GetDataAsync()
{
var result = await client.GetStringAsync(url);
return result.ToUpper();
}
コンパイラはこれを、「どこまで処理したか」を管理するステートマシンに変換します。await の地点で処理を中断・再開できるのはこの仕組みのおかげです。
💡 確認ツール:SharpLab に非同期コードを貼り付けて「IL / C# (Lowered)」モードで表示すると、コンパイラが生成したステートマシンの実体を確認できます。
2. 非同期でない関数から async 関数を呼ぶとき ― ビギナーが最初に詰まる壁
「async じゃない関数で await を使いたい」問題
非同期プログラミングを学び始めたビギナーが最初に壁にぶつかる場面のひとつが、async キーワードのない既存メソッドの中で、async な別のメソッドを呼び出したいときです。
// 既存のメソッド。async になっていない
public void LoadData()
{
var result = FetchDataAsync().Result; // とりあえず .Result を付けてみた…
Console.WriteLine(result);
}
public async Task<string> FetchDataAsync()
{
await Task.Delay(500);
return "データ取得完了";
}
「await を使いたいけど、このメソッドは async にしていない。どうしたら…」と悩んで .Result や .Wait() で強引に同期的に待つ、というのがよくある流れです。しかし前章で解説したとおり、これはデッドロックの原因になります。
なぜ async は「感染」するのか
await は async メソッドの中でしか使えません。だから await を使いたければ、そのメソッドも async にする流れになります。するとそのメソッドを呼ぶ側も await したくなり、さらに上のメソッドも async にする必要が出てきます。
これが「async は感染する」と言われる理由です。一箇所だけ非同期にすることはできず、呼び出し階層を上まで全部 async にする(= async all the way down)のが正しいアプローチです。
// ✅ 呼び出し元も async にしていく
public async Task LoadDataAsync() // async Task に変える
{
var result = await FetchDataAsync(); // await が使える
Console.WriteLine(result);
}
どうしても async にできない場合の落とし所
完璧な解決策はありませんが、状況別に現実的な対処を示します。
パターン①:コンソールアプリ・Main メソッド
.NET 7以降は Main 自体を async Task にできます(async Main)。これが最もシンプルな解決策です。
// ✅ .NET 7以降:async Main が使える
static async Task Main(string[] args)
{
var result = await FetchDataAsync();
Console.WriteLine(result);
}
パターン②:WPF・WinForms のイベントハンドラ
イベントハンドラは async void にできます。これが唯一 async void が許容される場面です。
⚠️ なぜイベントハンドラ以外で async void にしてはいけないのか
async voidのメソッドで例外が発生すると、呼び出し元でキャッチできません。// ❌ async void は例外が呼び出し元に届かない async void LoadAsync() { await Task.Delay(100); throw new Exception("エラー!"); // ← これが消える } // 呼び出し側でキャッチしようとしても… try { LoadAsync(); // ← await できないので例外を受け取れない } catch (Exception ex) { // ここには来ない。例外はアプリ全体の未処理例外ハンドラに飛ぶ }
async Taskにすればawaitできるので例外が呼び出し元に伝わります。イベントハンドラはvoidシグネチャが決まっているため仕方なくasync voidになりますが、それ以外の場面で使うとエラーが音もなく消えてデバッグ不能になりえます。
// ✅ イベントハンドラは async void にする(これは OK)
private async void Button_Click(object sender, EventArgs e)
{
var result = await FetchDataAsync();
label.Text = result;
}
パターン③:結果を待たなくていい処理(Fire-and-forget)
ログ送信など「呼び出したら結果は気にしない」処理は、Task.Run を使って完全にバックグラウンドに切り離す方法が使えます。このパターンの詳細と注意点は次章(3章)でまとめて解説します。
ビギナーへの結論:「感染させる」のが正解
まとめると、ビギナーが取るべき判断順序はこうです。
① まず呼び出し元のメソッドも async Task にできないか検討する ← これが原則
↓ どうしても無理なら
② イベントハンドラなら async void にする
↓ 結果を待たなくてよい処理なら
③ Task.Run で切り離す(詳細は3章)
「async は感染する」を嫌がって .Result に逃げるのが最もリスクが高い選択です。
3. Task.Run の2つの顔を整理する
Task.Run は記事のここまでで何度か顔を出してきました。実はこのメソッド、目的が異なる2つの場面で使われるため混乱しやすいポイントです。最初に整理してから説明します。
| 用途 | 目的 | 典型的な場面 |
|---|---|---|
| ① CPUバウンド処理の切り離し | 重い計算でUIをフリーズさせない | WPF/WinForms で画像処理・データ集計 |
| ② Fire-and-forget(応急処置) | asyncにできない文脈から切り離す | ログ送信など結果を待たない処理 |
用途①:CPUバウンド処理をUIスレッドから切り離す
WPF や WinForms のような UIアプリケーションで、重い計算処理を実行するとUIがフリーズします。
// ❌ UIスレッドで重い処理を実行するとフリーズする
private async void Button_Click(object sender, EventArgs e)
{
var result = HeavyCalculation(largeData); // ボタンを押した瞬間にUIが固まる
label.Text = result.ToString();
}
// ✅ Task.Run でスレッドプールに逃がす
private async void Button_Click(object sender, EventArgs e)
{
var result = await Task.Run(() => HeavyCalculation(largeData));
label.Text = result.ToString(); // await 後なのでUIスレッドで安全に更新できる
}
Task.Run で処理をスレッドプールに投げ、await でその完了を待ちます。await の後は元のUIスレッドに戻るため、コントロールの更新も安全に行えます。
💡 I/Oバウンド処理に Task.Run は不要
Task.Runが活きるのはCPUバウンドな処理だけです。DB・HTTP・ファイルといったI/O処理は「待つだけ」でスレッドを占有していないため、Task.Runで包んでも無駄なスレッドを消費するだけです。// ❌ I/O処理を Task.Run で包む必要はない var users = await Task.Run(() => dbContext.Users.ToListAsync()); // ✅ I/O処理はそのまま await するだけ var users = await dbContext.Users.ToListAsync();
用途②:Fire-and-forget(応急処置)
「結果を待たなくていい処理」を、async にできない文脈から呼ぶ場合の応急処置です。前章のパターン③に相当します。
// ⚠️ Fire-and-forget パターン(例外の握りつぶしに注意)
public void SendLog(string message)
{
_ = Task.Run(async () =>
{
try
{
await LoggingService.SendAsync(message);
}
catch (Exception ex)
{
// 必ず例外をキャッチする。しないとエラーが完全に消える
Console.Error.WriteLine($"ログ送信失敗: {ex.Message}");
}
});
}
_ = は「この Task の結果を意図的に無視する」という明示的な書き方です。単に Task.Run(...) と書くとコンパイラ警告が出ます。
⚠️ 用途②はあくまで応急処置 呼び出し元が
asyncにできるなら、素直にawaitで待つのが正解です。Fire-and-forget は「本当に結果がいらない」かつ「async に感染させられない」限定の選択肢として使いましょう。
よくある誤用:ASP.NET Core で Task.Run を多用する
ASP.NET Core はリクエストを最初からスレッドプールのスレッドで処理しています。そこでさらに Task.Run を使うと、スレッドプールの消費が増えてスループットが悪化します。
// ❌ ASP.NET Core の Controller で Task.Run は基本不要 var users = await Task.Run(() => _service.GetUsersAsync()); // ✅ そのまま await する var users = await _service.GetUsersAsync();
Task.Run を使う場面のまとめ
| 場面 | Task.Run | 理由 |
|---|---|---|
| WPF/WinForms で重い計算をUIから切り離す | ✅ 用途① | CPUバウンド処理でUIフリーズを防ぐ |
| コンソールアプリでCPU重い処理を並列化 | ✅ 用途① | CPUバウンド処理 |
| 結果を待たないFire-and-forget処理 | ✅ 用途② | 非async文脈からの応急処置 |
| DB/HTTP/ファイルのI/O処理 | ❌ 不要 | await で十分、スレッドの無駄遣いになる |
| ASP.NET Core での処理全般 | ❌ 不要 | すでにスレッドプール上で動いている |
4. デッドロック ― 「なぜフリーズするのか」を理解する
非同期コードで最も遭遇しやすく、かつ原因が分かりにくいのがデッドロックです。特に WPF・WinForms・旧 ASP.NET(non-Core) の環境で頻繁に発生します。
デッドロックが起きる典型パターン
// ❌ WPF/WinForms の UIスレッドから呼ばれると確実にデッドロック
public string GetData()
{
return GetDataAsync().Result; // ← ここでUIスレッドをブロック
}
public async Task<string> GetDataAsync()
{
await Task.Delay(1000); // ← 完了後にUIスレッドに戻ろうとする
return "Done";
}
一見問題なさそうに見えますが、このコードはUIスレッドから呼ばれると必ずフリーズします。
なぜデッドロックになるのか
仕組みを順を追って見てみましょう。
① UIスレッドが GetData() を呼ぶ
↓
② .Result でUIスレッドをブロック(待ち状態に入る)
↓
③ GetDataAsync() が Task.Delay(1000) を await する
↓
④ 1秒後、Delay が完了 → GetDataAsync の続きを実行しようとする
↓
⑤ SynchronizationContext:「UIスレッドで再開するよ」
↓
⑥ UIスレッド:「.Result で待ってるから使えないよ」← ここで詰まる
↓
⑦ お互いが待ち合って永遠に動かない = デッドロック
鍵となるのは SynchronizationContext(同期コンテキスト)です。WPF・WinForms では、await の後の処理はデフォルトで元のスレッドに戻って実行されます。これはUIコントロールの更新などを安全に行うための仕組みです。ところが .Result でそのスレッドをブロックしてしまうと、戻ろうとしても入れないという悪循環が発生します。
根本的な解決策:async all the way down
デッドロックの正しい解決策は、呼び出し階層を上まですべて非同期にすることです。
// ✅ async all the way down
public async Task<string> GetDataAsync()
{
return await GetDataInnerAsync();
}
// UIイベントハンドラも async にする
private async void Button_Click(object sender, EventArgs e)
{
var result = await GetDataAsync(); // .Result も .Wait() も使わない
label.Text = result;
}
⚠️ 注意:
async voidはイベントハンドラ以外では使ってはいけません。詳しくは後述します。
ASP.NET Core ではデッドロックが起きにくい理由
ASP.NET Core は SynchronizationContext を持たない設計になっています。そのため await の後は元のスレッドに戻ろうとせず、空いているスレッドプールのスレッドで再開します。これにより、古い ASP.NET で頻発していたデッドロックのリスクが大幅に低減されています。
ただし、「起きにくい」であって「起きない」ではありません。.Result や .Wait() は ASP.NET Core でも避けた方がよいです。スレッドプールの飢餓(Thread Pool Starvation)という別の問題を引き起こすことがあります。
5. ConfigureAwait(false) の正しい理解
ConfigureAwait(false) は、おそらく C# の非同期コードの中で最も誤解されているAPIの一つです。
ConfigureAwait(false) が何をするか
await は、デフォルトで現在の SynchronizationContext を捕捉し、処理完了後にそのコンテキスト(=元のスレッド)で再開しようとします。
ConfigureAwait(false) は「コンテキストに戻らなくてもいい」という指示です。処理完了後、スレッドプールの空いているスレッドで再開するようになります。
// デフォルト:元のコンテキスト(UIスレッドなど)に戻って再開 await SomeAsync(); // ConfigureAwait(false):スレッドプールのどこかで再開(コンテキスト不問) await SomeAsync().ConfigureAwait(false);
よくある誤解①:「デッドロックを防ぐために使う」
これは非常によく広まっている誤解です。ConfigureAwait(false) でデッドロックを「防ぐ」ためには、呼び出し階層のすべての await に付ける必要があります。自分のコードだけでなく、使っているライブラリのすべての await にも付いていなければなりません。これは現実的ではありません。
.NET 非同期の第一人者である Stephen Cleary 氏も明言しています。
ConfigureAwait(false) でデッドロックを避けようとするのはせいぜいハックに過ぎない。呼び出し階層の全 await に付けなければならず、メンテナブルな解決策ではない。
根本的な解決策は「async all the way down」、つまり .Result や .Wait() を使わないことです。
よくある誤解②:「別スレッドで実行する命令」
// これは「別スレッドで実行する」命令ではない await SomeAsync().ConfigureAwait(false);
ConfigureAwait(false) は「どこで再開するか」の制御であり、「どこで実行するか」ではありません。しかも await が実際に処理を中断(yield)した場合にのみ効果があります。すでに完了している Task に付けても効果はありません。
よくある誤解③:GetAwaiter().GetResult() に付けても意味がない
// ❌ ConfigureAwait(false) は何もしていない(GetResult() はブロッキングコール) SomeAsync().ConfigureAwait(false).GetAwaiter().GetResult();
ConfigureAwait は await の挙動を設定するものです。.GetResult() で同期的にブロックする場合には全く意味を持ちません。
ConfigureAwait(false) を使うべき場面
正しい使いどころはライブラリコードです。
// ✅ 再利用可能なライブラリメソッド内での使い方
public async Task<string> FetchAsync(string url)
{
using var client = new HttpClient();
// このメソッドはUIコントロールを触らない。コンテキストに戻る必要がない。
var response = await client.GetStringAsync(url).ConfigureAwait(false);
return response;
}
ライブラリは呼び出し元の環境(UIアプリか、ASP.NETか、コンソールかなど)を知りません。UIスレッドに戻ろうとする必要がないコードでは ConfigureAwait(false) を付けておくと、余計なコンテキストスイッチを防ぎパフォーマンスが改善します。
.NET 8 の新API:ConfigureAwaitOptions
.NET 8 では ConfigureAwait に bool の代わりに ConfigureAwaitOptions 列挙型を渡せるようになりました。
// .NET 8 以降 await task.ConfigureAwait(ConfigureAwaitOptions.None); // false と同じ await task.ConfigureAwait(ConfigureAwaitOptions.ContinueOnCapturedContext); // true と同じ await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); // 例外を握りつぶす(テスト用途など)
使用判断まとめ
| 環境・用途 | ConfigureAwait(false) |
|---|---|
| ライブラリ・再利用コード | ✅ 付ける |
| WPF/WinForms でUIを更新する箇所 | ❌ 付けない(コンテキストが必要) |
| ASP.NET Core | 基本不要(SyncContextがない) |
| デッドロックの「根本的な」解決策として | ❌ 誤用 |
6. CancellationToken ― 正しいキャンセル処理
長時間の処理、ユーザーが「キャンセル」ボタンを押した場合、クライアントが切断した場合など、非同期処理を途中でやめられる設計は大切だと感じています。
基本パターン
// CancellationTokenSource でトークンを発行する var cts = new CancellationTokenSource(); // キャンセルを依頼する(例:ボタンクリック) cancelButton.Click += (s, e) => cts.Cancel(); // トークンを渡して非同期処理を実行 await DoWorkAsync(cts.Token);
// ✅ トークンを受け取り、下に伝播させる
public async Task DoWorkAsync(CancellationToken cancellationToken = default)
{
// 処理の区切りごとにキャンセルをチェック
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(1000, cancellationToken); // 組み込みメソッドにも渡す
await NextStepAsync(cancellationToken); // 子メソッドにも渡し続ける
}
よくある誤り:黙って return する
// ❌ キャンセルされたら黙って return するのは誤り
if (cancellationToken.IsCancellationRequested)
return; // 呼び出し元はキャンセルされたのか、正常完了したのか判断できない
// ✅ 例外でキャンセルを通知する
cancellationToken.ThrowIfCancellationRequested(); // OperationCanceledException をスロー
キャンセルが成功した場合は OperationCanceledException をスローするのがC# の正規のキャンセル契約です。呼び出し元はこれをキャッチしてキャンセルされたことを知ります。
タイムアウトにも使える
// 5秒でタイムアウト
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("タイムアウトまたはキャンセルされました");
}
Task.Run にトークンを渡す際の注意
// ⚠️ Task.Run の第2引数のトークンはデリゲートのキャンセルではない
await Task.Run(() =>
{
// ここでは cancellationToken は自動では観測されない
DoHeavyWork();
}, cancellationToken); // このトークンは「Start前のキャンセル」にしか使われない
Task.Run の第2引数に渡したトークンは、タスクがまだ開始されていないときのキャンセルにのみ使われます。デリゲート内部でキャンセルに応答したい場合は、デリゲート内でも明示的にチェックする必要があります。
// ✅ デリゲート内でもトークンを観測する
await Task.Run(() =>
{
for (int i = 0; i < 1000; i++)
{
cancellationToken.ThrowIfCancellationRequested();
DoPartialWork(i);
}
}, cancellationToken);
7. Task vs ValueTask
Task の問題点
Task はクラス(参照型)なので、非同期処理のたびにヒープにオブジェクトが確保されます。通常のアプリケーションではほぼ気になりませんが、1秒間に何万回も呼ばれるような hot path では GC への負荷が問題になることがあります。
ValueTask が存在する理由
ValueTask は構造体(値型)で、同期的に完了することが多いメソッドでのアロケーションを削減するために .NET Core 2.0 で追加されました。
// キャッシュが温まっていれば同期的に完了する
public ValueTask<string> GetCachedDataAsync(string key)
{
if (_cache.TryGetValue(key, out var cached))
return new ValueTask<string>(cached); // アロケーションなし
return new ValueTask<string>(FetchFromDbAsync(key)); // 通常の Task を包む
}
ValueTask の制約(重要)
ValueTask には Task にはない厳しい制約があります。
// ❌ ValueTask を複数回 await してはいけない ValueTask<int> vt = SomeValueTaskAsync(); int result1 = await vt; int result2 = await vt; // 未定義動作 // ✅ 複数回使う場合は Task に変換する Task<int> task = SomeValueTaskAsync().AsTask(); int result1 = await task; int result2 = await task; // OK
使い分けの原則
迷ったら Task を使いましょう。
ValueTask はパフォーマンス計測で実際にアロケーションが問題になってから検討するものです。むやみに使うとコードが複雑になります。
Microsoft の公式ドキュメントも「ValueTask を最初から選ぶのではなく、Task で問題が生じてから移行を検討せよ」というスタンスをとっています。
Task |
ValueTask |
|
|---|---|---|
| 型 | クラス(ヒープ) | 構造体(スタック効率的) |
| 複数回 await | ✅ OK | ❌ 1回のみ |
| 並行 await | ✅ OK | ❌ 不可 |
| 基本的な使い方 | ✅ こちらを使う | 特殊なパフォーマンス最適化 |
8. Task.WhenAll / Task.WhenAny ― 複数タスクの並列実行
逐次 await の問題
// ❌ 合計2秒かかる(順番に待つ) var user = await GetUserAsync(); // 1秒 var orders = await GetOrdersAsync(); // さらに1秒
2つの処理に依存関係がない場合、これは無駄です。
Task.WhenAll で並列実行
// ✅ 約1秒で完了(同時に実行して両方完了を待つ) var userTask = GetUserAsync(); var ordersTask = GetOrdersAsync(); await Task.WhenAll(userTask, ordersTask); // WhenAll の後は両方完了している。.Result は安全に使える var user = userTask.Result; var orders = ordersTask.Result;
💡 補足:
await Task.WhenAll()の後に.Resultを使うのは安全です。タスクがすでに完了しているため、デッドロックは起きません。
Task.WhenAny ― 最初に完了したものを処理する
// 2つのうちどちらか速い方の結果を使いたい場合 var fast = await Task.WhenAny(taskA, taskB); var result = await fast;
タイムアウト処理にも応用できます。
var workTask = DoWorkAsync();
var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));
if (await Task.WhenAny(workTask, timeoutTask) == timeoutTask)
{
throw new TimeoutException("5秒でタイムアウトしました");
}
var result = await workTask;
9. async void の危険性
なぜ async void は危険か
// ❌ async void は例外が握りつぶされる
async void DoWorkAsync()
{
await Task.Delay(100);
throw new InvalidOperationException("エラー!");
// この例外は呼び出し元に届かない。アプリがクラッシュする可能性もある
}
async void では、内部でスローされた例外を呼び出し元でキャッチできません。例外は SynchronizationContext に直接ポストされ、アプリケーション全体の未処理例外ハンドラに伝わります。
原則:async void はイベントハンドラだけ
// ✅ 戻り値のない非同期メソッドは async Task
public async Task DoWorkAsync()
{
await Task.Delay(100);
throw new InvalidOperationException("エラー!"); // 呼び出し元に伝わる
}
// イベントハンドラは仕方なく async void
private async void Button_Click(object sender, RoutedEventArgs e)
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
// イベントハンドラ内では必ず try-catch で例外を捕捉する
MessageBox.Show($"エラー: {ex.Message}");
}
}
イベントハンドラは void を返すシグネチャが要求されるため async void を使わざるを得ませんが、内部で必ず try-catch するようにしましょう。
10. まとめ ― 2026年の非同期ベストプラクティス早見表
本記事で解説したポイントをまとめます。
判断チートシート
| シチュエーション | 正解 | 誤り |
|---|---|---|
| DB / HTTP / ファイルの I/O | await をそのまま使う |
Task.Run で包む |
| WPF で重い計算をUIから切り離す | Task.Run |
UIスレッドで同期実行 |
| ASP.NET Core での処理 | await をそのまま |
Task.Run を多用 |
| 複数の独立した非同期処理 | Task.WhenAll |
逐次 await |
| 処理をキャンセルする | CancellationToken + ThrowIfCancellationRequested() |
IsCancellationRequested で return |
| 非同期を同期的に待つ | ❌ そもそもやらない | .Result / .Wait() |
| ConfigureAwait(false) | ライブラリコードに使う | デッドロック対策として使う |
| 戻り値なし非同期メソッド | async Task |
async void |
| 基本の非同期型 | Task |
最初から ValueTask |
覚えておきたい3つの原則
1. async all the way down
.Result や .Wait() は使わない。一度非同期にしたら、呼び出し階層を上まですべて非同期にする。
2. I/OバウンドとCPUバウンドを区別する
I/Oは await で、CPU処理を切り離したいときだけ Task.Run。
3. ConfigureAwait(false) の目的を正しく理解する デッドロック対策ではなく、ライブラリコードでのコンテキストスイッチ削減が目的です。
AIは魔法使いじゃない ― 社内会議で気づいた、日本企業のAI誤解
「なんでもできる」と思ったら、使いこなせなくなる

社内会議でこんな発言を聞いたことはないでしょうか。
「AIに全部任せればいいじゃないですか」
「なんでもできるんでしょ?」
「うちの業務もAIに置き換えられますよね」
悪意はないのでしょう。むしろ期待しているんだと感じ取れます。
・・・でもその期待は、少しズレていませんか?
そしてそのズレが、「全然使えなかった」「うちには向かない」という誤った結論につながっていきます。
ここで最初にハッキリと断言しておきます。
残念ながらAIは🧙魔法使いではないのです。確かにAIは魔法のように動いてくれることもあります。ですが、AIを🧙魔法使いだと思ったまま使うと、魔法が発動しないたびに失望することになります。
この記事では、AIがなぜ「魔法に見える」のか、実際はどういう仕組みなのか、そして仕組みを知ることでどう使い方が変わるのかを、できるだけ平易にエンジニアでなくても読める内容を目指しています。
AIが「魔法に見える」3つの理由
理由1: インターフェースが「話しかけるだけ」だから
電卓を使うには数字とボタンの操作が必要ですよね。Excelを使うには関数を覚える必要があります。でも ChatGPT は「〇〇を教えて」と書くだけで動いてくれます。
「話しかけるだけで動く」という体験は、これまでどんなツールにもありませんでした。だから「なんでもできる道具」に見えてしまいます。
操作が簡単なことと、できることの範囲は別の話です。でも直感的にはつながって見えてしまいます。
理由2: 結果だけが見えて、過程が見えないから
ChatGPTに「この売上データを分析して」と送ると、数秒後に分析結果が返ってきます。でもその裏では、AIがプログラム(Python コード)を生成して実行していたり、複数の検索を行っているんですね。
利用者にはその過程が見えない(正確には少し見える場合もある)ので、何が起きているか分からないまま答えが返ってくると、魔法に見えてしまいます。
「裏でなにが動いているか」を想像できると、「ここは任せられる」「ここは自分で確認が必要」という判断ができるようになります。
理由3: メディアの伝え方の問題
「AIが小説を書いた」「AIが絵を描いた」「AIが医師国家試験に合格した」
これらの見出しは間違っていませんが、少し正確さに欠けます。
正確には「LLM(Large Language Model=大規模言語モデル。ChatGPTやClaudeのような生成AIの中核技術)が文章を生成した」「画像生成AIが画像を出力した」です。
「書いた」「描いた」「合格した」という動詞は、人間の意図・理解・目的意識を含む言葉です。AIが同じことをやっているわけではありませんが、見出しはそう読めてしまいます。
こうした積み重ねが「AIは人間と同じように考えている」という印象をつくっていきます。
種明かし ― AIは「司令塔」であって「🧙魔法使い」ではない
AIの仕組みを料理に例えると分かりやすいかもしれません。
🧙魔法使いのイメージ:
シェフが食材の調達から調理器具の製造、料理まで、すべてを一人でこなします。(ユーザーもAIも区別なく、全部ひとつの魔法でこなすイメージ)
実際のAIエージェント:
オーナーシェフ(ユーザー)が「今日はこれを作りたい」と伝えます。
スーシェフ(LLM)がその意図を受け取り、「では誰に何をさせるか」を判断して指示を出します。
実際に切るのは包丁(Pythonなどのプログラム)、冷蔵庫から取り出すのは検索エンジン、盛り付けるのは別のツールです。
LLMがやっているのは「何をどの道具でやるか判断して指示すること」であり、すべてを自分でやることではありません。
これを知ると、見方が変わります。「なんでもできる」ではなく「うまく組み合わせれば、いろんなことができる」という理解になります。そしてそこには、組み合わせ方を考える人間の役割が残ります。
得意なことと苦手なこと
仕組みが分かると、得意・不得意も自然に見えてきます。
| 得意なこと | 苦手なこと |
|---|---|
| 文章を書く・要約する | 正確な数値計算(単体では) |
| パターンを見つける | 最新情報を知る |
| 言い換え・翻訳 | 事実を保証する |
| アイデア出し | 意思決定の責任を持つ |
| コードを生成する | コードが正しいと保証する |
| 大量テキストの整理 | 「本当のこと」を言う |
特に重要なのが最後の行です。
LLMは「嘘をつく」わけではありません。「もっともらしいことを言う」のが本質です。正しいかどうかより、文脈に自然に続く言葉を生成する仕組みになっています。
これを「ハルシネーション(幻覚)」と呼びます。存在しない論文を引用したり、実在しない法律を答えたりします。自信満々に、流暢な文章で。
ここを知っているかどうかで、AIの使いこなし方が大きく変わります!!
「AIが言ったから正しい」は危険です。「AIが出した案を、自分で確認する」が正しい向き合い方です。
🧙魔法使いだと思ったまま使うと起きること
誤解したまま使い続けると、いくつかのパターンで困ることになります。
下記のエピソードも、根本は「仕組みへの誤解」から来ている気がします。
※まぁ、こういう画像生成はAIのお陰で大変便利になりました。絵が描けない人には魔法ですね、これは😄
(以下はノンフィクションです。ただし登場人物・団体名はすべて仮名で、一部脚色しています。)
エピソード1: 存在しない統計データで会議が凍った話
若手社員の田中くん(仮)は、上司から「国内EC市場のレポートを明日までに」と言われ、ChatGPTに丸投げした。返ってきた文章はよく整っていた。「2023年の国内EC市場規模は約28兆円で、前年比14.2%増(経済産業省調査)」――完璧な一文に見えた。田中くんはそのまま資料に貼り付けた。
翌日の会議。先輩が首をかしげた。「田中くん、これ経産省のどのレポートですか?」

調べると、そんな調査は存在しなかった。数字も出典も、AIが「それっぽく」でっち上げたものだった。
「AIが出してきたので、てっきり正しいと……」
田中くんの言い訳に、上司は静かに言った。「AIの出力を確認するのが、君の仕事だよ」
エピソード2: 「AIの時代だ」と言い続けた部長の誤算
営業部の山田部長(仮)は、AIツール導入の推進者だった。「これからはAIの時代だ」が口癖で、部員全員に使わせた。最初の数週間は好評だった。
ところがある朝、山田部長は部下のデスクに立ち、声を荒らげた。「昨日の競合他社の決算、なんでAIは教えてくれないんだ。最新情報くらい持ってるだろ」
部下は困った顔で答えた。「ChatGPTは学習データに時間的な遅れがあるので、昨日のニュースは持っていないんですよ」
「じゃあ導入した意味がないじゃないか」

部長の一言に、部屋が静まり返った。
エピソード3: 「AIがそう言ったから」が通じなかった日
スタートアップのCTO・鈴木さん(仮)は、採用候補者のスキルシートをAIに渡し、「この人は採用すべきか?」と聞いた。AIは流暢に答えた。「技術力は高く、チームへの貢献が期待できます」。鈴木さんはその場で採用を決めた。
入社3ヶ月後、その人物が書いていたスキルの多くが実態と乖離していることが判明した。チームから不満が上がり、鈴木さんは社内でこう言った。「AIの評価を参考にしたので……」

その場にいた共同創業者が、静かに返した。「採用の判断は、あなたがしたんですよね」
AIは「魔法じゃないから面白い」
魔法だったら、使い手は関係ありません。呪文を唱えれば誰でも同じ結果が出ます。
でも AI は違います。
何を聞くか、どう分割するか、どこを疑うか、どのツールと組み合わせるか。これによって結果が大きく変わります。同じ ChatGPT を使っていても、引き出せるものが人によって全然違います。
魔法じゃないから、使い手の判断が意味を持ちます。
魔法じゃないから、使いこなせるかどうかに差が出ます。
「AIに全部任せればいい」と思っている人と、「AIをどう使えば自分の仕事が変わるか」を考えている人とでは、1年後に見えている景色がかなり違ってきます。
仕組みを知ることは、怖がることでも、崇めることでもありません。適切に付き合うための第一歩だと感じています。
大事なことなので、最後にもう一度。
「AIは🧙魔法使いではありません」
Moq から NSubstitute への乗り換えガイド
テストコードからDBと外部APIを切り離す ― NSubstituteで始めるモック入門
C# でユニットテストを書くとき、「データベースや外部 API に依存していてテストしにくい」という壁にぶつかることがあります。そこで活躍するのがモックライブラリです。この記事では、モックとは何かという基本から、2023年の Moq 騒動を経て現在有力な選択肢となっている NSubstitute の使い方まで、まとめて紹介します。
モックライブラリとは
ユニットテストの目的は「テスト対象のクラスだけを動かすこと」です。ところが、実際のコードはデータベースや外部 API と密接に連携していることが多く、そのままではテストが難しくなります。

モックとは、インターフェースや抽象クラスを実装した「偽物のオブジェクト」です。「FindById(1) を呼んだらこの値を返す」といった振る舞いをテストコード側から自由に設定できます。モックライブラリは、その偽物オブジェクトを自動生成してくれるツールです。
モックを使ったテストの流れ

モックを使うことで、データベースなしでもテストが書けるようになります。
Moq とは、そして何があったか
Moq(モック)は長年 C# コミュニティで最も広く使われてきたモックライブラリです。流暢な API で書けるため、多くのプロジェクトに採用されていました。
// Moq の書き方(参考)
var mock = new Mock<IUserRepository>();
mock.Setup(x => x.FindById(1)).Returns(new User { Id = 1 });
IUserRepository repository = mock.Object;
ところが 2023年8月、Moq 4.20.0 に SponsorLink というコードが無断で追加され、ユーザーの git メールアドレスをハッシュ化して外部サーバーに送信していることが発覚しました。その後、作者は該当コードを削除しましたが、コミュニティの信頼が大きく揺らぎ、代替フレームワークへの移行を検討するプロジェクトが増えました。
NSubstitute とは
NSubstitute は Moq と同じくインターフェースのモックを自動生成するライブラリです。Moq との大きな違いは、より自然な C# の構文に近い API 設計にあります。
// NSubstitute の書き方
IUserRepository repository = Substitute.For<IUserRepository>();
repository.FindById(1).Returns(new User { Id = 1 });
Setup や .Object が不要で、コードがすっきりする点が好まれています。
環境・前提条件
- .NET 6 以降(この記事のサンプルコードは .NET 6 ベース)
- テストフレームワーク: xUnit(NUnit / MSTest でも同様に使えます)
Note: .NET Framework 4.8 をお使いの場合 NSubstitute 5.x は .NET 6 以降が必須です。.NET Framework 4.8 では NSubstitute 4.x(.NET Standard 2.0 対応)を使ってください。基本的な API はほぼ同じですが、一部の機能に差異があります。
dotnet add package NSubstitute --version 4.*
インストール
dotnet add package NSubstitute
Moq からの移行であれば、先に Moq を取り除いておきます。
dotnet remove package Moq dotnet add package NSubstitute
using ディレクティブも置き換えます。
// Before using Moq; // After using NSubstitute;
NSubstitute の基本的な使い方
モックの作成
インターフェースを渡すだけでモックオブジェクトが生成されます。
IUserRepository repository = Substitute.For<IUserRepository>();
Moq と違い、生成されたオブジェクトをそのまま使えます(.Object 不要)。
メソッドの戻り値を設定する
repository.FindById(1).Returns(new User { Id = 1, Name = "Alice" });
呼び出したいメソッドを実際に呼び出し、.Returns() をチェーンするだけです。
非同期メソッドも同じ書き方で対応できます。
repository.FindByIdAsync(1).Returns(new User { Id = 1, Name = "Alice" });
プロパティの値を設定する
repository.IsConnected.Returns(true);
引数マッチャー
特定の値に限らず「どんな値でも」と指定したい場合は Arg.Any<T>() を使います。
// どんな int が渡されても同じユーザーを返す repository.FindById(Arg.Any<int>()).Returns(new User()); // 条件付き repository.FindById(Arg.Is<int>(id => id > 0)).Returns(new User());
呼び出しを検証する
テスト対象が正しくモックを呼んだか確認するには Received() を使います。
// 1回呼ばれたか repository.Received(1).Save(Arg.Any<User>()); // 一度も呼ばれていないか repository.DidNotReceive().Save(Arg.Any<User>()); // ちょうど3回呼ばれたか repository.Received(3).Save(Arg.Any<User>());
例外をスローさせる
repository.FindById(Arg.Any<int>()).Throws(new NotFoundException());
コールバック(副作用の記録)
渡された引数を後から検証したいときなどに使います。
var capturedUser = default(User);
repository
.When(x => x.Save(Arg.Any<User>()))
.Do(callInfo => capturedUser = callInfo.Arg<User>());
callInfo.Arg<T>() で引数を取り出せます。
連続した戻り値
// 1回目は1、2回目は2、3回目は3を返す repository.GetNext().Returns(1, 2, 3);
Moq ユーザー向け対応表
すでに Moq を使っている方向けに、主要な API の対応をまとめます。
| やりたいこと | Moq | NSubstitute |
|---|---|---|
| モック作成 | new Mock<T>().Object |
Substitute.For<T>() |
| 戻り値設定 | Setup(...).Returns(...) |
method(...).Returns(...) |
| 非同期戻り値 | Setup(...).ReturnsAsync(...) |
method(...).Returns(...) |
| 任意引数 | It.IsAny<T>() |
Arg.Any<T>() |
| 条件付き引数 | It.Is<T>(...) |
Arg.Is<T>(...) |
| 呼び出し検証 | Verify(..., Times.Once) |
Received(1).method(...) |
| 未呼び出し検証 | Verify(..., Times.Never) |
DidNotReceive().method(...) |
| 例外スロー | Setup(...).Throws(...) |
method(...).Throws(...) |
| コールバック | Setup(...).Callback(...) |
When(...).Do(...) |
| 連続戻り値 | SetupSequence(...) |
Returns(v1, v2, v3) |
移行のコツ
It.IsAny< → Arg.Any<、It.Is< → Arg.Is< はエディタの一括置換でほぼ対応できます。mock.Object の削除と Verify → Received の書き換えは構造が変わるため、手動で確認しながら進めるのが安心だと感じています。
まとめ
- モックライブラリを使うと、DB や外部 API に依存せずユニットテストが書ける
- Moq は長年の定番だったが、2023年の騒動で信頼が揺らいだ
- NSubstitute は
Setupや.Objectが不要でコードがすっきりしやすい - .NET Framework 4.8 では NSubstitute 4.x を使う
- Moq からの移行は引数マッチャー周りは一括置換、検証は手動確認で進めるとスムーズ
実際に移行してみると、「あ、Moqより書きやすいな」と感じる場面が思いのほか多かったです。