Почему вызов рекурсивного конструктора делает недопустимым компиляцию кода С#?

После просмотра веб-семинара Jon Skeet Inspects ReSharper, я начал немного играть с рекурсивный вызов конструктора и нашел, что следующий код является допустимым кодом С# (по-моему, это означает, что он компилируется).

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 в конструкторах all. Но если у вас есть конструктор, вызывающий другой конструктор, у вас будет код инициализации только в вызываемом.

Например, если у вас есть конструктор с параметрами, вызывающими конструктор по умолчанию, у вас будет назначение a = 42 только в конструкторе по умолчанию.

Чтобы проиллюстрировать второй случай, следующий код:

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.

У меня есть два вопроса:

Почему компилятор разрешает рекурсивные вызовы конструктора вообще?

Почему мы наблюдаем такое поведение компилятора для полей, инициализированных в этом классе?


Некоторые примечания: ReSharper предупреждает вас с помощью Possible cyclic constructor calls. Более того, в Java такие вызовы конструктора не будут компилировать события, поэтому компилятор Java более строгий в этом сценарии (Джон упомянул эту информацию на веб-семинаре).

Это делает эти вопросы более интересными, потому что со всем уважением к сообществу Java компилятор С#, по крайней мере, более современен.

Это было скомпилировано с помощью С# 4.0 и С# 5.0 и декомпилировано с помощью dotPeek.

Ответ 1

Интересная находка.

Похоже, что существуют только два типа конструкторов экземпляров:

  • Конструктор экземпляра, который связывает другой конструктор экземпляра того же типа с синтаксисом : this( ...).
  • Конструктор экземпляра, который связывает конструктор экземпляра базового класса. Сюда входят конструкторы экземпляров, в которых не указано ничья, поскольку : base() является значением по умолчанию.

(я проигнорировал конструктор экземпляра System.Object, который является частным случаем. System.Object не имеет базового класса! Но System.Object тоже не имеет полей.)

Инициализаторы полей экземпляра, которые могут присутствовать в классе, должны быть скопированы в начало тела всех конструкторов экземпляров типа 2. выше, тогда как конструкторы экземпляров типа 1. нужен код назначения поля.

Поэтому, видимо, нет необходимости компилятору С# выполнять анализ конструкторов типа 1., чтобы увидеть, есть ли циклы или нет.

Теперь ваш пример дает ситуацию, когда все конструкторы экземпляров имеют тип 1.. В этой ситуации код инициализатора поля не требуется нигде. Поэтому, по-видимому, он не анализируется очень глубоко.

Оказывается, когда все конструкторы экземпляров имеют тип 1., вы даже можете получить из базового класса, у которого нет доступного конструктора. Однако базовый класс должен быть не запечатан. Например, если вы пишете класс только с конструкторами экземпляров private, люди все равно могут получить свой класс, если они сделают все конструкторы экземпляров в производном классе типа 1. выше. Однако, конечно, новое выражение создания объекта никогда не закончится. Чтобы создать экземпляры производного класса, нужно было бы "обмануть" и использовать такие вещи, как метод System.Runtime.Serialization.FormatterServices.GetUninitializedObject.

Другой пример: класс System.Globalization.TextInfo имеет только конструктор экземпляра internal. Но вы все равно можете получить этот класс в сборке, отличной от mscorlib.dll с помощью этой техники.

Наконец, что касается

Invalid<Method>Name<<Indeeed()

синтаксис. Согласно правилам С#, это нужно читать как

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

потому что оператор с левым сдвигом << имеет более высокий приоритет, чем как меньший оператор <, так и оператор большего размера >. Последние два оператора имеют одинаковый приоритет и поэтому оцениваются лево-ассоциативным правилом. Если типы были

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

и если MySpecialType ввел перегрузку (MySpecialType, int) operator <, то выражение

Invalid < Method > Name << Indeeed()

будет законным и значимым.


На мой взгляд, было бы лучше, если бы компилятор выпустил предупреждение в этом сценарии. Например, он может сказать unreachable code detected и указать номер строки и столбца инициализатора поля, который никогда не переводится в IL.

Ответ 2

Я думаю, потому что спецификация языка запрещает прямо ссылаться на тот же самый конструктор, который определяется.

От 10.11.1:

Все конструкторы экземпляров (за исключением тех, что относятся к классу object) неявно включают вызов другого экземпляра экземпляра непосредственно перед куском конструктора. Конструктор для неявного вызова определяется конструктором-инициализатором

...

  • Инициализатор конструктора экземпляра формы this( argument-list opt) приводит к вызову конструктора экземпляра из самого класса... Если объявление конструктора экземпляра включает в себя инициализатор конструктора, который вызывает сам конструктор, возникает ошибка времени компиляции

Последнее предложение, по-видимому, только препятствует прямому вызову как порождающему ошибку времени компиляции, например

Foo() : this() {}

является незаконным.


Я признаю, что - я не вижу конкретной причины для его разрешения. Конечно, на уровне IL такие конструкции допускаются, потому что, по-моему, во время выполнения могут быть выбраны различные конструкторы экземпляров, поэтому вы можете иметь рекурсию при условии, что она завершится.


Я думаю, что другая причина, по которой он не указывает или не предупреждает об этом, состоит в том, что ему не нужно обнаруживать эту ситуацию. Представьте, что вы преследуете сотни различных конструкторов, просто чтобы убедиться, что цикл существует, - когда любое попытка использования будет быстро (как мы знаем) взорваться во время выполнения, для довольно кратного случая.

Когда он создает генерацию кода для каждого конструктора, все, что он считает, это constructor-initializer, инициализаторы полей и тело конструктора - он не учитывает какой-либо другой код:

  • Если constructor-initializer является конструктором экземпляра для самого класса, он не испускает инициализаторы поля - он выдает вызов constructor-initializer, а затем тело.

  • Если constructor-initializer является конструктором экземпляра для прямого базового класса, он испускает инициализаторы полей, затем вызов constructor-initializer, а затем тело.

В любом случае это не нужно искать в другом месте - так что это не случай, когда он "неспособен" решить, куда помещать инициализаторы поля - он просто следует за некоторыми простыми правилами, которые рассматривают только текущий конструктор.

Ответ 3

Ваш пример

class Foo
{
    int a = 42;

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

будет работать нормально, в том смысле, что вы можете создать экземпляр этого объекта Foo без проблем. Однако следующее будет больше похоже на код, который вы спрашиваете о

class Foo
{
    int a = 42;

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

Оба и ваш код создадут stackoverflow (!), потому что рекурсия никогда не заканчивается. Поэтому ваш код игнорируется, потому что он никогда не запускается.

Другими словами, компилятор не может решить, где поставить неисправный код, потому что он может сказать, что рекурсия никогда не заканчивается. Я думаю, что это потому, что оно должно быть там, где оно будет вызываться только один раз, но рекурсивный характер конструкторов делает невозможным.

Рекурсия в смысле конструктора, создающего экземпляры самого себя внутри тела конструктора, имеет смысл для меня, потому что, например, которые могут использоваться для создания экземпляров деревьев, где каждый node указывает на другие узлы. Но рекурсия через предконструкторы такого рода, проиллюстрированные этим вопросом, никогда не может быть снизу, поэтому для меня было бы разумно, если бы это было запрещено.

Ответ 4

Я думаю, что это разрешено, потому что вы можете (могли) по-прежнему захватывать Exception и делать что-то значимое с ним.

Инициализация никогда не будет запущена, и она почти наверняка вызовет исключение StackOverflowException. Но это все еще может быть разыскиваемым поведением и не всегда означает, что процесс должен потерпеть крах.

Как объясняется здесь fooobar.com/info/64409/...