C# いつも脳死で Program.cs に実装している内容

# はじめに
タイトルの通りです。

今時 .NET Framework の Windows.Forms で新規開発案件とかあるんですか？と聞かれそうですが、4年ほど受託開発してきて担当案件の9割9分はこれでしたし、半年以上先の案件/開発予定もこれだけで埋まっている状況です...！

この感じがまだしばらく続くのかなと思ったので、コピペ用に書き残しておきます。

# 2024-04-20 追記
たくさんのコメントありがとうございました...！
頂いた内容の方法でも試そうと、ひとつずつ機能の差異なども調べていたのですが、自分にはまだ理解が追いつかない部分もあり（くやしいくやしい...:joy:）、少し沼ってきたため、追記という形ではなくどこかで個別に新規記事として投稿できればと思います...:wink:

# 環境
* Microsoft .NET Framework 4.8.1
* Windows.Forms

# ソース
例外捕捉時、メッセージを表示していますが、実際はログ出力にすることが多いです。
```Program.cs
using System;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace App
{
    internal static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.ThreadException += Application_ThreadException;
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var mutex = new Mutex(true, Application.ProductName, out bool createdNew);

            try
            {
                if (!createdNew)
                {
                    MessageBox.Show(
                        text: "このアプリケーションは既に起動中です。",
                        caption: "Error",
                        buttons: MessageBoxButtons.OK,
                        icon: MessageBoxIcon.Error);

                    return;
                }

                Application.Run(new SampleForm());
            }
            finally
            {
                if (createdNew)
                {
                    mutex.ReleaseMutex();
                }

                mutex.Close();
            }
        }

        private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
        {
            var text = new StringBuilder();
            text.AppendLine(e.Exception.Message);
            text.AppendLine("スタックトレース:");
            text.AppendLine(e.Exception.StackTrace);

            MessageBox.Show(
                text: text.ToString(),
                caption: "Error",
                buttons: MessageBoxButtons.OK,
                icon: MessageBoxIcon.Error);
        }
    }
}
```

# 動作確認
動作確認用の Form を作成したので、テストしていきます。
二重起動防止については省略です。

``` SampleForm.cs
using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace App
{
    public partial class SampleForm : Form
    {
        public SampleForm() => InitializeComponent();

        // UI スレッドで例外
        private void ThrowExceptionButton_Click(object sender, EventArgs e)
            => int.Parse(null);

        // async/await で例外
        private async void AwaitTaskRunButton_Click(object sender, EventArgs e)
            => await Task.Run(() => int.Parse(null));

        // Task.Wait() で例外
        private void TaskRunWaitButton_Click(object sender, EventArgs e)
            => Task.Run(() => int.Parse("Hoge")).Wait();

        // Task.Run() で例外
        private void TaskRunButton_Click(object sender, EventArgs e)
            => Task.Run(() => int.Parse("2147483648"));
    }
}
```
画面はこんな感じ、ボタンをクリックすると例外が発生します。
![SampleForm.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679508/ea283292-b36d-d4b1-3fde-df8716a38875.jpeg)

## 1. UIスレッドでの例外発生
未処理の例外を捕捉できていて良い感じです。
![Error1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679508/26bc3891-ab6d-79ad-aab3-c4ae4db872ae.jpeg)

## 2. サブスレッドでの例外発生 (await)
こちらも良い感じです。
![Error2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679508/d2f0068d-11f8-e47e-dbc1-ec7e8ad5c3fd.jpeg)

## 3. サブスレッドでの例外発生 (Wait())
これは良い感じのように見えて、例外メッセージがおかしいですね。
「1 つ以上のエラーが発生しました。」と表示されており、どの例外を捕捉したのか分かりません。
![Error3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3679508/0096da24-cc57-1ab0-3aef-5500c5fc0e3a.jpeg)

これは Task.Wait() を使用した場合、複数の例外が [AggregateException](https://learn.microsoft.com/ja-jp/dotnet/api/system.aggregateexception?view=net-5.0) にまとめられる仕様のためです。
詳細は下記の記事をご覧ください（丸投げ①）。

https://qiita.com/tera1707/items/d5a3bc12ffa5f80069a1

個人的にはそもそも開発時に Task.Wait() や Task.Result の使用を非推奨（というか禁止）にしているため、今回このケースには対応していません...！
非推奨の理由は下記の MSDN 参照です（丸投げ②）。

https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

async/await を使ってください...！

## 4. サブスレッドでの例外発生 (待たない場合)
このケースでは、そもそも例外を捕捉できません。
待たない場合は下記のように try catch で囲んでも捕捉できません...
``` SampleForm.cs
        private void TaskRunButton_Click(object sender, EventArgs e)
        {
            try
            {
                Task.Run(() => int.Parse("2147483648"));
            }
            catch (Exception ex)
            {
                // ※ここには入ってこない...
                MessageBox.Show(ex.ToString());
            }
        }
```

# おわりに
コピペ用にサクッと書き切るつもりが、非同期処理の例外関連で分量が多くなりました。
調べ物が少し大変でしたが、勉強になったのでよかったです...！