После просмотра веб-семинара Джон Скит инспектирует 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
только в конструкторе по умолчанию.
Чтобы проиллюстрировать второй случай, следующий код:
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 .
int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed();
надо провести викторину: «В какой ситуации эти объявления полей подходят?» (Есть предупреждение о том, что поля не используются, но вы можете избавиться от этого предупреждения, прочитав каждое поле внутри тела одного из конструкторов intance (или где-либо еще).)Ответы:
Интересная находка.
Похоже, что на самом деле существует только два типа конструкторов экземпляров:
: this( ...)
синтаксисом.: 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
этого метода.Наконец, что касается
синтаксис. Согласно правилам C # это следует читать как
потому что оператор сдвига влево
<<
имеет более высокий приоритет, чем оператор «меньше»<
и «больше»>
. Последние два оператора имеют одинаковый приоритет и поэтому оцениваются по левоассоциативному правилу. Если бы типы былиMySpecialType Invalid; int Method; int Name; int Indeed() { ... }
и если
MySpecialType
введена(MySpecialType, int)
перегрузкаoperator <
, то выражениебудет законным и значимым.
На мой взгляд, в этом случае было бы лучше, если бы компилятор выдавал предупреждение. Например, он может сказать
unreachable code detected
и указать номер строки и столбца инициализатора поля, который никогда не переводится в IL.источник
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; } }
Я думаю, потому что спецификация языка исключает только прямой вызов того же конструктора, который определяется.
С 10.11.1:
...
Последнее предложение, похоже, только исключает прямой вызов самого себя как порождающего ошибку времени компиляции, например
Foo() : this() {}
незаконно.
Признаюсь, я не вижу конкретной причины для этого. Конечно, на уровне IL такие конструкции разрешены, потому что я считаю, что во время выполнения могут быть выбраны разные конструкторы экземпляров - так что вы можете иметь рекурсию, если она завершается.
Я думаю, что другая причина, по которой он не сигнализирует и не предупреждает об этом, заключается в том, что ему не нужно обнаруживать эту ситуацию. Представьте себе , чеканку через сотню различных конструкторов, просто чтобы увидеть , если цикл делает существует - при любой попытке использования быстро (как мы знаем) взрывает во время выполнения, для довольно краев корпусов.
Когда он генерирует код для каждого конструктора, он учитывает
constructor-initializer
только инициализаторы полей и тело конструктора - он не учитывает никакой другой код:Если
constructor-initializer
это конструктор экземпляра для самого класса, он не генерирует инициализаторы поля - он испускаетconstructor-initializer
вызов, а затем тело.Если
constructor-initializer
это конструктор экземпляра для прямого базового класса, он испускает инициализаторы поля, затемconstructor-initializer
вызов, а затем тело.Ни в том, ни в другом случае ему не нужно искать в другом месте - так что это не тот случай, когда он «не может» решить, где разместить инициализаторы поля - он просто следует некоторым простым правилам, которые учитывают только текущий конструктор.
источник
int e = Invalid<Method>Name<<Indeeed();
. Я говорю, что это ошибка компилятора.int e = Invalid < Method > Name << Indeed();
бинарные операторы «меньше», «больше» и «сдвиг влево». С синтаксической точки зрения это нормально, но было бы действительно сумасшедшим перегрузом операторов, чтобы сделать это нормально со строгой типизацией.Invalid
т. Д., Которые сделают его действительным.) Ошибка обычно обнаруживается при генерации кода, но вы нашли способ написать код, который никогда не генерируется. Вы обнаружили скрытую дыру в компиляторе (способ написать код, который никогда не будет компилироваться), но не серьезную, поскольку вредоносный код в любом случае недоступен.Ваш пример
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 (!), Потому что рекурсия никогда не завершится. Таким образом, ваш код игнорируется, потому что он никогда не запускается.
Другими словами, компилятор не может решить, куда поместить ошибочный код, потому что он может сказать, что рекурсия никогда не завершится. Я думаю, это потому, что он должен поместить его туда, где он будет вызываться только один раз, но рекурсивный характер конструкторов делает это невозможным.
Рекурсия в том смысле, что конструктор создает экземпляры самого себя в теле конструктора, имеет для меня смысл, потому что, например, это может быть использовано для создания экземпляров деревьев, где каждый узел указывает на другие узлы. Но рекурсия с помощью предварительных конструкторов, проиллюстрированных этим вопросом, никогда не может достичь дна, поэтому для меня это имело бы смысл, если бы это было запрещено.
источник
ambiguous method call
, он не пропускает такой вызов метода. Если бы я был компилятором, я бы тоже выдал ошибку в этом сценарии.Я думаю, что это разрешено, потому что вы все еще можете (могли) поймать исключение и сделать с ним что-то значимое.
Инициализация никогда не будет запущена, и она почти наверняка вызовет исключение StackOverflowException. Но это все еще может быть желаемым поведением и не всегда означает, что процесс должен завершиться сбоем.
Как описано здесь https://stackoverflow.com/a/1599236/869482
источник