22 сентября 2012 в 01:44

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

Полагаю, что многие пришедшие в славный мир .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-го собрания на третьей неделе четвертого месяца.

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

Комментарии (116)

  • +7
    Казалось бы в последнем примере все логично: метод не виртуальный и поэтому реализация выбирается в момент компиляции. Мы же не просим от такого кода:
    class A {}
    class B: A {}

    void method(A a) { System.Console.WriteLine(«A»); }
    void method(B b) { System.Console.WriteLine(«B»); }

    A b = new B();
    method(b);

    вывода на экран «B». Тут абсолютно все также и логично.
    • –3
      Там интересная деталь в том, что вызов метода через преобразование к классу А выдает правильный вариант, а через преобразование к классу А и затем к интерфейсу — вариант, как будто метод изначально был виртуальным.
      • +3
        Все правильно: все методы интерфейса — по сути виртуальные (хотя и называются немного по-другому)
        • 0
          Я в глубине души догадываюсь, что вся проблема в том, что вызовы через интерфейс идут в динамике («типа виртуальные»), а через преобразование — компилятор видит невиртуальный метод и вставляет прямой вызов в статике. И вся проблема именно в этом.
          • +1
            Могу переделать ваш пример без интерфейсов:

            class A {
              public Action FooAct { get; protected set; }
              public void Foo() { Console.WriteLine("A"); }
            
              public A() { FooAct = Foo; }
            }
            
            class B : A {
              public new void Foo() { Console.WriteLine("B"); }
            
              public B() { FooAct = Foo; }
            }
            
            ...
            
            var a = new A();
            var b = new B();
            
            a.Foo(); //A
            a.FooAct(); //A
            b.Foo(); //B
            b.FooAct(); //B
            // А теперь самое интересное:
            a = b;
            a.Foo(); //A
            a.FooAct(); //B :)
            


            Я думаю, в данном случае каждый поймет, что именно произошло. Но почему-то, стоит заменить делегат интерфейсом — возникают сомнения или непонимание.
            • 0
              Как говорится, «дело не в тебе — дело во мне».
          • 0
            так может быть не надо копиться в глубине души и просто не начать читать подходящую литературу?

            >вызовы через интерфейс идут в динамике («типа виртуальные»)
            не типа, а для диспетчеризации вызовов методов интерфейса в CLR существует отдельный Vtable Map (IVMap).

            в то время как для экземпляров объектов через Method Descriptor (MethodDesc), инкапсулирующий в себе текущую реализацию метода.

            если разбирать примеры по виртуальным примерам, то
            warning CS0108
            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
            


            в последней строке ((IFoo)(FooA)new FooB()).Foo(); лишний кастинг к FooA (FooB уже является имплементацией IFoo).

            вообще если метод не помечен как virtual и «переопределяют», то это плохой тон.
            то же самое как примера с интерфейсом, так и базовым классом.
            • 0
              *копаться
            • 0
              Я знаком с «подходящей литературой», но я не воспринимаю Стандарт как нечто Б-годанное. В нем достаточно нелогичностей и странностей.
              • 0
                можете привести примеры? если это implementation details, то это известная тема.
                лично для меня до CLR 2.0 было странным использование слабой модели памяти.
                • 0
                  Зачем?

                  Для того, чтобы кто-то из нас «победил»?

                  Избавьте меня от этого глупого времяпрепровождения.
                  • 0
                    весьма странное поведение с Вашей стороны, т.к. сами же пишете о стандарте как:
                    >В нем достаточно нелогичностей и странностей.
                    и при вопросе: «можно ли аргументировать» — отказываетесь.
                    однако дело Ваше :)
                    • 0
                      Можно подумать, что если я что-то написал — я обязан это аргументировать.

                      У нас не научный диспут. Ваше право не верить мне на слово. Я свою точку зрения никому не навязываю.

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

                      То, что я описываю, к этому не имеет никакого отношения. Это просто своего рода пятиминутка юмора, к которому почему-то отнеслись слишком серьезно.
  • +22
    Вопрос.
    Вот ты вы даете пример:
    Но как только мы хотим go some math, компилятор сразу появляется из небытия и дает нам по рукам метровой линейкой. Не сметь, мол, захламлять мне код уравнениями!

    int i = j = 0;


    А теперь внимание — вы ожидали, что эта строчка скомпилируется без объявления переменной j?

    int i = int j = 0;
    

    Про это я вообще молчу.

    Простите, вы из каких плюсов приехали? Марсианских?
    • –6
      Ну человек же однозначно понимает данную конструкцию, значит ее можно формализовать, значит можно научить ей компилятор. Я думаю, автор хотел сказать это.
    • НЛО прилетело и опубликовало эту надпись здесь
      • +4
        Кстати, функциональный C# существует и называется Nemerle.

        Там многие из моих «хотелок» есть.
        • +1
          Немерле — это в первую очередь мета-C#. Функциональщина там через это.
          • –1
            А это уже не особо важно.
    • –7
      Why so serious? Я же написал — что это даже не «хотелки» — это просто попытка написать те вещи, которые мы привыкли никогда не писать (потому что они не компилируются), опираясь на «логику».
      • +5
        Лично я не вижу «логики» в этом:

        int i = int j = 0;

        Ни в C++, ни в других известных мне языках объявление переменной не может быть использование как выражение (rvalue). Какой результат оно должно выдавать, где логика? А вот операция присваивания в С++ может быть rvalue, что и позволяет делать фокусы типа i=j=0 или даже

        while((res=next_result())!=false)

        и даже это не везде — в том же Delphi операция присваивания не может быть использована как rvalue, то есть нельзя написать а:=b:=2;
        • –1
          =>Какой результат оно должно выдавать, где логика?
          Если честно я вообще нигде логики нашел.
          Из того, что я знаю, между C# и С++ есть такие отличия, навскидку
          1)Конструктор базового класса всегда вызывается в С++, а в С# ему надо указывать, иначе не вызовется.
          2)protected в С# позволяет вызвать методы базового класса только на this и не позволяет вызвать их на другом объекте того же класса, в C++ это можно.
          3)Более строгая типизация, нельзя int использовать как bool, например, но это уже более очевидно.
          При этом у меня почему то не возникало мыслей, о том, что человеческий разум оказывается понять поведение языка C#, когда в плюсах я делал вот так а теперь не работает. Хотя местами, если не сказать большинством, эти «хочу странного», как правильно сказали, и на плюсах не имеет смысла и не будет компилиться.

          ЗЫ и вообще, все эти «хочу странного» приводят к нечитаемому и неподдерживаемому коду, когда натыкаешься, хочется автора казнить ректально и прилюдно.
          • +1
            Немного уточнений:

            1)Конструктор базового класса всегда вызывается в С++, а в С# ему надо указывать, иначе не вызовется.

            Неверно.

            2)protected в С# позволяет вызвать методы базового класса только на this и не позволяет вызвать их на другом объекте того же класса, в C++ это можно.

            Кто вам это сказал?

            3)Более строгая типизация, нельзя int использовать как bool, например, но это уже более очевидно.

            Это — единственный образец «более строгой типизации». Зато в C# любое примитивное значение можно забоксить в object, чего нельзя сделать в C++ за отсутствием аналога object.
            • 0
              Упс, извиняюсь за пункт 2 — я прочитал немного не то, что вы написали.
              • 0
                Неверно.

                Перепутал с джавой, видимо.
                Это — единственный образец «более строгой типизации».

                Отнюдь, int нельзя присвоить в short, например, возникает ошибка предлагающая принудительный каст. Хотя «расширение» происходит автоматически.
                Но это еще ладно, а вот bool меня шокировал до глубины души.
  • +4
    По поводу строки:
    int i = int j = 0;
    

    Вы немного перемудрили:
    1) Я думаю надо посмотреть описание языка и будет все ясно что не так. Любой оператор это: for, (do)while, using, try, foreach, if, либо [имя_типа имя_переменной = ] выражение[, [имя_типа имя_переменной = ] выражение]...;
    2) В случаях с интерфейсами и классами тоже на самом деле все очевидно. Разница лишь в том, что у интерфейса нет реализованного метода, а у класса, от которого Вы наследуете — есть. Этим все и объясняется.
    • 0
      *извиняюсь вот так должно быть [имя_типа имя_переменной = ] выражение[, [имя_переменной = ] выражение]...;
    • 0
      Я знаю описание языка. И знаю, почему так писать нельзя. Речь не о том, что нельзя, и не о том, почему нельзя — а о том, что вроде бы «логично», но нельзя.
  • –10
    Компания микрософт основана инопланетянами для того, чтобы человечество было готово к тому, что во вселенной существует инопланетный разум. Почему экзопсихологи не изучают .net и PHP для меня загадка :)
    • –1
      Похоже, вы сильно задели макак=)
      • +1
        Я ошибся в слове экзопсихология, по иннерции. Оно на самом деле эзопсихология.
        • +1
          Да и в слове «инерция» тоже.
        • 0
          Я вас правильно понял:)
          Имел (имею?) дело с эзотерикой в программировании.
          • 0
            Эзотерика и эзопсихология совсем разные понятия. Нет, не правильно вы меня поняли, но я рад, что вам тоже не скучно :)
  • +13
    Полагаю, что многие пришедшие в славный мир .NET из славного мира С++ прекрасно помнят, как им приходилось буквально впиваться в стандарт, чтобы разобраться, почему язык ведет себя именно так, а не иначе

    Какие-то странное утверждение. C# — гораздо проще и прямолинейней в сравнении с C++. Даже компилятор вам прямо указывает на места, которые выглядят «подозрительно», чего ещё для счастья надо-то?
    • 0
      Мне — ничего. Это пятиминутка юмора.
  • +17
    Что за, простите, неведомая чушь?

    Уважаемый автор, вы серьезно верите в то, что пишите?
    У меня просто непрерывный WTF мозга от прочитанного:

    1.
    int i = int j = 0;

    Серьезно? Объявление переменной сразу в месте использования? А инициализация переменной?
    Не надо оправдываться тем, что, мол, вы там выражение с операторами сравнения делаете. Создавать отдельные выражения-мульти-объявления-с-мульти-инициализацией — бредовый синтаксический сахар, создающий только больше путаницы в синтаксис (иногда присваивание — это оператор, а иногда — спец-синтаксис для спец-выражений).

    2.
    if((int i = 5) < 10)


    Опять та же самая чушь — одновременная инициализация и использование переменной.
    (Кстати, а вы вообще знаете чем отличаются объявление и инициализация переменных?)

    Для «for» есть специальный синтаксис, который делит инициализацию некоторых переменных от их использования, т.к. это упрощает запись итерирования.
    А нафига нам нужно что-то объявлять и инициализировать внутри «if»? Чтобы сразу провести сравнение? Или чтобы использовать в теле «if»'а?
    Получите дополнительные очки, если догадаетесь, почему это никому не нужно.

    3.
     public override void SomeMethod(int data) : base (data) { }

    А вы вообще знаете, что такое «виртуальная функция»? И почему ваша идея из-за этого превращается в бред, т.к. функция уже становится не виртуальной а… черт я даже не знаю как это назвать.
    Кроме того, если приложить минимальные умственные усилия, то можно понять, что идея сама по себе не имеет смысла: если в предке виртуальная функция имеет реализацию, то значит возвращает какое-то значение. Простыми словами: она сделает «return value» еще до того, как управление вообще сможет перейти к методу потомка.
    Или же предлагается «сначала метод предка сделает ЧТО-ТО, вернет значение, а потом и метод потомка сделает ЧТО-ТО ЕЩЕ, используя те же самые входные параметры и возвращенное методом предка значение»?
    Ну, счастливой отладки вам тогда…

    Дальше уже перестал читать, т.к. реально не было сил.

    Резюмирую: автор, рекомендую лучше изучить мат. часть, прежде чем пытаться делать подобные статьи, утверждая, что это все «логично». Очень надеюсь, что вы просто пошутили.
    • +2
      ааа, о втором пункте всю жизнь мечтал
      представляете, можно было бы написать такое
      var dic = new Dictionary<string, string>();
      ...
      if (var item = dic.TryGetValueOrDefault("Key") != null)
      {
         Console.WriteLine(item);
      }
      


      куча плюсов такого подхода
      1. можно использовать var (сейчас холивар будет);
      2. не засоряется область лишними переменными (мне не нужен доступ к переменной item в не цикла, так как она будет равна null).
      3. одна строка — вместо двух/трех.

      эх, в F# так можно написать и даже больше

      // xml предварительно проверили на валидность через xsd.
      if (let node = root.SelectSingleNode("node")
            node.Value == "value1" or node.Value == "value2")
      {
         ...
      }
      
      • +6
        представляете, можно было бы написать такое

        Никогда не задумывались о том, почему TryGetValue имеет сигнатуру bool… out T, а не просто возвращает null, если значение не найдено?

        А вообще, то, что вы хотите, в один прием делается с уже навязшей в зубах монадой MayBe:

        dic.TryGetValueOrDefault("Key").Do(item => Console.WriteLine(item));
        
        • 0
          TryGetValue — не решает проблему лишней переменной вне условия :)
          Монады круто, но в C# синтаксис не очень для них подходит (устанете копаться в закрывающихся скобочках).
          • +2
            Полагаю lair намекает, что null — корректное значение, которое может храниться в dic.
            • 0
              Если разрешать null, то всё равно придется либо получать значение два раза, либо использовать дополнительную внешнюю переменную.

              if (dic[«key»] != null)
              {
              Console.WriteLine(dic[«key»]);
              }

              либо

              var item = dic[«key»];
              if (item != null)
              {
              }

              в монадами хорошо, но кроме фигурных скобок появляются ещё и круглые, а если использовать вложенность, то совсем плохо смотрится.
              • 0
                Уточню:

                В исходном примере вы делаете следующую проверку: «элемент есть и он не равен null», хотя гораздо чаще требуется проверка «элемент есть». И при помощи TryGetValueOrDefault эту проверку не осуществить.

                Выходы: монады, туплы и out-параметр.
                Для монад требуется более полная поддержка в языке.
                Туплы без сопоставленя с образцом крайне неудобны.
                Так что выбора нет.

                А скобочки — в if они тоже есть.
          • +1
            устанете копаться в закрывающихся скобочках

            К счастью, в IDE для решения этой проблемы достаточно средств.
      • +1
        Вспоминая обсуждение в недавней статье про монады, ваш пример со сравнение с нулем можно запсиать так:

        dic.TryGetValueOrDefault("Key").With(Console.WriteLine);
        


        Добавив собственный метод:

        public static class TWith
        {
        	public static void With<T>(this T t, Action<T> a) where T : class
        	{
        		if(t != null)
        		{
        			a(t);
        		}
        	}
        }
        


        Да и все остальные проверки сдалеть не сложно, добавив таким-же образом немного LINQ-подобного синтаксиса, аналогичного Where.
        • 0
          Так и запишем: не обновляет комментарии, прежде чем оставлять свой.
      • +2
        Второй пункт так, как вы его описали у себя, возможен и в С++.
      • 0
        Зачем сразу вводить новый метод?

        Если уж мечтать, то сразу так:
        if (dic.TryGetValue("Key", out var item))
        {
            Console.WriteLine(item);
        }
        
        • +1
          Кстати, не менее смешной вариант, чем мой.
          • 0
            Я старался :)
    • 0
      #include <iostream>
      
      int main()
      {
         int i;
         std::cin >> i;
         if (int j = i > 0)
         {
            std::cout << "ok!";
         }
      }
      


      output: ok!

      liveworkspace.org/code/123aa4598c57ec541046f61bc844de20
      • 0
        и? здесь как rvalue переменная используется, а не объявление.

        #include int main()
        {
        int i = -1;
        if (int j = i > 0)
        {
        std::cout << «ok!»;
        }
        else
        {
        std::cout << «not ok!»;
        }
        }

        Все логично.
      • 0
        А, все понял, извините.
    • –2
      Я просто пошутил. Я прямо написал об этом в первых абзацах еще до хабраката.

      Ни одни вменяемый человек не будет возмущаться тому, что код, который помечен как нерабочий, не работает.

      Там разве что объявление переменной в блоке if — полезная вещь.
      • 0
        Дык в том то и проблема, что шутка получилась слишком глупой: ваши примеры, которые «нам понятны и логичны», на самом деле совершенно нам не понятны и не логичны.
        Получился этакий «сортирный юмор» IT-сферы.
    • 0
      А нафига нам нужно что-то объявлять и инициализировать внутри «if»? Чтобы сразу провести сравнение? Или чтобы использовать в теле «if»'а?
      Google Go так умеет и это очень здорово.
  • 0
    К статье ещё можно пункт добавить: «Почему мне надо переменную объявлять вне цикла do-while, чтобы использовать её в проверке выхода»

    Почему нельзя написать так:
    do
    {
    bool repeat = // расчет в несколько строк
    }
    while (repeat);
    

    Вне цикла от переменной нет никакого толку. Ну и соответственно почему нельзя объявить переменну в цикле while-do, а в for можно.

    while (bool repeat = true == true) // либо while (bool repeat = true; repeat)
    {
      ... 
    }
    
    • +4
      А зачем городить второй for?
      for(bool repeat = true; repeat;)
      {
        ...
      }
      
      • +1
        Честно говоря, мне пришлось в голове разложить цикл на компоненты, чтобы понять, что именно тут написано.
        • 0
          Так эта запись не отличается от записи с while выше, и несколько проще для понимания, чем большинство конструкций ФЯП. И проще даже чем int i = int j = 0
        • +4
          Ну «вон из профессии» тогда. for это не конструкция вида «переменной присвоить ноль; сравнить меньше ли переменная N; прибавить один», а вовсе даже «что-то проинициализировать; проверить значение; сделать что-то после итерации цикла».
    • –1
      А что вы хотели сказать своим while?

      while ((repeat = true) == true) // while (true)
      {
        ...
      }
      

      while (repeat = (true == true)) // while (true)
      {
        ...
      }
      


      Может, поэтому в C# (и аналогичных языках) и нет такой конструкции?
      • 0
        Самое интересное то, где объявлена repeat. У Вас она за циклом. А зачем она нужна вне цикла?
        • 0
          Не спорю. Но внесение объявления и инициализации в while, как мне кажется, несет больше проблем, чем пользы. В том числе и из-за подобных неочевидностей (например, придется оговаривать, что первый знак "=" после объявления — это не присваивание, а инициализация переменной, выполняемая лишь однажды при входе в цикл).
          • 0
            Ну в for ведь разрешили :)
            • 0
              Там иной синтаксис: инициализация, условие и итератор явно разделяются точкой с запятой. Ну а на отсутствие необходимости изобретать еще один for уже указывали выше.
              • 0
                Предусловие для For? На сколько я понимаю в For условие проверяется после выполнения тела цикла :)
                • 0
                  ???

                  Вообще-то перед первым входом тоже, иначе бы для пустых массивов были бы очень смешные побочные эффекты.
                  • 0
                    Окей, тогда хочу пост условия :)
                • 0
                  Тогда вам дорога к Nemerle: в нем, если не ошибаюсь, есть макросы, позволяющие расширять язык :)
                  Мое личное мнение: не стоит чрезмерно раздувать синтаксис, вводя новые конструкции и налагая различные ограничения / соглашения. Повторюсь, IMHO.
                  • 0
                    Ну это же так, рассуждения в слух :) О том, что иногда кажется, что это «должно работать именно так».
  • 0
    Ещё есть «неадекватность» по моему мнению в операторе ??

    var name = root.Element(«Employee»).Element(«PersonInfo»).Attribute(«Name»).Value ?? "";

    я же (вроде как), четко написал компилятору, что меня интересует значение левой части, если оно не пустое, иначе права часть.

    Реализовать такое тоже думаю не сложно, объявив набор стандартных исключений, которые глотаются оператором ?? (например, KeyNotFound, NullRefenecesException и т. п.)
    • +2
      Вот тут точно нет никакой неадекватности. Компилятор не может за вас знать, вы хотите значение левой части, если дерево валидно, или в любом случае. Так что, следуя принципу fail early, среда выполнения избавляет вас от трудноотлавливаемых ошибок.

      И да, это тоже место для монады MayBe.
      • 0
        Да, согласен. Но чтобы написать правильно тот кусок (с проверками на null), необходимо затратить значительное количество строчек, при написании которых, можно будет отстрелить себе не только ногу, но и голову :)
        • 0
          Ну и возьмите себе монады, где стрелять в ноги сложнее.
          • 0
            И читать сложнее :) По крайне мере в C#.
            • 0
              Всяко проще, чем вложенные проверки на null.

              Dense code.
              • 0
                Согласен. Но на самом деле функциональщина нужна далеко не везде. Например, в моём примере проще сделать так.

                static class Ext
                {
                    public static XElement Element(this XElement el, string name)
                    {
                         if (el == null) return null;
                         return el.Element(name);
                    }
                }
                
                • 0
                  Во-первых, это та же самая функциональщина, ничем не отличается. Просто у вас частный случай, а в With/Do — общий.
                  • 0
                    Методы расширения — это функциональщина? Возможно, но писать значительно проще.

                       var name = root.Element(«Employee»).Element(«PersonInfo»).Attribute(«Name»).Value() ?? "";
                    


                       var name = root.Element("Employee").With(e=>e.Element("PersonInfo").With(e=>e.Element("Name")).With(e=>e.Attribute("Name")).With(e=>e.Value) ?? "";
                    


                    а я линивый :) и ошибиться боюсь.
                    • 0
                      Нет, пайплайнинг функций — это функциональщика.

                      Возможно, но писать значительно проще.

                      Ну, я не вижу особой разницы с тем, что ниже, зато решение универсальное.

                      root.With(r => r.Element("Employee").Element("PersonInfo").Attribute("Name").Value()) ?? ""
                      
                      • 0
                        Что будет если тег Employee не будет найден?
                        • 0
                          Если любое звено в цепочке вернет null, будет возвращен null, дальше будет coalesce к пустой строке.
                          • 0
                            Это как так? r.Element(«Employee») возвращает null и вызов Element(«PersonInfo») у null'а, дает NullReferencesException. Или тут магия?
                            • 0
                              Насколько я понял, эта магия называется Expression
                            • 0
                              А кто вам сказал, что то, что вы видите после => — это исполняемый код? Это Expression, его интерпретация полностью на усмотрение метода.
    • 0
      Вам так часто (и совершенно обосновано) стали рекомендовать монады, что может уже пора попробовать ФП-язык?
      • 0
        Обожаю F#. Но мы ведь тут обсуждаем, что бы ещё такого можно было бы накрутить на C# :)
  • +3
    Я себе вообще не представляю как можно такие языковые конструкции использовать в сложных коммерческих проектах. Предпочитаю разделять какое-то действие на несколько строк в несколько символов, чтобы вернувшись через год к этому куску не вспоминать, что же тут все-таки происходит.
    Ваши примеры разве что для олимпиадных задач «уместить алгоритм какой-то там в 100 байт исходников»
  • +15
    Статья о том, как с# многократно и с особым цинизмом не давал автору выстрелить себе в ногу.
  • –2
    Пример с WAT совсем не в тему, т.к. автор допускает в видео несколько ошибок, которые новички воспринимают как неочевидности.
    • 0
      Дык у меня вообще весь код — одна сплошная ошибка, он даже не компилируется.
      • –1
        Нет нет, дело не в этом. Автор на видео говорит что складывает два объекта так: {} + {}, но он глубоко заблуждается. Он говорит что в результате мы получаем объект "[object Object]", но он неправ, так как в результате он получает строку "[object Object]". Именно поэтому видео с WAT не в тему, т.к. автор делает совсем не то что говорит.
  • –2
    И после этого люди удивляются, почему в школе учат паскалю…
    • +2
      Может разовьете свою мысль?

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

      Или вы считаете, что вопросы закономерны и логичны и показывают превосходство синтаксиса паскаля над C#? Тогда приведите примеры соответствующих синтаксических конструкций в паскале.

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

      Из вашего комментария не ясно выше отношение ни к топику. ни к паскалю.
  • 0
    вот ставить break-и внутри case-ов (после default метки) вот это создает вопрос «зачем»…
    • +1
      Вообще обязательность break после case полностью убивает смысл этого break.
      • 0
        У break после case нет никакой обязательности.
        • 0
          A jump statement such as a break is required after each case block, including the last block whether it is a case statement or a default statement. With one exception, (unlike the C++ switch statement), C# does not support an implicit fall through from one case label to another. The one exception is if a case statement has no code.


          Естественно вместо break там может стоять return, только не понятно зачем break вообще нужен, раз уж нельзя его убрать и попасть в следующий блок после завершения текущего.
          • 0
            Про это и речь.

            Кстати, там еще может стоять goto case.
    • +1
      Меня больше расстраивает тот факт, что case не создает отдельной области видимости переменной.
      Например,
      switch (...) 
      {
        case ...:
          int x = 5;
          break;
      
        case ...:
          int x = 6; //Ошибка
          break;
      }
      


      Во втором блоке case видима переменная x из первого блока, что и приводит к ошибке. Лично я еще ни разу не сталкивался с необходимостью иметь доступ к переменным из другого блока, а вот с ситуациями наподобие приведенной выше сталкиваться приходилось.
      • 0
        Например,
        switch (...) 
        {
          case ...:
          {
            int x = 5;
            break;
          }
        
          case ...:
          {
            int x = 6; // Нет ошибки
            break;
          }
        }
        • 0
          К сожалению, ваш красивый трюк студия автоматически превратит вот в это:
          switch (...) 
          {
            case ...:
              {
                int x = 5;
                break;
              }
          
            case ...:
              {
                int x = 6; // Нет ошибки
                break;
              }
          }
          • 0
            Эмм… Не вижу в этом особой проблемы. К тому же, форматирование кода — это фича студии, не имеющая отношения к языку. Я лишь хотел показать, что при необходимости вполне возможно создать в case локальную область видимости (хотя сам предпочитаю выносить объявление переменной за пределы switch).
            • 0
              Да, но подобное создание области видимости несколько неудобно, вот и все. Если бы case создавала свою область видимости, было бы чуть удобнее.
              • 0
                Согласен. Думаю, авторы языка исходили из двух предпосылок:
                1. Единообразие: область видимости ограничена {… } (с небольшим отступлением для цикла for).
                2. Преемственность: такое поведение case уже привычно для программистов на C / Java (а легкость перехода на новый язык была одним из приоритетов при создании C#). Последствия нарушения преемственности хорошо видны на запрете «проваливания» между case'ами: возмущенные крики слышны до сих пор, хотя языку уже около 10 лет.
  • –1
    Бред.
    Похоже на результат портирования джуниора работающего со слаботипизироваными языками, на .NET

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

    В .NET есть куда более не тривиальные вещи, и поведения.
    Если уж задумались, то подумайте, как в .NET можно создать экземпляр _интерфейса_
    (это возможно сделать, при некоторых условиях)
    • –1
      Вы уже не можете отличить бред от юмора?
  • 0
    Если не ошибаюсь, у вас ошибка в примере с юзингом. В том смысле, что в трай-кэтч не попадает сама инициализация переменной.
    • –2
      Попадает. В противном случае была бы утечка неуправляемых ресурсов в случае прерывания потока сразу после присвоения переменной до начала блока try.

      В .NET 3.5, впрочем, именно так и происходило.
      • 0
        Очень интересно, видел как раз вопрос на собеседовании на эту тему пару лет назад. А не покажите источник с примером? А то мсдн как-то либо не обновился, либо вы все-таки путаете msdn.microsoft.com/en-us/library/yh598w02.aspx
        • 0
          Я за МСДН-ом не особо слежу, я у Липпета в блоге читал, что он знает про проблему (в контексте обсуждения 3.5) и они думают, как ее решать. В качестве одного из вариантов рассматривался и такой. Чем все закончилось — не знаю, но мне почему-то казалось, что они возьмут именно его как наиболее логичный.
      • 0
        А кто мешает прервать поток непосредственно перед присваиванием? Подобное передвижение не исправляет ошибки.

        Единственной способ предотвратить утечку неуправляемых ресурсов в условиях Thread.Abort() — SafeHandle
        • 0
          Прервать управляемый поток можно только в том месте где это допускается CLR, а не в любом. Например, в случае CER прерывания потока в регионе быть не может в принципе.

          Так что немного подкрутив CLR, можно было получить требуемое поведение.
          • 0
            Его и получили — при помощи SafeHandle.
  • 0
    Насчет «проглатываний» объявлений переменных — да и gcc так делает с выключенной оптимизацией. Насчет последнего «волшебного» примера — я думаю все C#-разработчики с хотя бы небольшим опытом за плечами спокойно читают такой код и понимают почему такие результаты с ходу. Пытаться показать себя умнее авторов языка — странно.

    На мой взгляд в C++ куча «нелогичных» и «неправильных» вещей, потому что я изо дня в день вижу C# код и он мне логичен и понятен, а «странные конструкции» и «а почему нельзя вот так» в C++ видятся. Писал бы постоянно на нем, начал бы подумывать так о C#.

    А еще непонятно, зачем там хаб «ненормальное программирование».

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.