- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

Автор: bobermaniac


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/net/15633

Ссылки в тексте:

[1] WAT: https://www.destroyallsoftware.com/talks/wat