再帰的なコンストラクター呼び出しによって無効なC#コードがコンパイルされるのはなぜですか?


82

ウェビナーのJonSkeet 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を使用して逆コンパイルされました


3
どのように彼**は私がこのビデオを逃したのですか?
Royi Namir 2013

7
すばらしい質問です。
デニス

2
そこにある素晴らしいフィールド初期化子int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed();:「これらのフィールド宣言はどのような状況で問題ありませんか?」というクイズを作成する必要があります。(使用されていないフィールドに関する警告がありますが、インスタンスコンストラクターの1つ(または他の場所)の本体内の各フィールドを読み取ることで、その警告を取り除くことができます。)
Jeppe Stig Nielsen

4
同じ理由でこれは許されると思います。
GSerg 2013年

4
フィールドの初期化は、基本コンストラクターを呼び出すすべてのコンストラクターに組み込まれます。結果として、基本コンストラクターを呼び出すコンストラクターがない場合、フィールドの初期化はどこにも配置されません。少なくともその部分は私には完全に理にかなっています。コンパイラがどこに配置するかわからないわけはありません。コンパイラは、どこに配置する必要もないことに気付いたからです。

回答:


11

興味深い発見。

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

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

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に変換されないフィールド初期化子の行番号と列番号を指定して指定できます。


1
わかりません... ctorの前にフィールドのインスタンス化が呼び出されませんか?
Royi Namir 2013

2
@RoyiNamirはい。しかし、ILを見ると、質問者が次のように書いているように機能します。「おそらくご存知のとおり、フィールドの初期化はコンパイラーによってコンストラクターに移動されます。」つまり、このクラスをC#で記述したとするとclass Example { int field = 42; internal Example() { /* some code here */ field = 100; } }、それによって生成されたILは、42他のすべての前に、次のように記述した場合とまったく同じように、割り当てをインスタンスコンストラクターに配置しますclass Example { int field; internal Example() { field = 42; /* some code here */ field = 100; } }
。– Jeppe Stig Nielsen 2013

5

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

10.11.1から:

すべてのインスタンスコンストラクター(クラスのものを除くobject)には、コンストラクター本体の直前に別のインスタンスコンストラクターの呼び出しが暗黙的に含まれます。暗黙的に呼び出すコンストラクターは、コンストラクター初期化子によって決定されます

..。

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

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

Foo() : this() {}

違法です。


しかし、私は認めます-それを許可する具体的な理由はわかりません。もちろん、ILレベルでは、実行時に異なるインスタンスコンストラクターを選択できるため、このようなコンストラクトは許可されます。したがって、終了した場合は再帰を使用できます。


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

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

  • 場合はconstructor-initializer、クラス自体のインスタンスコンストラクタで、それはフィールドの初期化子を排出しない-それが発するconstructor-initializerコールして、身体を。

  • 場合はconstructor-initializer、直接、基本クラスのインスタンスコンストラクタで、それはその後、フィールド初期化子を発するconstructor-initializer身体、その後、呼び出し、および。

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


2
しかし、このような行をコンパイルできるという事実はどうでしょうかint e = Invalid<Method>Name<<Indeeed();。これはコンパイラのバグだと思います。
マシューワトソン

@MatthewWatsonこれはint e = Invalid < Method > Name << Indeed();、「より小さい」、「より大きい」、および「左シフト」の2項演算子と解釈される場合があります。これは構文的には問題ありませんが、強い型付けで問題がないようにするには、演算子が非常に過負荷になる可能性があります。
Jeppe Stig Nielsen

1
@JeppeStigNielsen Ayeですが、再帰的なコンストラクターコードを削除する以外はコードを同じままにしておくと、コンパイルされません。だからバグだと思います。
マシューワトソン

4
@MatthewWatsonクラスが不完全であるため、解析時にエラーを検出できません。(たぶんあなたのクラスはInvalidそれを有効にするetcと呼ばれるメンバーを定義するでしょう。)エラーは通常コード生成時に検出されますが、決して生成されないコードを書く方法を見つけました。コンパイラに卑劣な穴(決してコンパイルされないコードを書く方法)を見つけましたが、問題のコードはとにかく到達できないため、深刻な穴ではありません。
レイモンドチェン

2

あなたの例

class Foo
{
    int a = 42;

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

そのFooオブジェクトを問題なくインスタンス化できるという意味で、正常に動作します。ただし、以下はあなたが求めているコードに似ています

class Foo
{
    int a = 42;

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

再帰が底を打つことは決してないので、それとあなたのコードの両方がスタックオーバーフロー(!)を作成します。したがって、コードは実行されないため、無視されます。

言い換えると、コンパイラーは、再帰が決して底を打たないことを伝えることができるため、障害のあるコードをどこに置くかを決定できません。これは、一度だけ呼び出される場所に配置する必要があるためだと思いますが、コンストラクターの再帰的な性質により、それは不可能です。

コンストラクターの本体にそれ自体のインスタンスを作成するコンストラクターの意味での再帰は、私には理にかなっています。たとえば、各ノードが他のノードを指すツリーをインスタンス化するために使用できるためです。しかし、この質問で示されている種類のプレコンストラクターを介した再帰は、底を打つことはできないので、それが許可されていない場合は私にとって理にかなっています。


1
はい、同意します。それが私がこの質問を作成した理由です。コンパイラが初期化ロジックを配置する場所を決定できないのはなぜですか。したがって、再帰呼び出しをまったく許可しないのはなぜですか。これには理由がありますか?
イリヤ・イワノフ

再帰が決して底を打たないことがわかるので、コンパイラが障害のあるコードをどこに置くかを決定できないことは私には明らかなようです。なぜそれが謎なのですか?
確率的に

C#が呼び出すメソッドを決定できない場合、エラーがスローされ、ambiguous method callそのようなメソッド呼び出しはスキップされません。私がコンパイラーである場合、このシナリオでもエラーをスローします。
イリヤ・イワノフ

1
悪いことに、回答が非常に多くの反対票を受け取っているので、私はそれらのどれにも反対票を投じていません(ちょうどそうです)。このシナリオでは、初期化ロジックを配置する場所を決定することもできません。だから私の主な質問は、なぜ再帰呼び出しを許可するのですか?これには理由がありますか?多分私は何かが足りない
イリヤイワノフ

3
@ IlyaIvanov-より適切な質問は-コンパイラーで再帰的なコンストラクター呼び出しを検出するためにサイクル検出器を作成する理由です。
damien_The_Unbeliever 2013年

0

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

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

ここで説明されているようにhttps://stackoverflow.com/a/1599236/869482

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.