Наверное, почти каждый .NET-разработчик сталкивался со случаями, когда для удобства кодирования рутинных действий и сокращения boilerplate-кода при работе со стандартными типами данных не хватает возможностей стандартной же библиотеки.


И практически в каждом проекте появляются сборки и пространства имен вида Common, ProjectName.Common и т.д., содержащие дополнения для работы со стандартными типами данных: перечислениями Enums, Nullable-структурами, строками и коллекциями — перечислениями IEnumerable<T>, массивами, списками и собственно коллекциями.


Как правило, эти дополнения реализуются с помощью механизма extension methods (методов расширения). Часто можно наблюдать наличие реализаций монад, также построенных на механизме методов расширения.


(Забегая вперед — рассмотрим и вопросы, неожиданно возникающие, и которые можно не заметить, когда созданы свои расширения для IEnumerable<T>, а работа ведется с IQueryable<T>).


Написание этой статьи инспирировано прочтением давней статьи-перевода Проверки на пустые перечисления и развернувшейся дискуссии к ней.


Статья давняя, но тема по-прежнему актуальная, тем более, что код, похожий на пример из статьи, приходилось встречать в реальной работе от проекта к проекту.


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


Проблема в том, что подобные расширения в продуктовых проектах добавляются наспех, т.к. разработчики занимаются созданиям новых фич, а на создание, продумывание и отладку базовой инфраструктуры времени и ресурсов не выделяется.


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


В результате в многочисленных Common-подпапках проектов получаются залежи кода, приведенного в исходной статье:


public void Foo<T>(IEnumerable<T> items) 
{
 if(items == null || items.Count() == 0)
 {
  // Оповестить о пустом перечислении
 }
}

Автор указал на проблему с методом Count() и предложил создать такой метод расширения:


public static bool IsNullOrEmpty<T>(this IEnumerable<T> items)
{
  return items == null || !items.Any();
}

Но и наличие такого метода не решает все проблемы:


  • В комментариях развернулась дискуссия на тему, что метод Any() делает одну итерацию, что может привести к проблеме, когда последующая итерация по коллекции (которая и предполагается после проверки IsNullOrEmpty) будет произведена не с первого, а со второго элемента, и предметная логика об этом не узнает.
  • На что было получено возражение, что метод Any() для проверки создает отдельный итератор (заметим, это определенные накладные расходы).

А теперь обратим внимание, что все стандартные коллекции .NET, кроме, собственно "бесконечной" последовательности IEnumerable<T> — массивы, списки и непосредственно коллекции — реализуют стандартный интерфейс IReadOnlyCollection<T>, предоставляющий свойство Count — и не нужно никаких итераторов с накладными расходами.


Таким образом, целесообразно создать два метода расширения:


public static bool IsNullOrEmpty<T>(this IReadOnlyCollection<T> items)
{
  return items == null || items.Count == 0;
}

public static bool IsNullOrEmpty<T>(this IEnumerable<T> items)
{
  return items == null || !items.Any();
}

В таком, случае, при вызове IsNullOrEmpty<T> подходящий метод будет выбран компилятором, в зависимости от типа объекта, для которого происходит вызов расширения. Сам вызов в обоих случаях будет выглядеть одинаково.


Однако, далее в дискуссии один из комментаторов указал, что, вероятно, для IQueryable<T> (интерфейс "бесконечной" последовательности для работы с запросами к БД, наследующий от IEnumerable<T>) наиболее оптимальным будет как раз вызов метода Count().


Эта версия требует проверки, включая проверки работы с разными ORM — EF, EFCore, Linq2Sql, и, если это так, то появляется потребность в создании третьего метода.


На самом деле, для IQueryable<T> есть свои extension-реализации Any(), Count() и других методов работы с коллекциями (класс System.Linq.Queryable), которые и предназначены для работы с ORM, в отличие от аналогичных реализаций для IEnumerable<T> (класс System.Linq.Enumerable).


При этом, вероятно, Queryable-версия Any() работает даже оптимальнее, чем Queryable-проверка Count() == 0.


Для вызова нужных Queryable-версий Any() или Count(), если мы хотим вызвать именно нашу проверку IsNullOrEmpty, потребуется новый метод с IQueryable<T>-входным параметром.


Таким образом, нужно создать третий метод:


public static bool IsNullOrEmpty<T>(this IQueryable<T> items)
{
  return items == null || items.Count() == 0;
}

или


public static bool IsNullOrEmpty<T>(this IQueryable<T> items)
{
  return items == null || !items.Any();
}

В итоге, для реализации корректной для всех случаев (для всех ли?) простой null-безопасной проверки коллекций на "пустоту", нам пришлось провести небольшое исследование и реализовать три метода расширения.


А если на начальном этапе создать только часть методов, например, только первые два (не нужны эти методы; нужно делать продуктовые фичи), то может получиться вот что:


  • Как только эти методы появились, их начинают использовать в продуктовом коде.
  • В какой то момент вызовы Enumerable-версий IsNullOrEmpty проникнут в код работы с ORM, и эти вызовы точно будут работать неоптимально.
  • Что делать дальше? Добавлять Queryable-версии методов и пересобирать проект? (Добавляем только новые методы расширения, продуктовый код не трогаем — после пересборки переключение на нужные методы произойдет автоматически.) Это приведет к необходимости регрессионного тестирования всего продукта.

По этой же причине, все эти методы желательно реализовать в одной сборке и одном пространстве имен (можно в разных классах, например, EnumerableExtensions и QueryableExtensions), чтобы при случайном отключении пространства имен или сборки мы не возвратились к ситуации, когда с IQueryable<T>-коллекциями происходит работа с помощью обычных Enumerable-расширений.


На мой взгляд, обилие подобных расширений практически в каждом проекте говорит о недостаточной проработанности стандартной библиотеки и в целом модели платформы.


Часть проблем автоматически снялась бы при наличии поддержки Not Nullability в платформе, другая часть — наличием в стандартной библиотеке большего количества учитывающих более широкий спектр кейсов расширений для работы со стандартными типами данных.


Причем, реализованные на современный лад — именно в виде расширений с использованием обобщений (Generics).


Дополнительно поговорим об этой в следующей статье.


P.S. Что интересно, если посмотреть на Kotlin и его стандартную библиотеку, при разработке которого явно был внимательно изучен опыт других языков, в первую очередь, на мой взгляд — Java, C# и Ruby, то можно легко обнаружить как раз эти вещи — Not Nullability и обилие extensions, при наличии которых не возникает необходимости добавлять свои "велосипедные" реализации микробиблиотек для работы со стандартными типами.

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


  1. AgentFire
    25.02.2018 03:26

    Сколько писал код работы с БД — ну никогда, ни разу не требовалось ничего подобного (по смыслу — проверка IEnumerable/IQuerable на что-либо) типа IsNullOrEmpty.
    Как вообще может прийти в голову проверять итератор на наличие элементов? Его предназначение в другом — в том, чтобы забирать из него элементы. Нужна проверка от пустой коллекции? Используйте DefaultIfEmpty(). Нужен один элемент? (Single/First)OrDefault. И т.п.


    1. sand14 Автор
      25.02.2018 08:37
      +1

      Верно.


      Тем не менее, такие расширения я встречал в нескольких рабочих проектах. Плюс эта статья.
      Т.е. такой код пишут повсеместно.


      Понятно, что итератор должен содержать элементы, и мы должны ожидать, что сам он будет равен не null.
      Поэтому, если мы хотим как то обработать неожиданно пришедший null, то нам нужно не приравнивать его к пустому итератору, а сгенерировать NullReferenceException.


      Именно так работает вся стандартная библиотека .NET Framework (да, кстати, и JDK).
      Т.е., это общие принципы построения API — не превращать неожиданные значения аргументов в подходящие умолчания, а генерировать исключения.


      Но это уже означает проектирование API приложения в соответствии с принципом Design by Contract.


      А вот этот подход уже очень сложно встретить в реальных проектах — вместо разработки по контракту разработчики предпочитают создавать код так, что несмотря на наличие классов (POCO/POJO), слоев MVC/MVVM и прочего, данные без проверок при пересечении четко обозначенных контрактов свободно перетекают между слоями приложение.


      И тогда, если какие о данные неверные/неполные/неконсистентные, то ошибка из-за них (хорошо, если явная, приводящая к исключению, а не к неверному потоку управления в приложении), в зависимости от конкретного наполнения данных, может возникнуть не там, где они должны были бы быть проверены с немедленной генерацией исключения, а через N шагов по стеку где то совсем в другом методе или классе.


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


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


      Но это не отменяет важности вопроса, поднятого в текущей статье — недостаточной продуманности стандартных вещей в платформ, что провоцирует разработчиков создавать из проекта в проект множество похожих друга на друга Common-библиотек.
      Тем более, наличие стандартных вещей типа string.IsNullOrEmpty провоцирует разработчиков создавать такие же методы для тех же коллекций.


      1. sand14 Автор
        25.02.2018 08:45

        И тогда, если какие о данные неверные/неполные/неконсистентные, то ошибка из-за них (хорошо, если явная, приводящая к исключению, а не к неверному потоку управления в приложении), в зависимости от конкретного наполнения данных, может возникнуть не там, где они должны были бы быть проверены с немедленной генерацией исключения, а через N шагов по стеку где то совсем в другом методе или классе.

        Ну и соответственно, часто приходится наблюдать фиксы таких багов, когда в месте возникновения проблемы пишут "if (obj == null) / Try do something default /" — в то время как истинная проблема (неверные данные) возникла как минимум на один шаг по стеку выше.


      1. lair
        25.02.2018 09:30

        недостаточной продуманности стандартных вещей в платформ, что провоцирует разработчиков создавать из проекта в проект множество похожих друга на друга Common-библиотек.

        Как из одного вытекает другое? Наличие common-библиотек означает нехватку функциональности, но не обязательно непродуманность.


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


        1. sand14 Автор
          25.02.2018 10:08

          Как из одного вытекает другое? Наличие common-библиотек означает нехватку функциональности, но не обязательно непродуманность.

          Нехватка базовой функциональности — уже серьезная непродуманность.


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

          Об этом написано в конце статьи. Из конкретных вещей — Not Nullability, над которой уже идет работа в C#8, судя по rumors.


          Более подробные примеры хочу поместить в отдельную статью.


          Но давайте небольшой пример.
          Например, если мы хотим получить массив всех возможных значений Enum, то, прочитав документацию к классу Enum (или Рихтера, но в данном случае лучше документацию), понимаем, что нам нужно (и достаточно) написать такой код:


          var values = (MyEnum[])Enum.GetValues(typeof(MyEnum));

          Код не очень удобный и красивый, а ведь еще с момента появления Generics в .NET 2.0 класс Enum мог бы обзавестись новой сигнатурой GetValues:


          public static TEnum[] GetValues<TEnum>(TEnum value) where TEnum : struct
          {
              return (TEnum[])Enum.GetValues(typeof(TEnum));
          }

          И клиентский код выглядел бы чище. И даже не чище, а максимально чисто:


          var values = Enum.GetValues<MyEnum>();

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


          public static TEnum[] GetValues<TEnum>(this TEnum value) where TEnum : struct
          {
              return (TEnum[])Enum.GetValues(typeof(TEnum));
          }

          Или хотя бы так, хотя этот вариант и содержит лишний код (хотя — ок, раз в документации не сказано явно, что Enum.GetValues всегда возвращает новую копию массива, то создание "защитной копии" не помешает):


          public static TEnum[] GetValues<TEnum>(this TEnum value) where TEnum : struct
          {
              return Enum.GetValues(typeof(TEnum)).Cast<TEnum>().ToArray();
          }
          

          Но доводилось встречать и что-то подобное (на самом деле еще грязнее чем этот код, но совсем уж трешевые примеры и приводить не хочется):


          public static TEnum[] GetValues<TEnum>(this TEnum value) where TEnum : struct
          {
              var result = new List<TEnum>();
              foreach (var item in Enum.GetValues(typeof(TEnum)).Cast<TEnum>())
              {
                  result.Add(item);
              }
              return result.ToArray();
          }
          

          Возвращаясь к вашему вопросу "что делать" — для данного кейса всего лишь добавить в Enum эталонную Generic-реализацию GetValues<TEnum>.


          1. lair
            25.02.2018 10:51

            Нехватка базовой функциональности — уже серьезная непродуманность.

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


            Из конкретных вещей — Not Nullability

            (non) nullability — это очень сложно (именно с точки зрения продуманности). И как бы это помогло в случае, когда код, из которого мы получаем данные, возвращает nullable?


            еще с момента появления Generics в .NET 2.0 класс Enum мог бы обзавестись новой сигнатурой GetValues

            Мог бы. Но это снова пример недостающей функциональности (а не непродуманности), а количество ресурсов у разработчиков ограничено.


            Кстати, а что должен делать предлагаемый код, если я скажу Enum.GetValues<Decimal>()?


            1. sand14 Автор
              25.02.2018 11:05

              Мог бы. Но это снова пример недостающей функциональности (а не непродуманности), а количество ресурсов у разработчиков ограничено.

              Т.е., на добавление весьма спорного метода bool Enum.HasFlag(Enum flag) (надеюсь, знаете, почему спорного) у производителя ресурсов хватило, а на добавление TEnum Enum.GetValue<TEnum>() — нет.
              Что же, ок.


              Кстати, а что должен делать предлагаемый код, если я скажу Enum.GetValues<Decimal>()?

              Не поверите, но то же самое, что и код Enum.GetValues(typeof(TEnum)) (ссылки на доки посылать не буду — гугль в помощь; а можно ведь еще и исходники посмотреть).
              Только в случае Generic-версии мы имеем возможность хотя бы часть неподходящих типов отсечь в статике.
              И кстати, невозможность указать в Generics ограничение по enum — тоже пример непродуманности модели.


              1. lair
                25.02.2018 11:15

                Т.е., на добавление весьма спорного метода Enum.HasFlag(Enum) (надеюсь, знаете, почему спорного) у производителя ресурсов хватило, а на добавление Enum.GetValue<TEnum> — нет

                Ну да, производитель сам решает, на что ему аллоцировать ресурсы.


                Не поверите, но то же самое, что и код Enum.GetValues(typeof(TEnum))

                Поверю как раз. И считаю это поведение непродуманным. Так зачем добавлять непродуманный код.


                И кстати, невозможность указать в Generics ограничение по enum — тоже пример непродуманности модели.

                Вот снова: почему непродуманности? Почему вы не допускаете, что люди подумали и решили, что это нерентабельно? Чтение ответов Липперта на вопросы "а почему в C# нет x" часто показывает, что за той или иной тривиальной, казалось бы, фичей есть нетривиальные побочные эффекты.


                1. sand14 Автор
                  25.02.2018 11:24
                  +1

                  Почему вы не допускаете, что люди подумали и решили, что это нерентабельно? Чтение ответов Липперта на вопросы "а почему в C# нет x" часто показывает, что за той или иной тривиальной, казалось бы, фичей есть нетривиальные побочные эффекты.

                  Вы исходите из того, что все решения они приняли верно?
                  Они сами часть вещей меняли с временем, причем даже ломая backward compatilility.


                  1. lair
                    25.02.2018 11:33

                    Я исхожу из того, что они подумали. Они могли ошибиться (все ошибаются), но они подумали. Иными словами, поддержки ограничения по enum в дженериках не было не потому, что никому в LDT не пришло в голову, что такое ограничение не будет нужно, а по другим причинам. Собственно, вот, что пишет Липперт (и там же в комментах есть примеры проблем с очевидным дизайном):


                    As I'm fond of pointing out, ALL features are unimplemented until someone designs, specs, implements, tests, documents and ships the feature. So far, no one has done that for this one. There's no particularly unusual reason why not; we have lots of other things to do, limited budgets, and this one has never made it past the "wouldn't this be nice?" discussion in the language design team.


                  1. a-tk
                    25.02.2018 12:02

                    Они сами часть вещей меняли с временем, причем даже ломая backward compatilility.

                    Ой, и много таких вещей было? Я две только знаю. Ну ладно, две с половиной.


                    1. sand14 Автор
                      25.02.2018 12:50

                      Только навскидку:


                      • ковариантность Generics — .NET 4.0.
                      • итерируемая переменная в foreach — C# 5.0.
                      • предположительные имена в Named Tuples — C# 7.1->7.2.
                      • кое-какие вещи при работе с сетью — .NET 4.0<->4.5 (рантайм один, а библиотеки работают по разному, да и не только сети это могло касаться, если покопать).
                      • добавление IDisposable к IEnumerable(T) — .NET 4.0.
                      • неясность с порядком итерации IEnumerable(T) — вначале в доках было написано, что сохранение порядка обхода по упорядоченной коллекции (массив, список) предполагается, но не гарантируется, потом эти разъяснения вроде пропали, и с вопросом еще большая неясность.

                      Думаю, список неполный.


                      Я уж не говорю про такие вещи, как отказ от поддержки J#, переход на project.json и обратно на csproj.
                      А неполная совместимость .NET 1.x -> .NET 2.0?


                      1. a-tk
                        25.02.2018 13:03

                        Давайте разберёмся, что такое обратная совместимость.
                        Это ситуации, в которых существующий код может не скомпилироваться или существенным образом изменить поведение.

                        1. Ковариантность generic-интерфейсов и делегатов была введена путём расширения синтаксиса, а не путём изменения, ломающего код.
                        2. Да.
                        3. Нет. Старый синтаксис продолжал работать.
                        4. Нет. Опять-таки, старый код продолжает работать.
                        5. Не к IEnumerable, а к IEnumerator, и не в .NET 4.0, а в 2.0.
                        6. Как неясность документации относится к реализации?

                        Мой список следующий:
                        1. Введение ключевого слова var в некоторых контекстах могло ломать компиляцию, если имеется тип var (вероятность минимизируется при следовании гайдлайнов по именованию), либо если были переменные/поля с именем var.
                        2. Поведение итератора при использовании в замыкании: до 4.0 замыкался объект итератора, начиная с 4.0 — копия текущего значения итератора. Вероятность нарваться невысока, поскольку редко кому нужен был захват именно последнего значения. Чаще делали локальную переменную с копией значения и замыкали её.
                        3. 1.x -> 2.0 — согласен, но это весьма специфический кейс, поэтому считаем его за половинку.

                        J# не имеет отношения к совместимости версий C#
                        project.json не имеет отношения к совместимости версий C#
                        Можно ещё порассуждать о том, как эволюционировал VB.NET, но это тоже мимо кассы.


                        1. lair
                          25.02.2018 13:05

                          Ковариантность generic-интерфейсов и делегатов была введена путём расширения синтаксиса, а не путём изменения, ломающего код.

                          К сожалению, все не так просто.


                          1. a-tk
                            25.02.2018 13:08

                            Опять-таки, какова была вероятность на такое различие при сборке старого кода в новых условиях?
                            Но в целом согласен, да.
                            Три с половиной.


                            1. lair
                              25.02.2018 13:11

                              Ну, собственно, они и решили, что плюсы от введения сильно перевешивали потенциальные проблемы с breaking changes. Я же не говорю, что не надо было так делать, я просто говорю, что формально это breaking change.


                              1. a-tk
                                25.02.2018 13:16

                                Полностью согласен.
                                Собственно, любое breaking change должно давать много плюсов, имея минимальную вероятность всё сломать.


                        1. sand14 Автор
                          25.02.2018 13:19

                          1. Нет. Старый синтаксис продолжал работать.

                          Синтаксис продолжает работать, но вот поведение кода скомпилированного компиляторами версий 7.1 и 7.2 в определенном случае — различно.
                          И это хуже, чем некомпиляция при, например, вводе нового ключевого слова — а в коде есть переменная с таким именем.
                          Но случай редкий — вряд ли на C# 7.1 успели написать специфический код.


                          1. Не к IEnumerable, а к IEnumerator, и не в .NET 4.0, а в 2.0.

                          Верно, спасибо, что напомнили.
                          Проверил на MSDN — да, это действительно относится к крупному вопросу NET 1 -> 2.


                          J# не имеет отношения к совместимости версий C#
                          project.json не имеет отношения к совместимости версий C#
                          Можно ещё порассуждать о том, как эволюционировал VB.NET, но это тоже мимо кассы.

                          Не совсем так. В случае с C# сложно определить границы, где заканчивается C#, а начинается стандартная библиотека или виртуальная машина.
                          Из C# мы могли пользоваться стандартной библиотекой J#, к примеру, и это ничем не отличалось от обращения к обычной стандартной библиотекой.
                          Пример, конечно, натянутый, но когда мы пишем, то пользуемся всей инфраструктурой, а не только языком.


                          Да и VB — его стандартная библиотека является часть основной стандартной библиотеки, и из C# можно пользоваться VB-фичами, не устанавливая дополнительно что-то отдельное, как в случае J#.


              1. nporaMep
                25.02.2018 11:18
                +1

                Скорее пример влияния backward compatibiltiy и того что они это делали в 2000-м году. Все умные бичевать это в 2018-м и говорить как модно :)


                1. sand14 Автор
                  25.02.2018 11:29
                  +1

                  Все так, но что мешало в еще 2005-м вместе с дженериками добавить и — как пример — тот же
                  public static TEnum[] GetValues<TEnum>(this TEnum value) where TEnum: struct
                  хотя бы как однострочную(!) обертку над старой версией, которая могла бы быть помечена как Obsolete, а потом и вовсе переехать в внутреннюю реализацию?
                  Вряд ли отсутствие ресурсов.


                  1. nporaMep
                    25.02.2018 11:34

                    Хз, про GetValues я только знаю что он ОЧЕНЬ медленный, обернув его в дженерики, не меняя ИЛ под ним, это ситуации не спасает. Оба метода были бы ужасны. Но согласен довольно часто такой экстеншн пишут.


                  1. nporaMep
                    25.02.2018 11:37
                    +1

                    и да, как сейчас хорошо замечают нынешние дизайнеры C# — в 2000-2005-м C# писали С++ программисты, которые особо не пользовались сами шарпом. Плюс микрософт был полностью close source и никакого фидбека микрософт не спрашивал по ходу разработки. В принципе такой был весь ентерпрайз в те времена.


                    1. sand14 Автор
                      25.02.2018 11:41

                      Вот это похоже на правду.


              1. a-tk
                25.02.2018 12:01

                Т.е., на добавление весьма спорного метода Enum.HasFlag(Enum) (надеюсь, знаете, почему спорного) у производителя ресурсов хватило, а на добавление Enum.GetValue[TEnum] — нет

                Можете предложить корректный вариант реализации обобщённого метода Enum.HasFlag(Enum)? В идеале, конечно с тайпчеком во время компиляции и без динамика, рефлексии и боксинга в рантайме, но, ставлю бутылку виски, у Вас это не получится.

                (хабр погрыз скобки — изменил на квадратные в цитате)


                1. sand14 Автор
                  25.02.2018 12:29

                  Так получается, что пока Enum имеет такую модель, какую имеет, HasFlag — лишний.
                  А сейчас получается, что в .NET Core его вроде оптимизируют, чтобы в рантайме не было боксинга (и прочие улучшения), но в статике никак тайп чек не добавишь.

                  И — а это точно хорошо, когда есть такая магия, когда в райнтайме некоторые типы становится привелигированными, и работают не так, как это можно ожидать из правил языка?
                  На мой взгляд, магия должна быть в синтаксисе языка, а не в обработке типов данных (отличие которых только в том, что один находится в сборке System, другой — в MyPorject).

                  А вот добавить Generic-типизированный GetValues — почему то не добавили, хотя это ничего не стоит.


                  1. lair
                    25.02.2018 12:33

                    это ничего не стоит

                    В комментах по приведенной выше ссылке есть разбор одного такого "ничего не стоит".


                    1. sand14 Автор
                      25.02.2018 13:05

                      Там пишут, почему нет Generic-версии парсинга Enum в C#, в то время как в IL она есть?


                      Ну так и про фильтры исключений говорили "сложно", хотя в IL фильтры есть, и изначально были были в VB как надстройка над IL.
                      Потом добавили и в C#.


                      Что касается Generic-версии GetValues, тут скорее дело в том, что разработчики платформы не ожидают, что GetValues будут часто пользоваться (об этом — у Рихтера), поэтому и не переделывали.


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


                      1. lair
                        25.02.2018 13:09

                        Там пишут, почему нет Generic-версии парсинга Enum в C#, в то время как в IL она есть?

                        Да, я уже цитировал.


                        Но потом добавили и в C#.

                        Потому что появилось обоснование для применения ресурсов на это?


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

                        В модели Enum действительно не все "так", но если разработчики платформы считают, что какая-то функциональность не нужна — вы можете пытаться или убедить их в обратном, или реализовать ее самостоятельно (и отправить pull-request, теперь это стало намного проще).


            1. sand14 Автор
              25.02.2018 11:14

              (non) nullability — это очень сложно (именно с точки зрения продуманности). И как бы это помогло в случае, когда код, из которого мы получаем данные, возвращает nullable?

              Сложно, но уже делается. А в Kotlin — уже сделано.


              Помогает это очень просто:


              • Пусть наш метод принимает на вход коллекцию, вот только если мы параметр коллекции указали как IEnumerable<T>, а не IEnumerable<T>?, то код не скомпилируется, если вызывающая сторона объявила коллекцию как IEnumerable<T>? и перед вызовом не сделала что-то типа collection.OrEmpty().
              • Т.е. проблема остается там, где и должна — в месте формирования и подготовки данных для передачи в метод. И в нашем методе уже нет необходимости реализовать контракт с генерацией NullReferenceException или писать boilerplate-код, приравнивающий null с пустой коллекции.


              1. lair
                25.02.2018 11:18

                Сложно, но уже делается.

                … и вызывает вопросы. А когда будет выпущено — будет вызывать жалобы на непродуманность.


                А в Kotlin — уже сделано.

                Опираясь (в том числе) на опыт C# и Java, в которых этого сделано не было. А на что было опираться авторам C#?


                И в нашем методе уже нет необходимости реализовать контракт с генерацией NullReferenceException или писать boilerplate-код, приравнивающий null к пустой коллекции.

                Зато его надо писать во всех местах, которые получают откуда-то (из внешнего кода) IEnumerable<T>? и вызывают наш метод. Вы решили проблему в методе, но не решили ее системно.


                1. sand14 Автор
                  25.02.2018 11:38
                  +1

                  А когда будет выпущено — будет вызывать жалобы на непродуманность.

                  В случае C# — да, будет вызывать.


                  А на что было опираться авторам C#?

                  У них было достаточно возможностей опираться на своей же опыт, что они отчасти и делали (и делают), но не в полной мере.
                  И не очень похоже, что из-за отсутствия ресурсов.


                  Зато его надо писать во всех местах, которые получают откуда-то (из внешнего кода) IEnumerable<T>? и вызывают наш метод. Вы решили проблему в методе, но не решили ее системно.

                  А как вы хотели? Перед передачей данных в метод вы должны их подготовить, а не рассчитывать на то, что метод будет эвристически угадывать, что делать с неверными данными или неверными указателями на данные.
                  На этих принципах и сейчас построены базовые библиотеки .NET/JDK.
                  Это независимо от Not Nullabilty.


                  А в случае Not Nullabilty и данными, приезжающим из внешних источников, это уже ваша работа как архитектора — не везде по коду обращаться к внешним источникам и бойлерплейтить вызовы OrEmpty(), а инкапсулировать такие вызовы в отдельном слое, а в других слоях работать с уже нормализованными данными.
                  Или принять решение и вызывать OrEmpty (или обрабатывать как то еще) всегда перед передачей в методы — больше кода, чем сейчас (без Not Nullability) все равно не напишите.


                  1. lair
                    25.02.2018 11:42
                    +1

                    В случае C# — да, будет вызывать.

                    … вы так говорите, как будто где-то не будет.


                    У них было достаточно возможностей опираться на своей же опыт

                    Которого на момент выпуска .net 1 было намного меньше, чем сейчас, не правда ли?


                    А как вы хотели?

                    А я хотел услышать от вас "продуманное решение", которое позволит полностью избавиться от изначально описанных вами методов-расширений.


                  1. lair
                    25.02.2018 12:07

                    А в случае Not Nullabilty и данными, приезжающим из внешних источников

                    И еще. Вот убрали мы проблему с null, окей. Но исходный метод делал две вещи: он проверял на null и на пустоту — и именно проверка на пустоту заставила вас написать три оверлоада, потому что вы считаете, что эту проверку эффективнее делать тремя разными способами. Какое продуманное решение вы ждете от платформенной команды для этой проблемы? (ну, кроме того, чтобы убрать текущую абстракцию IQueryable, что, будем честными, не представляется возможным)


                    1. sand14 Автор
                      25.02.2018 12:21

                      Какое продуманное решение вы ждете от платформенной команды для этой проблемы?

                      1. Not Nullability — это избавит нас в большинстве случае от необходимости проверять одновременно на null и Empty.
                        Для проверки на Empty мы сразу будем вызывать уже существующие Any() или Count(), а в зависимости от типа коллекции — IEnumerable или IQueryable, компилятор будет подставлять подходящий extension.
                        Именно так происходит и сейчас, если null нас не заботит, либо если мы его проверку вынесли в контракт с генерацией исключения.


                      2. Для случаев, когда нам нужно поработать с nullable TSomeCollection? из внешних источников — возможно, будет полезным наличие в стандартной библиотеке как раз тех трех (или более) оверлоадов.
                        Собственно, как и сейчас происходит — куча оверлоадов с одинаковым именем в сигнатуре (Enumerable, Queryable, Convert, etc), среди которых нужный автоматически выбирается компилятором по типу данных.

                      Другими словами, если разработчики продуктовой функциональности постоянно пишут одни и те же экстеншены, значит, разработчикам платформы нужно подумать либо о модели, с который эти экстеншены не понадобятся, либо включить эти экстеншены (или предложить более подходящие) в стандартный комплект.


                      1. lair
                        25.02.2018 12:32

                        Для проверки на Empty мы сразу будем вызывать уже существующие Any() или Count(), а в зависимости от типа коллекции — IEnumerable или IQueryable, компилятор будет подставлять подходящий extension.

                        Тогда почему вы не ограничились двумя оверлоадами — одним для IEnumerable, а вторым — для IQueryable, в обоих из которых не вызвали Any?


                        значит, разработчикам платформы нужно подумать либо о модели, с который эти экстеншены не понадобятся

                        Вот мне интересно услышать про такую модель, да. На примере Empty для множеств и корректной обработки уникальности элемента (поиск по ключу) для них же (Single не устраивает по понятным причинам: хотим понятные исключения вместо InvalidOperation).


                        включить эти экстеншены (или предложить более подходящие) в стандартный комплект.

                        Возвращаемся к вопросу "почему в .net/C# нет фичи x". Ответ выше, и он чаще всего не "не подумали".


              1. nporaMep
                25.02.2018 12:00

                на то как это сделано в Kotlin тоже кстати немало плохих фидбеков


                1. sand14 Автор
                  25.02.2018 12:10

                  Есть такое. Ведь Kotlin тоже в какой-то степени "пионер", да и при его разработке приходилось учитывать совместимость с Java.


                  Но все равно, Kotlin это прямо отдохновение: куча вещей, которая раньше бойлерплейтилась, теперь встроена в модель — хотя бы такие очевидные вещи вещи, как backing fields, видимые внутри свойства, и delegated properties.
                  Причем именно эти вещи привел в пример, т.к. о них думалось при работе с C# до того, как узнал о выходе Kotlin.


                  1. Szer
                    25.02.2018 18:03

                    Ведь Kotlin тоже в какой-то степени "пионер"

                    В январе 2016 в F# впилили Null Safety примерно как это сделали позже в Kotlin. В самом языке и его стандартной либе нормальный null-safety (ни присвоить null, ни даже проверить на null), а интероп с C# (или с Java-либами в случае с Kotlin) уже может кидать NRE.


                    Да и F# не был первый. Так что Котлину было у кого подсмотреть.


          1. Interreto
            26.02.2018 08:11
            -2

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

            Enum<SomeEnemType>.GetValues()
            А ещё непонятно какой уличной магией вы добавили статический метод в абстрактный класс Enum через экстеншен О_о


            1. mayorovp
              26.02.2018 08:43

              Если Enum сделать параметризованным — получится просто SomeEnemType.GetValues()...


              1. Interreto
                26.02.2018 11:03
                -1

                Речь о абстракции Enum в C#, referencesource.microsoft.com/#mscorlib/system/enum.cs


                1. lair
                  26.02.2018 11:14

                  … и что? Почему нельзя сделать GetValues у типов, унаследованных от System.Enum?


                1. mayorovp
                  26.02.2018 11:56
                  +1

                  Вот про нее я и говорю. Если сделать ее параметризованной — то все ее методы автоматически попадут в производные классы. То есть в SomeEnemType.


                  1. a-tk
                    26.02.2018 20:48
                    -1

                    Есть ещё вариант как было сделано с делегатами. Метод Invoke автогенерируемый, аргументы и возвращаемое значение зависят от сигнатуры в объявлении. Аналогичный функционал мог бы быть выполнен компилятором и обходился бы действительно даром. Но завернуть в generic стало бы невозможно такое, как, впрочем, попытки параметризовать что-нибудь типом-делегатом.


      1. Bonart
        25.02.2018 13:33

        Тем не менее, такие расширения я встречал в нескольких рабочих проектах. Плюс эта статья.

        Да, такие вещи называются антипаттернами.


        Т.е. такой код пишут повсеместно.

        Нет, лично я не видел ни разу.


        1. sand14 Автор
          25.02.2018 13:41

          Т.е. такой код пишут повсеместно.

          Нет, лично я не видел ни разу.

          Вам повезло в хорошем смысле слова.

          Но неужели ни разу не видели string.IsNullOrEmpty(string)?
          Ведь это точно тот же антипаттерн, но включен в стандартную библиотеку и применяется еще шире.


          1. mayorovp
            25.02.2018 14:53

            Потому что отсутствующие строки могут в программе возникать из многих источников, а отсутствующие коллекции — обычно ошибка.


          1. dmitry_dvm
            25.02.2018 15:13

            Почему вдруг string.IsNullOrEmpty(string) антипаттерн? Кроме этого вашего бэкенда есть еще ui, где IsNullOrEmpty и IsNullOrWhiteSpace необходимы, как воздух. Вся эта якобы непродуманность вами видится потому что вы думаете только о своей предметной области, а проектировщики языка — о всех возможных.


            1. sand14 Автор
              25.02.2018 15:22

              Проверка WhiteSpace — да, очень нужна, и в первую очередь для UI.
              Но IsNullOrXxx? — если писать код по хорошему, откуда на UI null-строки?
              Как из поля ввода вам может придти null?
              К примеру, есть три поля ФИО — если какое то их них не заполнено, то из него должна придти пустая строка, а не null.

              Как раз в бек-енде, пока не появится not nullability, проверка строк на null актуальнее.
              Да и после появления not nullability останутся внешние источники — JSON'ы из сети, строки из БД с null колонками.


              1. lair
                25.02.2018 16:18

                К примеру, есть три поля ФИО — если какое то их них не заполнено, то из него должна придти пустая строка, а не null.

                К сожалению, разработчики половины модел-биндеров забыли с вами посоветоваться, и там придет null.


              1. dmitry_dvm
                25.02.2018 22:20

                Пока юзер не ввел что-то в поле связанная строка остается нетронутой, т.е. null-ом.


          1. Bonart
            25.02.2018 16:26

            Но неужели ни разу не видели string.IsNullOrEmpty(string)?

            Видел и использовал.


            Ведь это точно тот же антипаттерн

            Не тот же и не антипаттерн.
            Проверка длины строки не меняет ее состояния и имеет сложность O(1), в отличие от IEnumerable и IQueryable


            1. mayorovp
              26.02.2018 08:45
              +1

              Поправка: у нормальных IEnumerable и IQueryable состояние от вызова Any тоже не меняется...


              Исключение — штуки вроде той которую возвращает GetConsumingEnumerable для BlockingCollection


              1. Bonart
                26.02.2018 18:35

                Грамотные разработчики IEnumerable два раза не перебирают, так как не надеются на «нормальность», а знают, что IEnumerable второй проход не гарантирует.


  1. a-tk
    25.02.2018 11:54
    -1

    Где ж Вы были 10 лет назад, когда эта фишка только появилась…


  1. tsul
    27.02.2018 14:33
    +1

    Перегрузки для IReadOnlyCollection<T> недостаточно. К примеру, для IDictionary<K,V> вызывается вариант с IEnumerable<T>, хотя IDictionary<K,V> is IReadOnlyCollection<KeyValuePair<K,V>>. Нужна ещё перегрузка для ICollection<T>. Но, например, для List<T> тогда получается ambiguous call… В общем, не всё так просто, без if (или pattern matching?) не обойтись..