1. ホーム
  2. c#

[解決済み] なぜ再帰的なコンストラクタ呼び出しは、無効なC#コードをコンパイルするのでしょうか?

2023-08-11 15:16:11

質問

ウェビナー視聴後 Jon Skeetが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はWebinarでこの情報に言及しました)。

このため、これらの質問はより興味深いものとなっています。Java コミュニティに敬意を表しつつ、C# コンパイラーは 少なくとも より現代的です。

これをコンパイルするには C# 4.0 C# 5.0 コンパイラを使用し、デコンパイルは ドットピーク .

どのように解決するのですか?

面白い発見がありました。

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

  1. 別のインスタンス コンストラクタをチェーンするインスタンス コンストラクタ。 を連鎖させます。 で、そのインスタンスを : this( ...) の構文を使用します。
  2. インスタンスコンストラクタをチェーンするインスタンスコンストラクタ を連鎖させるインスタンスコンストラクタです。 . これは鎖が指定されていないインスタンスコンストラクタも含みます。 : base() がデフォルトであるためです。

(のインスタンスコンストラクタは無視しました)。 System.Object というのは特殊なケースで System.Object には基底クラスがありません! しかし System.Object にはフィールドもありません)。

クラス内に存在するインスタンスフィールドのイニシャライザは、以下の型のすべてのインスタンスコンストラクタのボディの最初にコピーされる必要があります。 2. のインスタンスコンストラクタはありません。 1. は、フィールドの割り当てコードが必要です。

のコンストラクタを解析する必要はないようです。 1. を使用して、サイクルがあるかどうかを確認します。

さて、あなたの例では、次のような状況を与えています。 すべて インスタンスコンストラクタが 1. . この状況では、フィールドのイニシャライザコードはどこにも置く必要がありません。ということで、あまり深く解析されていないようです。

これは、すべてのインスタンスコンストラクタが型 1. のように、アクセス可能なコンストラクタを持たない基底クラスから派生させることも可能です。ただし、ベースクラスは非シール型である必要があります。例えば、もしあなたが private のインスタンスコンストラクタしかないクラスを書いたとしても、 派生されたクラスのインスタンスコンストラクタをすべて 1. のようになります。しかし、新しいオブジェクトの作成式は、当然ながら決して終了しません。派生クラスのインスタンスを作成するには、"cheat" のようなものを使う必要があります。 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 detected と表示し、IL に決して変換されないフィールド初期化子の行と列番号を指摘することができます。