- 型の初期化:これは、クラスのすべての静的変数を初期化し、静的コンストラクターを実行するために実行されるコードです。
- ループ:互いに依存する2つのコード。 私たちの場合、これらは2つのクラスであり、その型の初期化には別のクラスの既に初期化された型が必要です。
さて、危機にしているものを示す小さな例:
using System; class Test { static void Main() { Console.WriteLine(First.Beta); } } class First { public static readonly int Alpha = 5; public static readonly int Beta = Second.Gamma; } class Second { public static readonly int Gamma = First.Alpha; }
このコードの結果は0になります
もちろん、仕様を検討しないと、このような期待は機能しません。 したがって、仕様(C#4バージョンのセクション10.5.5.1)を見てみましょう。
クラスの静的フィールド変数初期化子は、クラス宣言に現れるテキスト順で実行される一連の割り当てに対応します。 クラスに静的コンストラクター(§10.12)が存在する場合、静的フィールド初期化子の実行は、その静的コンストラクターを実行する直前に発生します。 そうでない場合、静的フィールド初期化子は、そのクラスの静的フィールドを最初に使用する前の実装依存の時間に実行されます。
翻訳:
クラスの静的フィールドの初期化順序は、クラスのソースコードでの位置の順序に対応しています。 クラスに静的コンストラクターがある場合、クラスの静的フィールドの初期化コードの実行は、静的コンストラクターが呼び出される直前に配置されます。 そうではなく、静的コンストラクターが存在しない場合、静的フィールドの初期化は、特定の実装に依存する場所で実行されます。これは、静的フィールドの最初の使用前に発生します。
言語仕様に加えて、
CLI
仕様からの抜粋を引用でき
CLI
。これにより、型の初期化、特に循環依存関係とマルチスレッド操作に関する詳細が明らかになります。 ただし、これは行いませんが、短い抜粋をいくつか書いてください。
- 型の初期化中にスレッドの安全性を保証
- タイプAを初期化する必要があることに
CLI
気づき、同じスレッドで初期化を行っている場合、CLI
はタイプAがすでに初期化されているかのように機能し続けます。
だからあなたの意見ではどうなりますか:
-
Test
初期化:これ以上のアクションは不要です。 -
Main
開始 -
First
初期化を開始します(First.Beta
が必要なFirst.Beta
) -
First.Alpha
を5に設定しFirst.Alpha
-
Second
初期化を開始します(Second.Gamma
が必要なSecond.Gamma
) -
Second.Gamma
をFirst.Alpha
設定しFirst.Alpha
(5) - 初期化終了
Second
-
First.Beta
をSecond.Gamma
設定しSecond.Gamma
(5) -
First
初期化を終了 - 「5」と入力します
そして、実際に起こっているのは、.Net Framework 4.5ベータがインストールされたコンピューター上です( .NET 4で型の初期化が変更されたことは知っています。これは不可能だと主張します)
-
Test
初期化:これ以上のアクションは不要です。 -
Main
開始 -
First
初期化を開始します(First.Beta
が必要なFirst.Beta
) -
Second
初期化を開始します(Second.Gamma
が必要Second.Gamma
) -
Second.Gamma
をFirst.Alpha
設定しFirst.Alpha
(0) - 初期化終了
Second
-
First.Alpha
を5に設定しFirst.Alpha
-
First.Beta
をSecond.Gamma
(0)に設定しFirst.Beta
-
First
初期化を終了 - タイプ0
ステップ(5)は非常に興味深いです。
First.Alpha
さらに取得するには、
First
を初期化する必要があることを知っています。 ただし、このスレッドはすでに
First
初期化しています。そのため、すべてが正常であることを期待して、初期化をスキップします。 ただし、この時点では変数はまだ初期化されていません。 おっと...
(説明されているすべての問題を回避する微妙な点が1つあります:constキーワードの使用)
現実の世界に戻る
型の初期化時に循環依存関係を使用することが人生を台無しにすることになる理由を、私の例が明らかにしたことを願っています。 そのような場所をキャッチしてデバッグするのは非常に困難です。 そして実際、それは古典的なガイゼンバッグです。 この例では、プログラムが最初に
Second
初期化する(たとえば、別の変数にアクセスする)場合、まったく異なる結果が得られることを理解することが重要です。 そして、実際には、すべての単体テストを開始すると、それらすべてが失敗するという事実につながる可能性があります。 しかし、同時にそれらを別々に実行する場合、それらは機能します(1つを除いて、かなり可能です)。
このような状況を回避する1つの方法は、型の初期化を完全に放棄することです。 ほとんどの場合、これは必要なものです。 ただし、通常はよく知られたものを使用します。
Encoding.Utf8
や
TimeZoneInfo.Utc
。 どちらの場合も、これらは静的プロパティですが、静的フィールドを使用しているようです。 一見すると、
public static readonly
public static get-only
プロパティの使用は同じように見えますが、後で見るように、プロパティを使用すると利点が得られます。
私の野田タイムライブラリには、私たちと同じような瞬間があります。 そして、すべてこのライブラリの多くのタイプは不変であるため 、つまり 不変です。 これは、独自の
UTC
タイムゾーンまたは
ISO calendar system
を作成する必要がある場合に意味があります。 さらに、公開されている値に加えて、ライブラリ内で(主にタスクをキャッシュするために)使用される多くの静的変数があります。 これにより、ライブラリのテストがより難しくなりますが、この場合のパフォーマンス上の利点は非常に重要です。
残念ながら、これらのフィールドとプロパティの膨大な数には循環的な依存関係があります。 前述したように、新しい静的フィールドを追加すると、プログラムでさまざまな故障が発生する可能性があります。 差し迫った原因を修正することはできますが、コードの整合性について懸念を感じます。 結局、1つの問題を修正したとしても、他の問題がないという保証はありません。
タイプ初期化テスト
型を初期化する際の主な問題の1つは、
AppDomain
内の型が1回だけ初期化されるという保証と組み合わせて、初期化順序に敏感です。 前に示したように、1つの初期化順序ではこれによりエラーが発生し、他の一部ではエラーが発生しない可能性があります。
私自身は、Noda Timeを開発するときに、周期的な依存関係によって問題が発生しないことを絶対に確認したいと思いました。 T.O. 型が初期化されるとき、型が初期化される順序に関係なく、サイクルが形成されないことを確認したいと思います。 論理的に考えると、1つのタイプで始まり、同じサイクルにある他のタイプで始まる循環依存関係を定義できます。 極端な場合を見逃さないようにし、可能なすべてのオプションを整理し、何も見失わないようにすることを非常に心配しています。 したがって、ブルートフォース法-徹底的な検索を適用しました。
大まかな計画は次のとおりです。
- 依存関係の空のリストから始めます。
- ターゲットアセンブリの種類ごとに:
- 新しいAppDomainを作成
- アセンブリをダウンロードする
- タイプを初期化する(初期化プロセスを開始するアクションを実行する)
- 各タイプの初期化の最初からスタックトレースを表示し、すべての依存関係を記録します。
- 概要リストで循環依存関係を表示する
アプリケーションドメインの1回のダウンロードで循環依存関係を判断できる状況は決してないことに注意してください。 これを行うには、すべてのタイプをバイパスしてサイクルを特定し、結果を分析する必要があります。
コードがどのように機能するかの説明は、コード自体よりもはるかに大きくなり、実際、非常に理解しやすいので、記事の最後に記載します。
このソリューションはいくつかの理由であまり良くありません:
- ユニットテストプログラムから新しいAppDomainを作成し、それにアセンブリを読み込むことは、それほど簡単ではないかもしれません。 私のコードは
NCrunch
と連携して正しく機能しません。これを修正すると、ユニットテストシステムの残りの部分でプログラムが破損することは間違いありません。 - 各タイプ初期化子には、システムが動作するために必要なコード行が含まれるという事実に基づいています。
private static readonly int TypeInitializationChecking = NodaTime.Utility.TypeInitializationChecker.RecordInitializationStart();
- 興味のあるタイプごとにコード行を追加する必要があるのは悪いことです。 この行は、型が初期化されるたびに呼び出されるため、これは悪いです。 また、ヒープから少なくとも4バイトを選択します。これは、プログラムがテストモードで実行されていない場合は非常に不適切です。 もちろん、プリプロセッサディレクティブを使用して、テスト用ではなく、このコードをバージョンから削除できます。 しかし、これからコードはさらに汚くなります。
- このメソッドは、テストが実行された.Netバージョンの循環依存関係のみを検出します。 .Net Frameworkのバージョンが異なると、テストが状況の100%をカバーするかどうかはわかりません。 同様に、現在のCultureInfo、または一見一定の環境変数を変更すると、テストはまったく異なる方法で動作します。
- また、この実装では、コードがマルチスレッド化されている状況は見ていません。 このような状況では、これが正しく機能するかどうかはわかりません。
そして、これらすべての予約を考慮して...使用する価値はありますか? 間違いなくはい。 この手法は、修正された多くのバグを見つけるのに役立ちました。
循環依存関係を修正
以前は、コードに沿ってフィールドを移動するだけで、型の初期化順序を「修正」していました。 サイクルはまだ存在していましたが、それらを無害にする方法を見つけました。 このアプローチはスケーラブルではなく、見かけよりもはるかに多くの労力がかかると言えます。 コードが難しくなります...そして、ある日、2つ以上の依存関係のループが発生した場合、それを安全にするための心のパズルになります。 現時点では、非常に単純な手法を使用して、静的変数の遅延初期化を実装しています。
したがって、
static readonly field
が循環依存関係を作成するものを探す代わりに、ネストされたプライベート静的クラスで
internal static readonly field
を返す
static readonly property
を使用し
static readonly property
。 単一の呼び出しを保証するスレッドセーフな初期化はまだありますが、
nested
型は、必要になるまで初期化されません。
代わりに:
// Requires Bar to be initialized - if Bar also requires Foo to be // initialized, we have a problem... public static readonly Foo SimpleFoo = new Foo(Bar.Zero);
私たちは書きます:
public static readonly Foo SimpleFoo { get { return Constants.SimpleFoo; } } private static class Constants { private static readonly int TypeInitializationChecking = NodaTime.Utility.TypeInitializationChecker.RecordInitializationStart(); // This requires both Foo and Bar to be initialized, but that's okay // so long as neither of them require Foo.Constants to be initialized. // (The unit test would spot that.) internal static readonly Foo SimpleFoo = new Foo(Bar.Zero); }
現時点では、これらのクラスに静的コンストラクターを含めて遅延初期化を実現するかどうかを判断できません。 Foo型の初期化子がFoo.Constants型の初期化子を呼び出す場合、開始点に戻ります。 しかし、ネストされたクラスのそれぞれに静的コンストラクターを追加するのはひどく聞こえます。
おわりに
私は、実際には私の一部がコードテストの作成や回避策の実行、松葉杖の作成を好まないことを伝えたいと思います。 そして間違いなく、静的フィールドのみのストレージを避けて、型(またはその一部)の初期化を実際に取り除くことができるかどうかを検討する価値があります。 これらの依存関係をすべて見つけて、プログラムまたはユニットテストの実行を回避できれば素晴らしいと思います。 これは、静的アナライザーを使用して実行できるようにするためです。 機会があれば、
NDepend
これを助けてくれるかどうかを調べてみます。
ただし、このアプローチはある種のハッキングのように見えますが、代替手段(エラーでいっぱいのコード)よりも優れています。 そして...私は言うのを恥ずかしく思いますが、私は
Noda Time
私がすべての周期的依存関係を見つけたとは思いません。 あなた自身のコードで試してみる価値があります-隠れた問題があるかもしれない場所を見てください
アプリケーション:テストコード
TypeInitializationChecker
internal sealed class TypeInitializationChecker : MarshalByRefObject { private static List<Dependency> dependencies = null; private static readonly MethodInfo EntryMethod = typeof(TypeInitializationChecker).GetMethod("FindDependencies"); internal static int RecordInitializationStart() { if (dependencies == null) { return 0; } Type previousType = null; foreach (var frame in new StackTrace().GetFrames()) { var method = frame.GetMethod(); if (method == EntryMethod) { break; } var declaringType = method.DeclaringType; if (method == declaringType.TypeInitializer) { if (previousType != null) { dependencies.Add(new Dependency(declaringType, previousType)); } previousType = declaringType; } } return 0; } /// <summary> /// Invoked from the unit tests, this finds the dependency chain for a single type /// by invoking its type initializer. /// </summary> public Dependency[] FindDependencies(string name) { dependencies = new List<Dependency>(); Type type = typeof(TypeInitializationChecker).Assembly.GetType(name, true); RuntimeHelpers.RunClassConstructor(type.TypeHandle); return dependencies.ToArray(); } /// <summary> /// A simple from/to tuple, which can be marshaled across AppDomains. /// </summary> internal sealed class Dependency : MarshalByRefObject { public string From { get; private set; } public string To { get; private set; } internal Dependency(Type from, Type to) { From = from.FullName; To = to.FullName; } } }
TypeInitializationTest
[TestFixture] public class TypeInitializationTest { [Test] public void BuildInitializerLoops() { Assembly assembly = typeof(TypeInitializationChecker).Assembly; var dependencies = new List<TypeInitializationChecker.Dependency>(); // Test each type in a new AppDomain - we want to see what happens where each type is initialized first. // Note: Namespace prefix check is present to get this to survive in test runners which // inject extra types. (Seen with JetBrains.Profiler.Core.Instrumentation.DataOnStack.) foreach (var type in assembly.GetTypes().Where(t => t.FullName.StartsWith("NodaTime"))) { // Note: this won't be enough to load the assembly in all test runners. In particular, it fails in // NCrunch at the moment. AppDomainSetup setup = new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory }; AppDomain domain = AppDomain.CreateDomain("InitializationTest" + type.Name, AppDomain.CurrentDomain.Evidence, setup); var helper = (TypeInitializationChecker)domain.CreateInstanceAndUnwrap(assembly.FullName, typeof(TypeInitializationChecker).FullName); dependencies.AddRange(helper.FindDependencies(type.FullName)); } var lookup = dependencies.ToLookup(d => d.From, d => d.To); // This is less efficient than it might be, but I'm aiming for simplicity: starting at each type // which has a dependency, can we make a cycle? // See Tarjan's Algorithm in Wikipedia for ways this could be made more efficient. // http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm foreach (var group in lookup) { Stack<string> path = new Stack<string>(); CheckForCycles(group.Key, path, lookup); } } private static void CheckForCycles(string next, Stack<string> path, ILookup<string, string> dependencyLookup) { if (path.Contains(next)) { Assert.Fail("Type initializer cycle: {0}-{1}", string.Join("-", path.Reverse().ToArray()), next); } path.Push(next); foreach (var candidate in dependencyLookup[next].Distinct()) { CheckForCycles(candidate, path, dependencyLookup); } path.Pop(); } }