В 53 выпуске подкаста DotNet & More (site, youtube) мы обсуждали фичи C# 10 и разбирали их применимость в будущем. И уже потом возник вопрос: все ли языковые конструкции из C#1, 2, 3, 4 и т.д. мы применяем? И даже если они безбожно устарели, существуют ли ситуации, в которых им найдется применение?


Говоря о C#, я бы разделил его жизненный путь на 2 вехи: до C#6 и начиная с C#6. Именно с релизом шестой версии Microsoft поменяли подход к дизайну языка, начали активно слушать сообщество. Потому и хотелось затронуть не сколько "современный" C#, а C# образца 2012 года, который многими программистами "старой школы" считается True С# (а так как этим ребятам уже лет 30+, они как раз занимают позиции тимлидов и продукт менеджеров, соответственно, определяют технологический стек проекта).

Если смотреть на C# 5 с точки зрения лишних фичей, то их окажется не так много. На мой взгляд, я бы отметил следующие:

И тем интереснее попробовать найти применимость данных языковых конструкций в 2021 году.

Delegate operator

Тот факт, что данный оператор безбожно устарел подтверждают и сами Microsoft в своей справке:

Но внутренний манчкин не терпит такую растрату ключевых слов. Неужели lambda expressions по всем параметрам лучше такого "лампового" delegate? Нет, существует одна особенность, в delegate operator можно опускать параметры метода, если они нам не нужны. В lambda expressions можно использовать underscore  (_), но если в функции много параметров, получается неприятный, с эстетической точки зрения, код. Давайте сравним:

Func<int, string, double, User, bool> allowAll = (_, _, _, _) => true;

versus

Func<int, string, double, User, bool> allowAll = delegate { return true; };

Данный пример, если честно, очень вдохновляет. Ведь мы нашли применимость такому "динозавру"!

Конечно, я бы не рискнул использовать такой подход в реальных проектах: эстетика эстетикой, но фрустрация у других разработчиков от таких синтаксических конструкций может стоить не один человеко-час. И не хотелось бы тратить время команды впустую.

The dynamic type

Не секрет, что динамический тип был добавлен в C# для упрощения работы с COM (см https://stackoverflow.com/questions/14098958/c-sharp-dynamic-com-objects). Конечно, кто-то скажет что разработчики решили сделать программистам приятное и превратить C# в такой прекрасный и удивительный язык как JavaScript, но давайте будет честны сами с собой, в 2010 году Microsoft шла по пути Балмера с построением собственной закрытой экосистемы, минимально обращая внимание на окружающий мир. И потому сбрасывать со счетов dynamic не стоит, COM еще жив, и будет жить, коль жив MS Office.

Еще иногда приходится использовать dynamic при работе с библиотеками, в которых эта фича задействована, например, ASP Net MVC. Тем не менее я бы не сказал, что такая практика хоть сколь либо распространена.

Но вот если вы не разрабатываете Add-Ins для Excel, есть ли толк от динамических типов? На самом деле да: dynamic type крайне удобен при прототипировании. Когда мысль летит вперед, перепрыгивание на DTO файл с полями будет просто прерывать ее.

dynamic entity = new ExpandoObject();
// the genius code with 'entity' variable

Подобный подход крайне удобен при обсуждении реализации и "накидывании" вариантов решения задачи: вместо рисования на white-board можно сразу в коде расписать приблизительный вариант решения и, более того, запустить это решение.

В остальном же приходится признавать, что dynamic это огромный кусок ненужного функционала. И это очень грустно, так как комбинация статической и динамической типизации могла быть невероятно мощным инструментом. На мой взгляд, этого не случилось, потому что была потеряна одна из самых главных особенностей таких языков программирования как JavaScript и Python: интерпретируемость. Несмотря на то, что Dynamic Language Runtime есть интерпретация, у нас нет возможности избежать перекомпиляции сборки, а значит мы не можем вносить изменения в процессе работы приложения. Например, разработчики 1С очень часто  пишут код инкрементально - в обработчик нажатия кнопочки код вносится по мере прохождения по нему интерпретатора, так что нет необходимости перезапускать приложение для любого небольшого фикса. В C# же приходится компенсировать Test First подходом.

Query expression

Данный синтаксический сахар создавал очень много споров лет 10 назад, и это хорошо видно по коду, написанному в те времена, но сегодня всем достаточно очевидно, что код вида

var teenagers = from u in users
    where u.Age is > 10 and < 18
    select u.Name;

гораздо сложнее с точки зрения расширяемости и поддержки, чем "точечная нотация"

var teenagers = users
    .Where(u => u.Age is > 10 and < 18)
    .Select(u => u.Name);

Тем не менее существует, как минимум, один случай, когда Query Expression strikes back: работа с множественными SelectMany.  Представим, что мы хотим найти друзей нашего пользователя. Напишем в разных нотациях:

private IEnumerable<User> GetFriends(string myName) =>
    _users
        .Where(u => u.Name == myName)
        .SelectMany(u => u.Friends);
private IEnumerable<User> GetFriends(string myName) =>
    from u in _users
    where u.Name == myName
    from f in u.Friends
    select f;

В данном случае Dot Notation однозначно лучше, хотя бы с точки зрения количества строк. А если нам понадобится запросить друзей друзей?

private IEnumerable<User> GetFriendsOfFriends(string myName) =>
    _users
        .Where(us => us.Name == myName)
        .SelectMany(u =>
            u.Friends.SelectMany(f => f.Friends)
        );
private IEnumerable<User> GetFriendsOfFriends(string myName) =>
    from u in _users
    where u.Name == myName
    from f in u.Friends
    from fof in f.Friends
    select fof;

Тут ситуация уже куда интереснее, так как в первом случае не удается избежать вложенности. И это может быть достаточно серьёзной проблемой в случае действительно больших запросов, например если необходимо найти не просто друзей друзей, а друзей друзей друзей друзей друзей:

private IEnumerable<User> GetFriendsOfFriendsOfFriendsOfFriendsOfFriends(string myName) =>
    _users
        .Where(us => us.Name == myName)
        .SelectMany(u =>
            u.Friends.SelectMany(f =>
                f.Friends.SelectMany(fof =>
                    fof.Friends.SelectMany(fofof =>
                        fofof.Friends.SelectMany(fofofof => fofofof.Friends)
                    )
                )
            )
         );
private IEnumerable<User> GetFriendsOfFriendsOfFriendsOfFriendsOfFriends(string myName) =>
    from u in _users
    where u.Name == myName
    from f in u.Friends
    from fof in f.Friends
    from fofof in fof.Friends
    from fofofof in fofof.Friends
    from fofofofof in fofofof.Friends
    select fof;

Конечно, данный пример немного надуман, но чего не бывает в кровавом энтерпрайзе.

Главное, в C# есть инструмент "выпрямления" вложенности LINQ и в некоторых случаях он является просто незаменимым.

P.S.: Так же можно отметить, что использовать Join гораздо удобнее в виде Query Expression, нежели чем в Dot Notation, но на мой взгляд, все зависит от привычки.

Заключение

Давайте будем честны, приведенные выше примеры являются, по сути дела, стрельбой пушкой по воробьям. Нет ни какого смысла тратить время на изучение данных устаревших синтаксических конструкций, просто потому, что они будут немного полезны в некоторых случаях. И даже если вы мастер Query Expression, знаток DLR и не представляете свою жизнь без delegate, не стоит усложнять жизнь своим коллегам.

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

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


  1. kenoma
    04.10.2021 16:00
    +1

    Странно, из личного опыта могу сказать, что:

    1. Удобно использовать dynamic на уровне маппинга в ORMах типа Dapper'а. Кроме этого при десериализации можно порой обойтись dynamic'ом если надо быстро что то накостылять, а дтошки нет.

    2. Query expressions очень зашли в синтаксисе DSL фреймворка Sprache.


    1. KAW Автор
      04.10.2021 16:15
      +3

      Хорошее замечание.

      Относительно Sprache, query expression как раз удобен из-за необходимости "выпрямления" запроса

      Parser<string> identifier =
          from leading in Parse.WhiteSpace.Many()
          from first in Parse.Letter.Once().Text()
          from rest in Parse.LetterOrDigit.Many().Text()
          from trailing in Parse.WhiteSpace.Many()
          select first + rest;
      
      var id = identifier.Parse(" abc123  ");
      
      Assert.AreEqual("abc123", id);
      


      1. umbarov
        04.10.2021 18:35

        преимущество на совсем уж маленьких и одноразовых проектах.

        Иногда API сервиса может диктовать внешний сервис (в бизнесе всё возможно). В одном проекте для входных данных эндпоинта мне пришлось использовать свойства `dynamic From ... dynamic To ... dynamic Cc ...` отправитель(и) и получатель(и) почт, а потом полученные данные я маппил в массив строк. В c# пока нет discriminated unions, чтоб можно было сделать проще `string | string[]`. Вот так и иногда приходится использовать `dynamic`.


        1. KAW Автор
          04.10.2021 18:35
          -2

          Ну а как же наследование?


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


  1. SShtole
    04.10.2021 18:02
    +1

    Это, типа, «Даже я, адвокат, требую для подсудимых смертной казни»? В смысле, как-то такие примеры не очень вдохновляют на использование.

    А по существу:
    1. Никогда не любил смешивать SQL-like syntax и ЯП. Данный же конкретный пример наводит на мысль, что что-то неправильно спроектировано. И очень хорошо, что это заметно на уровне языка, гораздо хуже, когда язык это скрывает.

    2.

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

    ИМХО, очень вряд ли это могло бы случиться в C#. Мне больше всего нравится, как комбинация статики/динамики сделана, например, в TIS. С одной стороны — типов нет в принципе (при описании), мысль разработчика ничто не сдерживает, с другой — шаг влево/вправо,
    5 + '3'
    — сразу летят exception'ы, которые отлично заменяют компиляцию в плане безопасности. Это, конечно, много где сделано, можно хоть тот же OLE_VARIANT вспомнить, но без глубочайшей поддержки на уровне языка / стандартной библиотеки эффективности не добиться, а генетика у Шарпа для этой цели не та.


    1. mayorovp
      05.10.2021 12:42
      +3

      С одной стороны — типов нет в принципе (при описании), мысль разработчика ничто не сдерживает, с другой — шаг влево/вправо, 5 + '3' — сразу летят exception'ы

      Это называется "строгая динамическая типизация", а не "комбинация статики/динамики"


    1. a-tk
      05.10.2021 13:46

      Между прочим 5+'3' работает и в C#. 5+"3" тоже.


  1. pankraty
    04.10.2021 20:25
    +4

    Из моей практики, на одном проекте активно использовались dynamic типы для работы с JSON -ами, приходящими из базы. С одной стороны, не надо делать массу DTO- шек, и если структура документа в базе со временем меняется, то вроде бы как код продолжает работать без необходимости поддерживать сущности v1, v2, vN+1... Но преимущества мнимые, т. к. все начинает падать в рантайме, принося в .Net все "прелести" языков без строгой типизации. Бррр, до сих передергивает, как вспоминаю.

    Другой случай, где я применял dynamic, на этот раз самостоятельно, заключался в том, что внешняя библиотека (OpenXML, но это не важно) предоставляла несколько почти идентичных классов, с одинаковым набором полей, но в разных пространствах имён. Никаких общих интерфейсов или предков они не имели. Расширить внешнюю библиотеку тоже нельзя. И нужно было написать конвертацию из обоих типов в наш внутренний, по возможности, избежав копипасты. Для этого у меня было два internal метода, принимающих аргументы из разных пространств имён, и перенаправляющих вызовы в private метод, с dynamic аргументом. Чтобы это все не отвалилось при апгрейде OpenXML, код был плотненько покрыт тестами. И долгое время для меня это был единственный более-менее оправданного применения dynamic на практике, но и то потом оказалось, что это вызывало проблемы у пользователей, работающих в sandbox environment-ах (подробностей не помню, если кому будет интересно, найду соответствующий тикет на гитхабе). Так что и от такого применения пришлось отказаться.

    А в защиту query syntax скажу, что тоже не понимал, для чего им вообще пользоваться, пока не столкнулся с запросом, в котором цепочка джойнов добавляла по одному новому полю к анонимному объекту. С query- синтаксисом такой запрос оказался в несколько раз короче, и читается легче.


  1. pankraty
    04.10.2021 20:58
    +2

    Немного из другой серии, но тоже из разряда бесполезного - это возможность иметь internal abstract член в публичном классе. Если унаследоваться из внешней сборки, переопределять этот член нельзя, т.к. он internal, но не переопределять тоже нельзя, т. к. он абстрактный. Такой вот курьез. Вроде и не баг, но и практической пользы ноль. Будет интересно, если кто-то придумает юзкейс, в котором это применимо.

    Ещё одна вещь, которую вряд ли придумали бы в такой форме, если придумывали сразу, с нуля. "protected internal" является не взаимоусиливающей комбинацией "protected" и "internal", а действует как "или": чтобы получить доступ к члену надо быть наследником ИЛИ (не И) находиться в той же сборке (ср. с private static, например - на него одновременно распространяются ограничения private и static, усиливая друг друга). Зато логическое И выражается в виде модификатора "private protected", хотя от private в нём вообще ничего. Мне кажется, если бы не обратная совместимость, то "protected internal" должен был бы стать тем, что сейчас называется "private protected", а нынешний protected internal (с логическим ИЛИ) должен быть упразднен: если уж член protected, то ему обычно незачем быть публичным внутри своей сборки.


    1. mayorovp
      05.10.2021 12:53
      +3

      Немного из другой серии, но тоже из разряда бесполезного — это возможность иметь internal abstract член в публичном классе. Если унаследоваться из внешней сборки, переопределять этот член нельзя, т.к. он internal, но не переопределять тоже нельзя, т. к. он абстрактный. Такой вот курьез. Вроде и не баг, но и практической пользы ноль. Будет интересно, если кто-то придумает юзкейс, в котором это применимо.

      Практическая польза тут — в возможности создания закрытых иерархий классов, которые невозможно расширить в другой сборке. Пример из Windows Forms: Image — Bitmap, Metafile. Эти классы являются обёртками над объектами GDI, и новых типов объектов в этой иерархии не предусмотрено.


      Фактически, таким образом можно делать ООПшные аналоги Union-типов из ФП.


  1. navferty
    04.10.2021 23:19
    +2

    Странный аргумент в пользу query синтаксиса. Method chain так же можно сделать без вложенности:

     public IEnumerable<User> GetFriendsOfBlaBlaBla(User user, string name) =>
         user.Friends
           .Where(x => x.Name == name)
           .SelectMany(x => x.Friends)
           .SelectMany(x => x.Friends)
           .SelectMany(x => x.Friends)
           .SelectMany(x => x.Friends)
           .SelectMany(x => x.Friends)
           .SelectMany(x => x.Friends)
           .SelectMany(x => x.Friends);


  1. koodeer
    05.10.2021 07:35
    +7

    dynamic хорош для двойной диспетчеризации. В частности, паттерн Visitor с его помощью выражается намного проще.

    Query expression с применением let сильно упрощает сложные запросы. В синтаксисе методов придётся протаскивать сложный анонимный тип.

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


  1. Krey
    06.10.2021 07:52
    -2

    Вот жеж ахинея. Чуть позже разверну, если будет в этом смысл, после прочтения каментов...


    1. Krey
      06.10.2021 08:37

      Делегат это объявление сигнатуры функции. Его для этого и используют. И экспортируют. А не внутри функций. Как описать event лямбдами?

      Про query и expression tree и сахар

      Попробуйте написать аналог

      From var1 in set

      let var2=func(var1)

      let var3=func2(var1)

      From var4 in set2 join on...

      Where var4 between var2 and var3

      Select (f2=var2, f4=var4)

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

      Ну а в целом примеры какието не жизненные. Придуманные из пальца для статьи.


      1. ksbes
        06.10.2021 14:01
        -2

        Как изначально джавист, могу сказать, что как реализованы евенты в C# — это то ещё зло. Мало того что «застолбили» такое удобное название переменной в ключевые слова (туда же идёт и params), так ещё и постоянно приходится думать обо всяких утечках и «застрявших событиях». И ещё не очень совместимо с «новой идеологией», как вы и написали.
        Я-то ладно, уже давно на с# пишу — уже знаю хорошие практики, но джуны у меня регулярно косячат на event'ах. Приходится буквально семинары им устраивать с доской и партами.

        Попробуйте написать аналог ...

        Ну есть всякие SelectMany. Да и вообще иногда интересно декомпильнуть и посмотреть что именно скрывается за сахаром (Всякие Where() и лямбды — тоже сахар, кстати).


        1. Refridgerator
          06.10.2021 15:20

          А что не так с событиями на шарпе?


          1. ksbes
            06.10.2021 16:02

            Это целую статью писать надо. Но если совсем-совсем коротко: не стоит вводить в «ядро» языка то, чему место в стандартной библиотеке.


          1. VictorNS
            08.10.2021 20:21

            Я думаю, как минимум имеется ввиду, что если не отпишешься, то сборщик мусора не может подчистить объект. И потихоньку отъедается память.


  1. rundll32
    06.10.2021 13:26

    Добавьте C# в тэги, пожалуйста


  1. Sequd
    03.11.2021 14:17
    -1

    Вот простой пример, обходите в глубину сколько влезет. Без убогих конструкций.

    private static void SelectAll(IEnumerable<Friend> friends, List<Friend> all)
    {
      if (friends?.Any() == true)
      {
        all.AddRange(friends);
        var sub = friends
          .Where(x=>x.Friends != null)
          .SelectMany(x => x.Friends);
        SelectAll(sub, all);
      }
    }

    Дальше оптимизируйте под свои нужды. Избавляйтесь от рекурсии, от параметра all и тд