Чому рекурсивний виклик конструктора робить недійсним компіляцію коду C #?


82

Переглянувши веб-семінар Jon Skeet 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лише в конструкторі за замовчуванням.

Для ілюстрації другого випадку, наступний код:

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 .


3
Як він ** я пропустив це відео ???
Рої Намір

7
Відмінне запитання.
Денніс

2
Там хороші ініціалізатори полів: int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed();Слід зробити вікторину: "У якій ситуації ці декларації полів в порядку?" (Є попередження про те, що поля не використовуються, але ви можете позбутися цього попередження, прочитавши кожне поле в тілі одного з конструкторів екземплярів (або деінде).)
Jeppe Stig Nielsen,

4
Я вважаю, що це дозволено з тієї ж причини .
GSerg

4
Ініціалізація поля поміщається у всі конструктори, які викликають базовий конструктор. Як наслідок, якщо немає конструктора, який викликає базовий конструктор, ініціалізація поля нікуди не потрапляє. Принаймні ця частина для мене цілком зрозуміла. Справа не в тому, що компілятор не може зрозуміти, куди його покласти, це тому, що компілятор помічає, що його не потрібно нікуди розміщувати.

Відповіді:


11

Цікава знахідка.

Здається, насправді існує лише два типи конструкторів екземплярів:

  1. Конструктор екземпляра, який зв’язує інший конструктор екземпляра того ж типу із : this( ...)синтаксисом.
  2. Конструктор примірника, який ланцюжок конструктора примірника базового класу . Сюди входять конструктори екземплярів, де не вказаний ланцюговий код, оскільки : 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цієї техніки.

Нарешті, щодо

Invalid<Method>Name<<Indeeed()

синтаксис. Згідно з правилами C #, це слід читати як

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

оскільки оператор зсуву вліво <<має вищий пріоритет, ніж оператор менше, ніж оператор <більше, ніж оператор >. Два останні оператори мають однаковий пріоритет, і тому оцінюються за ліво-асоціативним правилом. Якби типи були

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

а якщо MySpecialTypeвведено (MySpecialType, int)перевантаження operator <, то вираз

Invalid < Method > Name << Indeeed()

було б законним та значущим.


На мою думку, було б краще, якби компілятор видав попередження за цим сценарієм. Наприклад, він міг би говорити unreachable code detectedі вказувати на номер рядка та стовпця ініціалізатора поля, який ніколи не перекладається в IL.


1
Я не розумію ... чи не викликається екземпляр поля перед ctor?
Рої Намір,

2
@RoyiNamir Так. Але якщо ви подивитесь на ІЛ, це працює так, як пише запитувач: "Як ми всі, мабуть, знаємо, ініціалізація поля переміщується в конструктор компілятором". Що мається на увазі під цим, припустимо, що ви пишете цей клас на C #:, class Example { int field = 42; internal Example() { /* some code here */ field = 100; } }тоді створений цим ІЛ поміщає 42присвоєння в конструктор екземпляру перед усім іншим, точно так, як ніби ви писали:class Example { int field; internal Example() { field = 42; /* some code here */ field = 100; } }
Джеппе Стіг Нільсен,

5

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

З 10.11.1:

Усі конструктори екземплярів (за винятком класу object) неявно включають виклик іншого конструктора екземпляра безпосередньо перед тілом конструктора. Конструктор для неявного виклику визначається конструктором-ініціалізатором

...

  • Ініціалізатор конструктора екземпляра форми викликає виклик конструктора екземпляра із самого класу ... Якщо оголошення конструктора екземпляра включає ініціалізатор конструктора, який викликає сам конструктор, виникає помилка під час компіляціїthis(argument-listopt)

Здається, це останнє речення виключає лише прямий виклик, що спричиняє помилку під час компіляції, наприклад

Foo() : this() {}

є незаконним.


Хоча я визнаю - я не бачу конкретної причини для дозволу. Звичайно, на рівні IL такі конструкції дозволені, оскільки я вважаю, що різні конструктори екземплярів можуть бути обрані під час виконання, щоб ви могли мати рекурсію за умови її закінчення.


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

Коли він виконує генерацію коду для кожного конструктора, все, що він враховує constructor-initializer, це ініціалізатори полів і тіло конструктора - він не враховує жодного іншого коду:

  • Якщо constructor-initializerє конструктором екземпляра для самого класу, він не видає ініціалізаторів полів - він випромінює constructor-initializerвиклик, а потім тіло.

  • Якщо constructor-initializerє конструктором екземпляра для прямого базового класу, він випромінює ініціалізатори полів, потім constructor-initializerвиклик, а потім тіло.

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


2
Але як щодо того , що вона дозволяє лінію , як ця компіляції: int e = Invalid<Method>Name<<Indeeed();. Я кажу, що це помилка компілятора.
Метью Уотсон,

@MatthewWatson Це може бути інтерпретовано як int e = Invalid < Method > Name << Indeed();із двійковими операторами "менше", "більше" та "зсув уліво". Це синтаксично нормально, але це було б по-справжньому божевільним перевантаженням операторів, щоб зробити це нормально при сильному наборі тексту.
Jeppe Stig Nielsen

1
@JeppeStigNielsen Так, але він не компілюється, якщо залишити код таким самим, крім видалення рекурсивного коду конструктора. Тому я думаю, що це помилка.
Метью Уотсон,

4
@MatthewWatson Помилку неможливо виявити під час розбору, оскільки клас неповний. (Можливо, ваш клас визначить членів, що викликаються Invalidі т.д., що зробить його дійсним.) Помилка зазвичай виявляється при генерації коду, але ви знайшли спосіб написати код, який ніколи не генерується. Ви знайшли підступну діру в компіляторі (спосіб написання коду, який ніколи не буде скомпільовано), але не серйозний, оскільки образливий код і так недосяжний.
Raymond Chen

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() { }
}

І це, і ваш код створить потік stackoverflow (!), Оскільки рекурсія ніколи не знижується. Отже, ваш код ігнорується, оскільки він ніколи не виконується.

Іншими словами, компілятор не може вирішити, куди помістити несправний код, оскільки він може сказати, що рекурсія ніколи не знижується. Я думаю, це тому, що він повинен поставити його там, де його буде викликано лише один раз, але рекурсивний характер конструкторів робить це неможливим.

Рекурсія у сенсі конструктора, який створює екземпляри себе в тілі конструктора, для мене має сенс, оскільки, наприклад, це може бути використано для створення екземплярів дерев, де кожен вузол вказує на інші вузли. Але рекурсія за допомогою попередніх конструкторів типу, проілюстрованого цим питанням, ніколи не може знизитися, тому для мене було б сенсом, якби це було заборонено.


1
Так, я погоджуюсь, саме тому я створив це питання. Чому компілятор не може вирішити, куди помістити логіку ініціалізації, а отже, чому він взагалі дозволяє рекурсивні виклики? Чи є для цього причина?
Ілля Іванов

Мені здається зрозумілим, що компілятор не може вирішити, куди помістити несправний код, оскільки він може сказати, що рекурсія ніколи не знижується. Чому це загадка?
Стохастично

Якщо C # не може вирішити, який метод викликати, він видає помилку ambiguous method callі не пропускає такий виклик методу. Якби я був компілятором, я б також видав помилку в цьому сценарії.
Ілля Іванов

1
це погано, що відповіді отримують стільки голосів проти, я не голосую проти жодного з них (просто так). У цьому випадку він також не може вирішити, куди помістити логіку ініціалізації. Тож моє головне питання - навіщо взагалі дозволяти рекурсивні дзвінки? Чи є за цим причина? Можливо, мені чогось не вистачає
Ілля Іванов

3
@IlyaIvanov - Я думаю, що більш доречним є питання - навіщо писати детектор циклу для виявлення рекурсивних викликів конструктора в компіляторі?
Damien_The_Unbeliever

0

Я думаю, що це дозволено, оскільки ви все ще можете (могли) зловити виняток і зробити з ним щось значуще.

Ініціалізація ніколи не буде запущена, і вона майже напевно викине StackOverflowException. Але це все-таки може бути бажаною поведінкою, і не завжди означає, що процес повинен розбитися.

Як пояснено тут https://stackoverflow.com/a/1599236/869482

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.