82

ウェビナーJon Skeet Inspects ReSharperを見た後、再帰コンストラクター呼び出しを少し試してみたところ、次のコードが有効な C# コードであることがわかりました (有効とは、コンパイルすることを意味します)。

class Foo
{
    int a = null;
    int b = AppDomain.CurrentDomain;
    int c = "string to int";
    int d = NonExistingMethod();
    int e = Invalid<Method>Name<<Indeeed();

    Foo()       :this(0)  { }
    Foo(int v)  :this()   { }
}

おそらくご存知のとおり、フィールドの初期化はコンパイラによってコンストラクタに移動されます。したがって、のようなフィールドがある場合int a = 42;a = 42すべてコンストラクターに含まれます。ただし、別のコンストラクターを呼び出すコンストラクターがある場合は、呼び出されたコンストラクターにのみ初期化コードがあります。

たとえば、既定のコンストラクターを呼び出すパラメーターを持つコンストラクターがある場合a = 42、既定のコンストラクターでのみ割り当てが行われます。

2 番目のケースを説明するには、次のコードを使用します。

class Foo
{
    int a = 42;

    Foo() :this(60)  { }
    Foo(int v)       { }
}

次のようにコンパイルします。

internal class Foo
{
    private int a;

    private Foo()
    {
        this.ctor(60);
    }

    private Foo(int v)
    {
        this.a = 42;
        base.ctor();
    }
}

したがって、主な問題は、この質問の冒頭で与えられた私のコードが次のようにコンパイルされることです。

internal class Foo
{
    private int a;
    private int b;
    private int c;
    private int d;
    private int e;

    private Foo()
    {
        this.ctor(0);
    }

    private Foo(int v)
    {
        this.ctor();
    }
}

ご覧のとおり、コンパイラはフィールドの初期化を配置する場所を決定できず、その結果、どこにも配置しません。baseまた、コンストラクター呼び出しがないことに注意してください。もちろん、オブジェクトを作成することはできずStackOverflowException、のインスタンスを作成しようとすると、常に終了しますFoo

2 つの質問があります。

コンパイラが再帰的なコンストラクター呼び出しを許可するのはなぜですか?

そのようなクラス内で初期化されたフィールドに対してコンパイラのそのような動作を観察するのはなぜですか?


いくつかの注意事項: ReSharperは で警告しますPossible cyclic constructor calls。さらに、Java では、このようなコンストラクター呼び出しはイベント コンパイルされないため、このシナリオでは Java コンパイラーはより制限的になります (Jon はウェビナーでこの情報について言及しました)。

これにより、これらの質問がより興味深いものになります。Java コミュニティに関しては、C# コンパイラは少なくともより最新であるためです。

これは、C# 4.0およびC# 5.0コンパイラを使用してコンパイルされ、 dotPeekを使用して逆コンパイルされました。

4

4 に答える 4

11

興味深い発見。

実際には、インスタンス コンストラクターは 2 種類しかないようです。

  1. 構文を使用して、同じ型の別のインスタンス コンストラクターをチェーンするインスタンス コンストラクター: this( ...)
  2. 基本クラスのインスタンス コンストラクターをチェーンするインスタンス コンストラクター。これには、chainig がデフォルトであるため、chainig が指定されていないインスタンス コンストラクターが含まれ: base()ます。

System.Object(私は特別なケースであるインスタンス コンストラクターを無視しました。System.Object基底クラスはありません!しかしSystem.Object、フィールドもありません。)

クラスに存在する可能性のあるインスタンス フィールド初期化子は、上記のタイプ2.のすべてのインスタンス コンストラクターの本体の先頭にコピーする必要がありますが、タイプ1.のインスタンス コンストラクターはフィールド代入コードを必要としません。

したがって、C# コンパイラがタイプ1のコンストラクターを分析して循環があるかどうかを確認する必要はないようです。

この例では、すべてのインスタンス コンストラクターが 1 型である状況を示しています。その場合、フィールド初期化コードをどこにも置く必要はありません。そのため、あまり深く分析されていないようです。

すべてのインスタンス コンストラクターの型が1.である場合、アクセス可能なコンストラクターを持たない基本クラスから派生することもできます。ただし、基本クラスは非シールでなければなりません。たとえば、privateインスタンス コンストラクターのみでクラスを作成する場合、派生クラスのすべてのインスタンス コンストラクターを型1にすると、ユーザーはクラスから派生することができます。ただし、もちろん、新しいオブジェクト作成式は終了しません。System.Runtime.Serialization.FormatterServices.GetUninitializedObject派生クラスのインスタンスを作成するには、メソッドのようなものを「チート」して使用する必要があります。

別の例:System.Globalization.TextInfoクラスにはinternalインスタンス コンストラクターしかありません。mscorlib.dllただし、この手法以外のアセンブリでこのクラスから派生させることはできます。

最後に、

Invalid<Method>Name<<Indeeed()

構文。C# の規則によると、これは次のように読み取られます。

(Invalid < Method) > (Name << Indeeed())

左シフト演算子は、小なり演算子と大なり<<演算子の両方よりも優先順位が高いためです。後者の 2 つの演算子は優先順位が同じであるため、左結合規則によって評価されます。タイプがあった場合<>

MySpecialType Invalid;
int Method;
int Name;
int Indeed() { ... }

のオーバーロードがMySpecialType導入された場合、式(MySpecialType, int)operator <

Invalid < Method > Name << Indeeed()

合法的で意味のあるものになります。


私の意見では、コンパイラがこのシナリオで警告を発行した方がよいでしょう。たとえば、unreachable code detectedIL に変換されることのないフィールド初期化子の行番号と列番号を指し示すことができます。

于 2013-05-20T19:49:45.950 に答える
5

言語仕様では、定義されている同じコンストラクターを直接呼び出すことのみが除外されているためだと思います。

10.11.1 以降:

すべてのインスタンス コンストラクター ( class のコンストラクターを除くobject) には、constructor-body の直前に別のインスタンス コンストラクターの呼び出しが暗黙的に含まれます。暗黙的に呼び出すコンストラクターは、constructor-initializer によって決定されます。

...

  • フォームのインスタンス コンストラクター初期化子により、クラス自体からインスタンス コンストラクターが呼び出されます ... インスタンス コンストラクター宣言に、コンストラクター自体を呼び出すコンストラクター初期化子が含まれている場合、コンパイル時エラーが発生します。this(argument-listopt)

その最後の文は、コンパイル時エラーを生成するため、直接呼び出し自体を排除するだけのようです。

Foo() : this() {}

違法です。


認めますが、それを許可する具体的な理由がわかりません。もちろん、IL レベルでは、実行時にさまざまなインスタンス コンストラクターを選択できるため、このような構成が許可されていると思います。


これについてフラグを立てたり警告したりしないもう1つの理由は、この状況を検出する必要がないためだと思います。サイクルが存在するかどうかを確認するためだけに、何百もの異なるコンストラクターを追跡することを想像してみてください。試行された使用法が実行時にすぐに爆発する場合 (私たちが知っているように)、かなり特殊なケースです。

各コンストラクターのコード生成を行うとき、考慮されるのはconstructor-initializer、フィールド初期化子、およびコンストラクターの本体だけです。他のコードは考慮されません。

  • がクラス自体のインスタンス コンストラクターである場合constructor-initializer、フィールド初期化子を発行しませんconstructor-initializer。呼び出しを発行してから本体を発行します。

  • 直接基底クラスのインスタンス コンストラクターの場合constructor-initializer、フィールド初期化子、constructor-initializer呼び出し、本体の順に発行されます。

どちらの場合も、他の場所を探す必要はありません。したがって、フィールド初期化子を配置する場所を決定できないということではありません。現在のコンストラクターのみを考慮するいくつかの単純な規則に従っているだけです。

于 2013-05-20T08:38:02.127 に答える
0

これは許可されていると思います。これは、例外をキャッチして意味のあることを行うことができる (可能である) ためです。

初期化は実行されず、ほぼ確実に StackOverflowException がスローされます。しかし、これはまだ望ましい動作である可能性があり、必ずしもプロセスがクラッシュすることを意味するわけではありません。

ここで説明したようにhttps://stackoverflow.com/a/1599236/869482

于 2013-05-22T12:31:05.850 に答える