Глупая последовательность глупа

Сегодняшний пост как обычно будет представлен в виде диалога.

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

Это хороший вопрос, но нельзя ли немного конкретики? Для начала стоит перечислить случаи, когда неявно типизированная локальная переменная должна, а иногда не должна использовать var .

Конечно. Неявно типизированная локальная переменная должна объявляться с var в следующих случаях:

 var x1 = whatever;
for(var x2 = whatever; ;) {}
using(var x3 = whatever) {}
foreach(var x4 in whatever) {}

И неявно типизированная локальная переменная не должна объявляться с var в следующих случаях:

 from c in customers select c.Name
customers.Select(c => c.Name)

В обоих случаях добавить var перед переменной « c » нельзя, хотя можно использовать тип явно:

 from Customer c in customers select c.Name
customers.Select((Customer c) => c.Name)

Почему?

Можно, прежде чем отвечать на ваш вопрос, я еще немного его покритикую. Действительно ли в лямбда-выражении и в выражении запроса (query expression) используются неявно типизированные переменные?

Хм... Вы правы; технически, ни в одном из этих случаях нет локальных переменных. В случае лямбда-выражения используется формальный параметр. Но формальный параметр ведет себя практически аналогично локальной переменной, так что мы можем рассматривать ее как локальную переменную. В выражении запроса компилятор преобразует переменную диапазона ( range variable) в формальный параметр лямбда-выражения без типа независимо от того, является ли переменная диапазона типизированной или нет.

Не могли бы вы подробнее остановиться на последнем замечании?

Конечно . Когда мы пишем :

 from Customer c in customers select c.Name

компилятор преобразовывает это выражение в:

 customers.Select((Customer c) => c.Name)

Более того, он скорее преобразовывает его в:

 customers.Cast<Customer>().Select(c => c.Name)

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

Хорошо; суть здесь заключается в том, что независимо от наличия типа в выражении запроса, лямбда-выражение в преобразованном коде будет содержать формальный параметр без типа.

Теперь, когда мы прояснили ваш вопрос, давайте выясним, в чем же он заключается?

Язык C# непоследователен; var требуется для неявно типизированных локальных переменных (независимо от места объявления), но var нельзя использовать для неявно типизированных параметров лямбда-выражения (независимо от того, является ли параметр «настоящим» или же он является результатом преобразования выражения запроса). Почему?

Вы продолжаете спрашивать «Почему?», потому что он, на самом деле, является завуалированной версией вопроса «А почему не по-другому?». Т.е. на самом деле вы имеете в виду, «У меня есть представление о дизайне языков программирования; почему ваш дизайн ему не соответствует? » Но поскольку я не знаю вашего представления о дизайне языков программирования, мне тяжело сравнивать все «за» и «против» этой возможности, реализацию которой вы считаете непоследовательной.

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

Я вижу три пути. Первое, ради согласованности сделать var требуемым в лямбда-выражениях и выражениях запроса. Второе, убрать использование var для всех локальных переменных, сделав эту возможность некорректной. И третье, сделав ее везде необязательной. Так в чем же заключается ваш вопрос «А почему не так?»

Вы правы; я обратил внимание на рассогласованность поведения, но я ничего не сказал о том, как от нее избавиться. Я не знаю, как бы звучал мой вопрос «А почему не по-другому?», так что давайте рассмотрим все варианты; какие есть «за» и «против»?

Давайте рассмотрим первый вариант: var требуется везде. Это означает, что вам придется писать так:

 from var c in customers join var o in orders...

А не так:

 from c in customers join o in orders...

И придется писать так:

 customers.Select((var c) => c.Name)

А не так:

 customers.Select(c => c.Name)

Выглядит неуклюже. Что нам дает использование здесь var? Это не повышает читабельности; на самом деле, код становится даже менее читабельным. Мы платим слишком большую цену за согласованность. Первый вариант кажется неподходящим.

Давайте рассмотрим второй вариант: убрать использование var везде. Это означает, что все примеры, использующие var, станут выглядеть так:

 x1 = whatever;
for(x2 = whatever; ;) {}
using(x3 = whatever) 
{}
foreach(x4 in whatever) {}

С последним фрагментом нет никаких проблем; мы знаем, что переменные, объявленные в цикле foreach, всегда приводят к появлению новой переменной. Во всех остальных случаях мы добавляем новую возможность в язык программирования; теперь у нас появляются не просто неявно типизированные локальные переменные, но и неявно объявленные локальные переменные. Теперь, для объявления новой локальной переменной достаточно присвоить значение новому идентификатору.

Существует множество языков программирования, поддерживающие неявное объявление локальных переменных, но эта возможность кажется совершенно не «сишарпной». Подобные возможности поддерживаются языками, типа VB и VBScript, и даже там вы должны включить эту возможность явно в настройках с помощью опции Explicit. Цена за согласованность сильно отличается от первого случая, но она все равно очень высока. Я не думаю, что мы готовы так дорого за нее платить.

Третий вариант, везде сделать var необязательным, является всего лишь разновидностью второго варианта; опять-таки, это приведет к неявному объявлению локальных переменных.

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

А не является ли эта рассогласованность показателем более глубоких проблем в дизайне языка?

Да. При рассмотрении трех вариантов решения рассогласованности, я упомянул, что сделал определенные допущения по поводу того, что можно изменять, а что нет. Если бы мы могли сделать более масштабные изменения или мы бы приняли другие решения при разработке C# 1.0, то у нас не было бы таких проблем вовсе. Более глубокая проблема языка связана с тем, что объявление локальной переменной выглядит так:

type identifier ;

Это, прежде всего, далеко не идеальный синтаксис выражений. Вместо этого, давайте предположим, что синтаксис объявления локальной переменной в C# 1.0 был бы таким:

varidentifier : type ;

Я понял, куда вы ведете; в JScript. NET используется этот синтаксис, и делает указание типа необязательным. И Visual Basic, конечно же, использует аналогичный синтаксис, только вместо var используется DIM, а вместо двоеточия используется « As ».

Совершенно верно. Таким образом, в C# 1.0 при обязательном указании типа синтаксис был бы следующим:

 var x1 : int = whatever;
for(var x2 : int = whatever; ;) {}
using(var x3 : 
IDisposable = whatever) {}
foreach(var x4 : int in whatever) {}

Да, это более многословный синтаксис. Но его значительно проще анализировать, как компилятору, так и человеку, и, скорее всего, он более очевиден для новичка. Абсолютно ясно, что в этом выражении объявляется новая локальная переменная. Кроме того, это очень напоминает объявление базового класса, что логически является аналогичным объявлением. (Вы могли бы задать и такой вопрос: «Почему ограничение типа располагается слева от идентификатора в локальных переменных, параметрах, полях, свойствах, событиях, методах и индексаторах, но справа от идентификатора класса, структуры, интерфейса и типа параметра?» Непоследовательность повсюду!)

В таком случае, мы добавили бы неявно типизированные переменные в язык C# 3.0, просто сделав аннотацию типа необязательной, позволив, таким образом, все следующие операторы и выражения:

 
var x1 = whatever;
for(var x2 = whatever; ;) {}
using(var 
x3 = whatever) {}
foreach(var x4 in whatever) {}
from c in customers select c.Name
from c : Customer in customers select 
c.Name
customers.Select(c => c.Name)
customers.Select((c : Customer) 
=> c.Name)

Так если этот синтаксис лучше, то почему вы им не воспользовались в C# 1.0?

Поскольку мы хотели создать привычный язык для программистов С и С++. Так что мы получили одну форму согласованности – согласованность опыта С-программистов, что через десять лет привело к проблемам согласованности самого языка C#.

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

Оригинал статьи