Почему скобки конструктора инициализатора объекта C # 3.0 необязательны?

114

Кажется, что синтаксис инициализатора объекта C # 3.0 позволяет исключить пару круглых скобок открытия / закрытия в конструкторе, когда существует конструктор без параметров. Пример:

var x = new XTypeName { PropA = value, PropB = value };

В отличие от:

var x = new XTypeName() { PropA = value, PropB = value };

Мне любопытно, почему пара скобок открытия / закрытия конструктора здесь необязательна XTypeName?

Джеймс Данн
источник
9
Кстати, мы обнаружили это в обзоре кода на прошлой неделе: var list = new List <Foo> {}; Если чем-то можно злоупотребить ...
blu
@blu Это одна из причин, по которой я хотел задать этот вопрос. Я заметил несогласованность в нашем коде. Непоследовательность в целом меня беспокоит, поэтому я подумал, что посмотрю, есть ли хорошее обоснование необязательности в синтаксисе. :)
Джеймс Данн

Ответы:

143

Этот вопрос был темой моего блога 20 сентября 2010 года . Ответы Джоша и Чада («они не добавляют ценности, зачем они нужны?» И «для устранения избыточности») в основном верны. Чтобы конкретизировать это немного подробнее:

Возможность исключать список аргументов как часть «более крупной функции» инициализаторов объектов соответствовала нашей планке для «слащавых» функций. Некоторые моменты мы рассмотрели:

  • стоимость дизайна и спецификации была низкой
  • в любом случае мы собирались существенно изменить код парсера, который обрабатывает создание объектов; дополнительные затраты на разработку, чтобы сделать список параметров необязательным, были невелики по сравнению со стоимостью более крупной функции
  • бремя тестирования было относительно небольшим по сравнению со стоимостью более крупной функции
  • объем документации был относительно небольшим по сравнению с ...
  • Ожидалось, что затраты на техническое обслуживание будут небольшими; Я не припомню, чтобы в этой функции сообщалось об ошибках за годы, прошедшие с момента ее выпуска.
  • эта функция не представляет сразу очевидных рисков для будущих функций в этой области. (Последнее, что мы хотим сделать, это сделать дешевую и простую функцию сейчас, которая значительно усложнит реализацию более привлекательной функции в будущем.)
  • эта функция не добавляет новых двусмысленностей в лексический, грамматический или семантический анализ языка. Это не создает проблем для своего рода "частичного анализа программы", который выполняется механизмом "IntelliSense" среды IDE во время набора текста. И так далее.
  • эта функция попадает в общую «золотую середину» для функции инициализации более крупного объекта; обычно, если вы используете инициализатор объекта, это происходит именно потому, что конструктор объекта не позволяет вам устанавливать желаемые свойства. Очень часто такие объекты являются просто «сумками свойств», у которых вообще нет параметров в ctor.

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

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

class P
{
    class B
    {
        public class M { }
    }
    class C : B
    {
        new public void M(){}
    }
    static void Main()
    {
        new C().M(); // 1
        new C.M();   // 2
    }
}

Строка 1 создает новый C, вызывает конструктор по умолчанию, а затем вызывает метод экземпляра M для нового объекта. Строка 2 создает новый экземпляр BM и вызывает его конструктор по умолчанию. Если бы круглые скобки в строке 1 были необязательными, тогда строка 2 была бы неоднозначной. Тогда мы должны были бы придумать правило, разрешающее двусмысленность; мы не могли сделать это ошибкой потому что это было бы критическим изменением, которое превращает существующую легальную программу C # в неработающую программу.

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

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

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

Как вы определили эту конкретную двусмысленность?

Это было сразу ясно; Я хорошо знаком с правилами C # для определения того, когда ожидается имя, разделенное точками.

При рассмотрении новой функции как определить, вызывает ли она какую-либо двусмысленность? Вручную, формальные доказательства, машинный анализ - что?

Все трое. В основном мы просто смотрим на спецификации и лапшу, как я сделал выше. Например, предположим, что мы хотим добавить в C # новый префиксный оператор под названием «frob»:

x = frob 123 + 456;

(ОБНОВЛЕНИЕ: frobконечно await; анализ здесь - это, по сути, анализ, который команда разработчиков прошла при добавленииawait .)

«frob» здесь похоже на «новый» или «++» - оно стоит перед каким-либо выражением. Мы определяли желаемый приоритет, ассоциативность и т. Д., А затем начинали задавать такие вопросы, как «что, если в программе уже есть тип, поле, свойство, событие, метод, константа или локальный объект с именем frob?» Это немедленно привело бы к таким случаям, как:

frob x = 10;

Означает ли это, что «выполнить операцию frob над результатом x = 10 или создать переменную типа frob с именем x и присвоить ей 10?» (Или, если frobbing создает переменную, это может быть присвоение 10. В frob xконце концов, *x = 10;анализирует и допустимо, если xесть int*.)

G(frob + x)

Означает ли это «заморозить результат унарного оператора плюса над x» или «добавить выражение frob к x»?

И так далее. Чтобы устранить эту двусмысленность, мы можем ввести эвристику. Когда вы говорите «var x = 10;» это неоднозначно; это может означать «вывести тип x» или «x имеет тип var». Итак, у нас есть эвристика: мы сначала пытаемся найти тип с именем var, и только если он не существует, мы делаем вывод о типе x.

Или мы можем изменить синтаксис, чтобы он не был неоднозначным. Когда они разрабатывали C # 2.0, у них была такая проблема:

yield(x);

Означает ли это «yield x в итераторе» или «вызов метода yield с аргументом x»? Изменив его на

yield return(x);

это сейчас однозначно.

В случае необязательных скобок в инициализаторе объекта легко понять, есть ли введенная двусмысленность или нет, потому что количество ситуаций, в которых допустимо вводить что-то, что начинается с {, очень мало . По сути, это просто различные контексты операторов, лямбда-выражения операторов, инициализаторы массивов и все такое. Легко рассуждать по всем случаям и показать, что нет двусмысленности. Обеспечить работоспособность IDE несколько сложнее, но это можно сделать без особых проблем.

Такого рода возни со спецификацией обычно бывает достаточно. Если это особенно сложная функция, мы выбираем более тяжелые инструменты. Например, при разработке LINQ один из разработчиков компилятора и один из разработчиков IDE, имеющих опыт работы в теории синтаксического анализатора, создали себе генератор синтаксического анализатора, который мог бы анализировать грамматики на предмет неоднозначности, а затем загружать в него предлагаемые грамматики C # для понимания запросов. ; таким образом было обнаружено много случаев, когда запросы были неоднозначными.

Или, когда мы выполняли расширенный вывод типов на лямбдах в C # 3.0, мы писали наши предложения, а затем отправляли их через пруд в Microsoft Research в Кембридже, где языковая группа была достаточно хороша, чтобы разработать формальное доказательство того, что предложение вывода типов было теоретически здравый.

Есть ли сегодня двусмысленность в C #?

Конечно.

G(F<A, B>(0))

В C # 1 ясно, что это значит. Это то же самое, что:

G( (F<A), (B>0) )

То есть он вызывает G с двумя аргументами типа bools. В C # 2 это могло означать то же, что и в C # 1, но это также могло означать «передать 0 универсальному методу F, который принимает параметры типа A и B, а затем передать результат F в G». Мы добавили в синтаксический анализатор сложную эвристику, которая определяет, какой из двух случаев вы, вероятно, имели в виду.

Точно так же приведения неоднозначны даже в C # 1.0:

G((T)-x)

Это «приведение -x к T» или «вычесть x из T»? Опять же, у нас есть эвристика, которая делает хорошее предположение.

Эрик Липперт
источник
3
Ой, извините, я забыл ... Подход с сигналом летучей мыши, хотя он, кажется, работает, предпочтительнее (ИМО) прямого средства связи, при котором никто не получит публичное освещение, необходимое для общественного образования в форме сообщения SO, которое индексируется, доступен для поиска и на него легко ссылаться. Должны ли мы вместо этого связаться напрямую, чтобы поставить постановочный танец SO post / answer? :)
Джеймс Данн
5
Я бы порекомендовал вам избегать постановки постов. Это было бы несправедливо по отношению к другим, кто может иметь дополнительное понимание вопроса. Лучшим подходом было бы опубликовать вопрос, а затем отправить ссылку на него по электронной почте с просьбой об участии.
chilltemp
1
@ Джеймс: Я обновил свой ответ, чтобы ответить на ваш следующий вопрос.
Эрик Липперт
8
@ Эрик, можно ли написать в блоге об этом списке «никогда не делай этого»? Мне любопытно увидеть другие примеры, которые никогда не будут частью языка C # :)
Илья Рыженков
2
@Eric: Я действительно очень ценю ваше терпение со мной :) Спасибо! Очень информативно.
Джеймс Данн
12

Потому что так был указан язык. Они не добавляют ценности, так зачем их включать?

Это также очень похоже на массивы с неявной типизацией

var a = new[] { 1, 10, 100, 1000 };            // int[]
var b = new[] { 1, 1.5, 2, 2.5 };            // double[]
var c = new[] { "hello", null, "world" };      // string[]
var d = new[] { 1, "one", 2, "two" };         // Error

Ссылка: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx

CaffGeek
источник
1
Они не добавляют ценности в том смысле, что должно быть очевидно, каково их намерение, но это нарушает последовательность в том, что теперь у нас есть два несопоставимых синтаксиса построения объектов, один с обязательными скобками (и выражениями аргументов с разделителями-запятыми) и один без .
Джеймс Данн,
1
@James Dunne, на самом деле это очень похоже на синтаксис неявно типизированного синтаксиса массива, см. Мою правку. Нет ни типа, ни конструктора, и цель очевидна, поэтому нет необходимости объявлять ее
CaffGeek
7

Это было сделано для упрощения конструкции объектов. Разработчики языка (насколько мне известно) конкретно не сказали, почему они считают, что это было полезно, хотя это явно упоминается на странице спецификации C # версии 3.0 :

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

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

Рид Копси
источник
4

В вашем первом примере компилятор делает вывод, что вы вызываете конструктор по умолчанию (спецификация языка C # 3.0 гласит, что если скобки не указаны, вызывается конструктор по умолчанию).

Во втором вы явно вызываете конструктор по умолчанию.

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

public class SomeTest
{
    public string Value { get; private set; }
    public string AnotherValue { get; set; }
    public string YetAnotherValue { get; set;}

    public SomeTest() { }

    public SomeTest(string value)
    {
        Value = value;
    }
}

Все три утверждения действительны:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};
Джастин Нисснер
источник
Правильно. В первом и втором случае в вашем примере они функционально идентичны, верно?
Джеймс Данн,
1
@ Джеймс Данн - Верно. Это часть, указанная в спецификации языка. Пустые скобки излишни, но вы все равно можете их указать.
Джастин Нисснер,
1

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

Джош
источник
Да, это избыточно, но мне просто любопытно, почему их внезапное введение необязательно? Кажется, это нарушает целостность синтаксиса языка. Если бы у меня не было открытой фигурной скобки для обозначения блока инициализатора, это должно быть недопустимым синтаксисом. Забавно, что вы упомянули мистера Липперта, я вроде как публично выискивал его ответ, чтобы мне и другим было выгодно праздное любопытство. :)
Джеймс Данн