C# にて、「スタティックなメンバは、そのクラスが始めて使われるときに初期化されるという誤解」をしていたという、ある自分のある8月の話。
次のようなコードで、不思議なことが起こります。
using System;
using System.Windows.Forms;
public class MyForm : Form
{
private MyForm()
{
MessageBox.Show("Called \"MyForm.MyForm()\"");
}
static MyForm defaultForm = new MyForm();
public static MyForm Default
{
get { return defaultForm; }
}
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
MessageBox.Show("Do \"Application.Run(MyForm.Default);\"");
Application.Run(MyForm.Default);
}
}
スタティックなメンバの初期化タイミングに関する罠
上記のコードで、Program.Main() 中の Application.SetCompatibleTextRenderingDefault(false); ですが、このメソッドは、Form の初期化前に呼ぶ必要があります。Form を初期化してから呼び出すと、以下のような例外が起こります。
System.InvalidOperationException はハンドルされませんでした。
Message="最初の IWin32Window オブジェクトがアプリケーションで作成される前に、SetCompatibleTextRenderingDefault が呼び出されなければなりません。"
Source="System.Windows.Forms"
StackTrace:
場所 System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(Boolean defaultValue)
場所 Program.Main() 場所 D:\CodeGagdet\TestStaticClass\TestStaticClass\Program.cs:行 25
場所 System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
場所 System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
場所 Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
場所 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
場所 System.Threading.ThreadHelper.ThreadStart()
InnerException:
上記のコードでも、きちんとこの順番を守って呼び出している「つもり」です。
しかし、よく見ると、MyForm は自身のインスタンスをスタティックメンバー defaultForm として持っています。
この defaultForm の初期化は、「MyForm が初めて使用される Application.Run() 時に行われる」ものだと思っていたら痛い目にあいました。。
実行条件によって変わるスタティックメンバーの初期化タイミング
実は、スタティックなメンバーの初期化タイミングは、実行条件によって変わっていました。
- Debugビルド + デバッグあり実行
- 初めて MyForm が使用される MyForm.Defaul プロパティの呼び出し直前。
- Debugビルド + デバッグなし実行
- 同上
- Releaseビルド + デバッグあり実行
- 同上
- Releaseビルド + デバッグなし実行
- Program.Main関数に入ったとき。あるいは、その直前。
4つ目のケース「Releaseビルド + デバッグなし実行」では、defaultForm が Application.SetCompatibleTextRenderingDefault() より手前で初期化されます。
これでは、先に挙げた例外 InvalidOperationException が発生します。
もちろん、上手くいく系もあるでしょうし、逆に Debug モードでだって上手くいかない系もあるかと思います。
解決方法(追記@2008-10-03)
static MyForm defaultForm;
public static MyForm Default
{
get
{
if (defaultForm == null)
defaultForm = new MyForm();
return defaultForm;
}
}
スタティックなメンバーのインスタンス生成を自分が意図したタイミングまで引き伸ばしますことで解決しました。
コメント欄にもあるように、スタティックなコンストラクタを用意することでも解決するようですが、これもタイミングが本当に一定なのかがわからないという意味で、やめた方が良いでしょう。
まとめ
スタティックなメンバーの初期化は、いつ行われるかわかりません。
少なくとも初めてそのクラスが使われるまでには初期化されますが、今回のようなケースだとだいぶ痛い目に合います。
そんなこんなで、気をつけましょうね、という日記でした。