Pull to refresh

Логично, но незаконно

Reading time 6 min
Views 22K
Полагаю, что многие пришедшие в славный мир .NET из славного мира С++ прекрасно помнят, как им приходилось буквально впиваться в стандарт, чтобы разобраться, почему язык ведет себя именно так, а не иначе. Многие вещи, которые казались им совершенно очевидны, при ближайшем рассмотрении оказались не то, что неочевидны — а просто-таки прямо противоположны здравому смыслу, на который мы все привыкли полагаться.

Впрочем, вероятнее всего, это проблема многих языков программирования. Многие, думаю, помнят известный ролик 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-го собрания на третьей неделе четвертого месяца.

Размышления над причинами этого, к сожалению, я вынужден оставить для самостоятельного осмысления — мой компилятор, к сожалению, слишком бурно отметил пятницу и они со стандартом сейчас храпят на кухне. Пожалуй, я займусь тем же. Приятных снов и хороших выходных!
Tags:
Hubs:
+4
Comments 117
Comments Comments 117

Articles