Этот вопрос был темой моего блога 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»? Опять же, у нас есть эвристика, которая делает хорошее предположение.