Впрочем, вероятнее всего, это проблема многих языков программирования. Многие, думаю, помнят известный ролик WAT, посвященный проблемам некоторых «очевидностей» языков JavaScript и Ruby. Логика привычного мира выходит покурить тогда, когда появляются пограничные области — те, в которые нормальные люди не лазят.
Впрочем, я предлагаю несколько отвлечься от этих высоких материй и взглянуть на язык C# несколько с другой, непривычной стороны. А именно посмотреть некоторые конструкции, которые, с одной стороны, совершенно понятны и легко описываются в терминах языка, а с другой — совершенно отказываются компилироваться.
Я даже не собираюсь спорить с тем, должны ли они компилироваться и работать. Я просто еще раз в очередной раз напомню, что все то, что для нас «логично и понятно» на самом деле может быть совершенно нелогично и непонятно.
И, да — я не буду в очередной раз ныть на тему того, что хотелось бы вернуть из функции тупл без использования громоздких структур и получить имя переменной с помощью простого оператора. Это не сюда — это к Майкрософту в фич-реквесты. У нас пятиминутка юмора. Итак, все то, что вы ежедневно хотите написать, но не пишете, потому что знаете — оно не скомпилируется. Поехали!
Начнем с самого простого.
int i = 0;
Что я слышу? Как «что делает этот код»? Вон из профессии! Таким интеллектуальному большинству не место в нашем клубе яйцеголовых. Этот код не делает ничего — компилятор выкинет его при оптимизации и будет совершенно прав. Но смысл этой записи понятен даже умственно отсталому — мы определяем переменную с именем i и присваиваем ей значение, равное 0.
Перейдем к более сложному примеру.
int i = 0; int j = i;
Еще половина зала покинула наш шабаш, не в силах разобраться в потрясающей сложности примеров. Не закрывайте вкладку — я театрально нагнетаю обстановку перед кульминацией.
Попробуем так
int i = 0, j = 0;
И так
int i = 0, j = i;
Нет никаких проблем.
Но как только мы хотим go some math, компилятор сразу появляется из небытия и дает нам по рукам метровой линейкой. Не сметь, мол, захламлять мне код уравнениями!
int i = j = 0; // error CS0103: The name `j' does not exist in the current context
Но русские не сдаются!
int i = int j = 0; // error CS1525: Unexpected symbol `j', expecting `.'
Теперь, когда слабые духом покинули наш кружок «Умелые ручки», можно несколько разнообразить дискуссию с помощью тернарного оператора!
int i = whatever ? 1 : 2;
Мы каждый день пишем такой код. Но не все знают (в особенности те, кто никогда не писал на С++, впрочем как и те, кто считает семантику этого языка в разы мощнее, чем у C# и не догадываются о том, что многие операции там делаются аналогично), что можно сделать вот так.
int i = whatever ? (j = 1) : 2;
Сомневаюсь, что есть люди, для которых данная запись — загадка. Означает она простую вещь — если whatever, то присвой j единицу и присвой результат присваивания(то есть значение j) переменной i. Иначе присвой ей 2. Вооруженные этой невероятной мощью, стреляем от бедра!
int i = whatever ? 2 * (j = 1) - 1 : 2;
Но всегда хочется странного — например, решить в тернарном операторе бикубическое уравнение. Или просто сделать что-нибудь левое.
int i = whatever ? { j = 1; return 2; } : 2; // error CS8025: Parsing error
Увы, человеческий разум терпит фиаско перед лицом кровожадной машины. Он пытается нащупать обходной путь…
int i = whatever ? ( () => 1 )() : 2; // error CS0119: Expression denotes a `anonymous method', where a `method group' was expected
Так и слышу, как компилятор кричит:
— Да сделаю я тебе делегат, сделаю, ты только скажи какого типа?
— Любого!
— Какого?
— Любого. Люююбого. Лююююбого!
— System.MulticastDelegate.
— Не, ну не любого конечно...
(кстати, кто поможет найти этот ролик на ютубе — буду очень благодарен)
И находит его!
int i = whatever ? new System.Func<int>( () => 1 )() : 2;
Хотя, увы — он не так красив, как первоначальная идея.
int i = whatever ? new System.Func<int>( () => { j = 1; return 2 + j; } )() : 2;
Но на безрыбье, как говорится, и сапог — сардина.
Теперь, когда половина оставшихся уже громогласно храпит, можно перейти к сложным инструкциям — блокам.
Все мы писали циклы.
for(int i = 0; i < 10; i++);
И знаем, что фактически данная запись аналогична следующей:
{ int i = 0; while(i < 10) i++; }
Ну а такая
int i; for(i = 0; i < 10; i++)
Аналогична такому коду
int i = 0; while(i < 10) i++;
Вообще, объявлять переменные у управляющих инструкций прямо в самих инструкциях, а не выделять под это специальный блок — здорово. Иначе бы многие компактные вещи приходилось писать куда более многосложно. Особенно это касается блока using
using(IDisposable data = GetSomeData()) { }
Который разворачивается во что-то подобное
{ IDisposable data = null; try { data = GetSomeData(); } finally { if(data != null) data.Dispose(); } }
А теперь представим себе, что мы хотим записать в «сокращенной форме» следующее выражение
{ int i = 5; if(i < 10) i = 7; }
Логично было бы внести объявление переменной в управляющую инструкцию. Победа?
if((int i = 5) < 10) // error CS1525: Unexpected symbol `i', expecting `)' or `.' i = 7;
So close…
Окей, но C# же у нас объектно-ориентированный. Поиграем с объектами!
Вспомним, как мы просто и эффективно вызывали родительский конструктор из конструктора производного класса. Какой элегантный и понятный синтаксис там был — загляденье!
class SomeClass { public SomeClass(int data) { } } class SomeOtherClass : SomeClass { public SomeOtherClass(int data) : base (data) { } }
И сразу хочется странного.
class SomeClass { public virtual void SomeMethod(int data) { } } class SomeOtherClass : SomeClass { public override void SomeMethod(int data) : base (data) { } // Unexpected symbol `:' in class, struct, or interface member declaration }
Последний раз, когда я попытался убедить компилятор в том, что моя форма записи имеет смысл — он сломал мне два ребра. Будьте осторожны с экспериментами!
А теперь, когда меня читает только один человек (да, это именно вы — пожалуйста, не уходите — я боюсь одиночества) мы с вами посмотрим еще один пример, который не то, чтобы представляет собой что-то такое, чего хочется, но не можется, а просто настолько странный, что ставит в тупик даже меня самого.
В отличие от остальных, он, кстати, компилируется во всех вариантах.
Мы сделаем все то, что делаем каждый день, но несколько необычным образом. Изподвыподверта, так сказать.
У нас будет иерархия из двух классов, реализующих один интерфейс. И мы будем делать следующие вызовы и смотреть за результатами.
new FooA().Foo(); new FooB().Foo(); ((IFoo) new FooA()).Foo(); ((IFoo) new FooB()).Foo(); ((FooA) new FooB()).Foo(); ((IFoo)(FooA) new FooB()).Foo();
Потому что вот такие вот мы смешные.
Начнем с самого простого — виртуальные методы. Как по книжке.
interface IFoo { void Foo(); } class FooA : IFoo { public virtual void Foo() { System.Console.WriteLine("A"); } } class FooB : FooA, IFoo { public override void Foo() { System.Console.WriteLine("B"); } }
Результат очевиден!
new FooA().Foo(); // A new FooB().Foo(); // B ((IFoo) new FooA()).Foo(); // A ((IFoo) new FooB()).Foo(); // B ((FooA) new FooB()).Foo(); // B ((IFoo)(FooA) new FooB()).Foo(); // B
Но мы же умные. Зачем нам виртуальные методы? У нас есть интерфейс.
interface IFoo { void Foo(); } class FooA : IFoo { public void Foo() { System.Console.WriteLine("A"); } } class FooB : FooA, IFoo { public void Foo() { System.Console.WriteLine("B"); } }
Конечно, мы получим предупреждение
// warning CS0108: `FooB.Foo()' hides inherited member `FooA.Foo()'. Use the new keyword if hiding was intended
Но не обращайте внимания — проблема не в нем, и ключевое слово new совершенно не влияет на эксперимент.
Результат ошеломляет.
new FooA().Foo(); // A new FooB().Foo(); // B ((IFoo) new FooA()).Foo(); // A ((IFoo) new FooB()).Foo(); // B ((FooA) new FooB()).Foo(); // A ((IFoo)(FooA) new FooB()).Foo(); // B
Самое чудовищное тут — разница между вызовом метода через приведение к базовому классу и вызовом метода через приведение к базовому классу, а затем — к интерфейсу. Потому что метод ведет себя так, как вел бы себя виртуальный метод за исключением одного случая — приведения к базовому классу.
Попробуем сэмулировать его без интерфейсов. Допустим, что интерфейс всегда будет вызывать метод первого класса в иерархии.
class IFoo { public void Foo() { System.Console.WriteLine("A (I)"); } } class FooA : IFoo { public void Foo() { System.Console.WriteLine("A"); } } class FooB : FooA { public void Foo() { System.Console.WriteLine("B"); } }
Увы, фиаско — поведение совершенно не такое, как нам хочется.
new FooA().Foo(); // A new FooB().Foo(); // B ((IFoo) new FooA()).Foo(); // A (I) ((IFoo) new FooB()).Foo(); // A (I) ((FooA) new FooB()).Foo(); // A ((IFoo)(FooA) new FooB()).Foo(); // A (I)
Если он будет вызывать последний метод в иерархии — поведение тоже решительно отличается от того, что мы имеем с интерфейсом.
Никакая логика не способна осмыслить именно такое поведение. Разве что пункт 3 подпункт 14 параграфа 1 главы 7 части 3 тома 2 стандарта 2005 года ревизии 4, принятого в 1 чтении 2-го собрания на третьей неделе четвертого месяца.
Размышления над причинами этого, к сожалению, я вынужден оставить для самостоятельного осмысления — мой компилятор, к сожалению, слишком бурно отметил пятницу и они со стандартом сейчас храпят на кухне. Пожалуй, я займусь тем же. Приятных снов и хороших выходных!

