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


после просмотра вебинара Джон Скит Осматривает Решарпер, я начал немного играть с рекурсивный конструктор вызывает и находит, что следующий код является допустимым кодом 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 только в конструкторе по умолчанию.

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

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, компилятор C# - это по крайней мере более современные.

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

4 80
c#

4 ответа:

интересные найти.

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

  1. конструктор экземпляра, который связывает другой конструктор экземпляра того же типа С : this( ...) синтаксис.
  2. конструктор экземпляра, который связывает конструктор экземпляра базового класса. Это включает в себя конструкторы экземпляров, где не указан chainig, так как : base() - это по умолчанию.

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

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

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

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

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

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

наконец, в отношении

Invalid<Method>Name<<Indeeed()

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

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

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

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

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

Invalid < Method > Name << Indeeed()

было бы законно и многозначительный.


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

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

от 10.11.1:

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

...

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

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

Foo() : this() {}

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


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


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

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

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

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

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

ваш пример

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 (!), потому что рекурсия никогда не достигает дна. Поэтому ваш код игнорируется, потому что он никогда не выполняется.

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

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

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

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

как объяснено здесь https://stackoverflow.com/a/1599236/869482