Наверное, почти каждый .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)
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?) не обойтись..
AgentFire
Сколько писал код работы с БД — ну никогда, ни разу не требовалось ничего подобного (по смыслу — проверка IEnumerable/IQuerable на что-либо) типа
IsNullOrEmpty
.Как вообще может прийти в голову проверять итератор на наличие элементов? Его предназначение в другом — в том, чтобы забирать из него элементы. Нужна проверка от пустой коллекции? Используйте DefaultIfEmpty(). Нужен один элемент? (Single/First)OrDefault. И т.п.
sand14 Автор
Верно.
Тем не менее, такие расширения я встречал в нескольких рабочих проектах. Плюс эта статья.
Т.е. такой код пишут повсеместно.
Понятно, что итератор должен содержать элементы, и мы должны ожидать, что сам он будет равен не null.
Поэтому, если мы хотим как то обработать неожиданно пришедший null, то нам нужно не приравнивать его к пустому итератору, а сгенерировать NullReferenceException.
Именно так работает вся стандартная библиотека .NET Framework (да, кстати, и JDK).
Т.е., это общие принципы построения API — не превращать неожиданные значения аргументов в подходящие умолчания, а генерировать исключения.
Но это уже означает проектирование API приложения в соответствии с принципом Design by Contract.
А вот этот подход уже очень сложно встретить в реальных проектах — вместо разработки по контракту разработчики предпочитают создавать код так, что несмотря на наличие классов (POCO/POJO), слоев MVC/MVVM и прочего, данные без проверок при пересечении четко обозначенных контрактов свободно перетекают между слоями приложение.
И тогда, если какие о данные неверные/неполные/неконсистентные, то ошибка из-за них (хорошо, если явная, приводящая к исключению, а не к неверному потоку управления в приложении), в зависимости от конкретного наполнения данных, может возникнуть не там, где они должны были бы быть проверены с немедленной генерацией исключения, а через N шагов по стеку где то совсем в другом методе или классе.
И итоге вместо инкапсуляции данных приходится говорить о глобальном состоянии.
Спасибо за этот комментарий. Это очень важный вопрос, и его есть смысл обсудить в отдельной статье.
Но это не отменяет важности вопроса, поднятого в текущей статье — недостаточной продуманности стандартных вещей в платформ, что провоцирует разработчиков создавать из проекта в проект множество похожих друга на друга Common-библиотек.
Тем более, наличие стандартных вещей типа string.IsNullOrEmpty провоцирует разработчиков создавать такие же методы для тех же коллекций.
sand14 Автор
Ну и соответственно, часто приходится наблюдать фиксы таких багов, когда в месте возникновения проблемы пишут "if (obj == null) / Try do something default /" — в то время как истинная проблема (неверные данные) возникла как минимум на один шаг по стеку выше.
lair
Как из одного вытекает другое? Наличие common-библиотек означает нехватку функциональности, но не обязательно непродуманность.
Что, впрочем, не отменяет более важного вопроса: что же именно здесь непродумано, и как должно быть сделано, чтобы было лучше?
sand14 Автор
Нехватка базовой функциональности — уже серьезная непродуманность.
Об этом написано в конце статьи. Из конкретных вещей — Not Nullability, над которой уже идет работа в C#8, судя по rumors.
Более подробные примеры хочу поместить в отдельную статью.
Но давайте небольшой пример.
Например, если мы хотим получить массив всех возможных значений Enum, то, прочитав документацию к классу Enum (или Рихтера, но в данном случае лучше документацию), понимаем, что нам нужно (и достаточно) написать такой код:
Код не очень удобный и красивый, а ведь еще с момента появления Generics в .NET 2.0 класс Enum мог бы обзавестись новой сигнатурой GetValues:
И клиентский код выглядел бы чище. И даже не чище, а максимально чисто:
Соответственно, это провоцирует разработчиков добавлять свои велосипедные расширения.
И хорошо еще, если расширение написано так:
Или хотя бы так, хотя этот вариант и содержит лишний код (хотя — ок, раз в документации не сказано явно, что Enum.GetValues всегда возвращает новую копию массива, то создание "защитной копии" не помешает):
Но доводилось встречать и что-то подобное (на самом деле еще грязнее чем этот код, но совсем уж трешевые примеры и приводить не хочется):
Возвращаясь к вашему вопросу "что делать" — для данного кейса всего лишь добавить в Enum эталонную Generic-реализацию GetValues<TEnum>.
lair
Нет, это в первую очередь вопрос, какая функциональность "базовая". И сколько производитель готов ждать до выхода на рынок. И вполне может быть, что кто-то подумал и осознанно решил, что на добавление этой функциональности времени нет.
(non) nullability — это очень сложно (именно с точки зрения продуманности). И как бы это помогло в случае, когда код, из которого мы получаем данные, возвращает nullable?
Мог бы. Но это снова пример недостающей функциональности (а не непродуманности), а количество ресурсов у разработчиков ограничено.
Кстати, а что должен делать предлагаемый код, если я скажу
Enum.GetValues<Decimal>()
?sand14 Автор
Т.е., на добавление весьма спорного метода bool Enum.HasFlag(Enum flag) (надеюсь, знаете, почему спорного) у производителя ресурсов хватило, а на добавление TEnum Enum.GetValue<TEnum>() — нет.
Что же, ок.
Не поверите, но то же самое, что и код Enum.GetValues(typeof(TEnum)) (ссылки на доки посылать не буду — гугль в помощь; а можно ведь еще и исходники посмотреть).
Только в случае Generic-версии мы имеем возможность хотя бы часть неподходящих типов отсечь в статике.
И кстати, невозможность указать в Generics ограничение по enum — тоже пример непродуманности модели.
lair
Ну да, производитель сам решает, на что ему аллоцировать ресурсы.
Поверю как раз. И считаю это поведение непродуманным. Так зачем добавлять непродуманный код.
Вот снова: почему непродуманности? Почему вы не допускаете, что люди подумали и решили, что это нерентабельно? Чтение ответов Липперта на вопросы "а почему в C# нет x" часто показывает, что за той или иной тривиальной, казалось бы, фичей есть нетривиальные побочные эффекты.
sand14 Автор
Вы исходите из того, что все решения они приняли верно?
Они сами часть вещей меняли с временем, причем даже ломая backward compatilility.
lair
Я исхожу из того, что они подумали. Они могли ошибиться (все ошибаются), но они подумали. Иными словами, поддержки ограничения по enum в дженериках не было не потому, что никому в LDT не пришло в голову, что такое ограничение не будет нужно, а по другим причинам. Собственно, вот, что пишет Липперт (и там же в комментах есть примеры проблем с очевидным дизайном):
a-tk
Ой, и много таких вещей было? Я две только знаю. Ну ладно, две с половиной.
sand14 Автор
Только навскидку:
Думаю, список неполный.
Я уж не говорю про такие вещи, как отказ от поддержки J#, переход на project.json и обратно на csproj.
А неполная совместимость .NET 1.x -> .NET 2.0?
a-tk
Давайте разберёмся, что такое обратная совместимость.
Это ситуации, в которых существующий код может не скомпилироваться или существенным образом изменить поведение.
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, но это тоже мимо кассы.
lair
К сожалению, все не так просто.
a-tk
Опять-таки, какова была вероятность на такое различие при сборке старого кода в новых условиях?
Но в целом согласен, да.
Три с половиной.
lair
Ну, собственно, они и решили, что плюсы от введения сильно перевешивали потенциальные проблемы с breaking changes. Я же не говорю, что не надо было так делать, я просто говорю, что формально это breaking change.
a-tk
Полностью согласен.
Собственно, любое breaking change должно давать много плюсов, имея минимальную вероятность всё сломать.
sand14 Автор
Синтаксис продолжает работать, но вот поведение кода скомпилированного компиляторами версий 7.1 и 7.2 в определенном случае — различно.
И это хуже, чем некомпиляция при, например, вводе нового ключевого слова — а в коде есть переменная с таким именем.
Но случай редкий — вряд ли на C# 7.1 успели написать специфический код.
Верно, спасибо, что напомнили.
Проверил на MSDN — да, это действительно относится к крупному вопросу NET 1 -> 2.
Не совсем так. В случае с C# сложно определить границы, где заканчивается C#, а начинается стандартная библиотека или виртуальная машина.
Из C# мы могли пользоваться стандартной библиотекой J#, к примеру, и это ничем не отличалось от обращения к обычной стандартной библиотекой.
Пример, конечно, натянутый, но когда мы пишем, то пользуемся всей инфраструктурой, а не только языком.
Да и VB — его стандартная библиотека является часть основной стандартной библиотеки, и из C# можно пользоваться VB-фичами, не устанавливая дополнительно что-то отдельное, как в случае J#.
nporaMep
Скорее пример влияния backward compatibiltiy и того что они это делали в 2000-м году. Все умные бичевать это в 2018-м и говорить как модно :)
sand14 Автор
Все так, но что мешало в еще 2005-м вместе с дженериками добавить и — как пример — тот же
public static TEnum[] GetValues<TEnum>(this TEnum value) where TEnum: struct
хотя бы как однострочную(!) обертку над старой версией, которая могла бы быть помечена как Obsolete, а потом и вовсе переехать в внутреннюю реализацию?
Вряд ли отсутствие ресурсов.
nporaMep
Хз, про GetValues я только знаю что он ОЧЕНЬ медленный, обернув его в дженерики, не меняя ИЛ под ним, это ситуации не спасает. Оба метода были бы ужасны. Но согласен довольно часто такой экстеншн пишут.
nporaMep
и да, как сейчас хорошо замечают нынешние дизайнеры C# — в 2000-2005-м C# писали С++ программисты, которые особо не пользовались сами шарпом. Плюс микрософт был полностью close source и никакого фидбека микрософт не спрашивал по ходу разработки. В принципе такой был весь ентерпрайз в те времена.
sand14 Автор
Вот это похоже на правду.
a-tk
Можете предложить корректный вариант реализации обобщённого метода Enum.HasFlag(Enum)? В идеале, конечно с тайпчеком во время компиляции и без динамика, рефлексии и боксинга в рантайме, но, ставлю бутылку виски, у Вас это не получится.
(хабр погрыз скобки — изменил на квадратные в цитате)
sand14 Автор
Так получается, что пока Enum имеет такую модель, какую имеет, HasFlag — лишний.
А сейчас получается, что в .NET Core его вроде оптимизируют, чтобы в рантайме не было боксинга (и прочие улучшения), но в статике никак тайп чек не добавишь.
И — а это точно хорошо, когда есть такая магия, когда в райнтайме некоторые типы становится привелигированными, и работают не так, как это можно ожидать из правил языка?
На мой взгляд, магия должна быть в синтаксисе языка, а не в обработке типов данных (отличие которых только в том, что один находится в сборке System, другой — в MyPorject).
А вот добавить Generic-типизированный GetValues — почему то не добавили, хотя это ничего не стоит.
lair
В комментах по приведенной выше ссылке есть разбор одного такого "ничего не стоит".
sand14 Автор
Там пишут, почему нет Generic-версии парсинга Enum в C#, в то время как в IL она есть?
Ну так и про фильтры исключений говорили "сложно", хотя в IL фильтры есть, и изначально были были в VB как надстройка над IL.
Потом добавили и в C#.
Что касается Generic-версии GetValues, тут скорее дело в том, что разработчики платформы не ожидают, что GetValues будут часто пользоваться (об этом — у Рихтера), поэтому и не переделывали.
Но почему то разработчики таки часто пользуются GetValues — значит, что-то не так в самой модели Enum, если им пользуются не так, как задумывалось.
lair
Да, я уже цитировал.
Потому что появилось обоснование для применения ресурсов на это?
В модели
Enum
действительно не все "так", но если разработчики платформы считают, что какая-то функциональность не нужна — вы можете пытаться или убедить их в обратном, или реализовать ее самостоятельно (и отправить pull-request, теперь это стало намного проще).sand14 Автор
Сложно, но уже делается. А в Kotlin — уже сделано.
Помогает это очень просто:
lair
… и вызывает вопросы. А когда будет выпущено — будет вызывать жалобы на непродуманность.
Опираясь (в том числе) на опыт C# и Java, в которых этого сделано не было. А на что было опираться авторам C#?
Зато его надо писать во всех местах, которые получают откуда-то (из внешнего кода)
IEnumerable<T>?
и вызывают наш метод. Вы решили проблему в методе, но не решили ее системно.sand14 Автор
В случае C# — да, будет вызывать.
У них было достаточно возможностей опираться на своей же опыт, что они отчасти и делали (и делают), но не в полной мере.
И не очень похоже, что из-за отсутствия ресурсов.
А как вы хотели? Перед передачей данных в метод вы должны их подготовить, а не рассчитывать на то, что метод будет эвристически угадывать, что делать с неверными данными или неверными указателями на данные.
На этих принципах и сейчас построены базовые библиотеки .NET/JDK.
Это независимо от Not Nullabilty.
А в случае Not Nullabilty и данными, приезжающим из внешних источников, это уже ваша работа как архитектора — не везде по коду обращаться к внешним источникам и бойлерплейтить вызовы OrEmpty(), а инкапсулировать такие вызовы в отдельном слое, а в других слоях работать с уже нормализованными данными.
Или принять решение и вызывать OrEmpty (или обрабатывать как то еще) всегда перед передачей в методы — больше кода, чем сейчас (без Not Nullability) все равно не напишите.
lair
… вы так говорите, как будто где-то не будет.
Которого на момент выпуска .net 1 было намного меньше, чем сейчас, не правда ли?
А я хотел услышать от вас "продуманное решение", которое позволит полностью избавиться от изначально описанных вами методов-расширений.
lair
И еще. Вот убрали мы проблему с
null
, окей. Но исходный метод делал две вещи: он проверял наnull
и на пустоту — и именно проверка на пустоту заставила вас написать три оверлоада, потому что вы считаете, что эту проверку эффективнее делать тремя разными способами. Какое продуманное решение вы ждете от платформенной команды для этой проблемы? (ну, кроме того, чтобы убрать текущую абстракциюIQueryable
, что, будем честными, не представляется возможным)sand14 Автор
Not Nullability — это избавит нас в большинстве случае от необходимости проверять одновременно на null и Empty.
Для проверки на Empty мы сразу будем вызывать уже существующие Any() или Count(), а в зависимости от типа коллекции — IEnumerable или IQueryable, компилятор будет подставлять подходящий extension.
Именно так происходит и сейчас, если null нас не заботит, либо если мы его проверку вынесли в контракт с генерацией исключения.
Собственно, как и сейчас происходит — куча оверлоадов с одинаковым именем в сигнатуре (Enumerable, Queryable, Convert, etc), среди которых нужный автоматически выбирается компилятором по типу данных.
Другими словами, если разработчики продуктовой функциональности постоянно пишут одни и те же экстеншены, значит, разработчикам платформы нужно подумать либо о модели, с который эти экстеншены не понадобятся, либо включить эти экстеншены (или предложить более подходящие) в стандартный комплект.
lair
Тогда почему вы не ограничились двумя оверлоадами — одним для
IEnumerable
, а вторым — дляIQueryable
, в обоих из которых не вызвалиAny
?Вот мне интересно услышать про такую модель, да. На примере
Empty
для множеств и корректной обработки уникальности элемента (поиск по ключу) для них же (Single
не устраивает по понятным причинам: хотим понятные исключения вместоInvalidOperation
).Возвращаемся к вопросу "почему в .net/C# нет фичи x". Ответ выше, и он чаще всего не "не подумали".
nporaMep
на то как это сделано в Kotlin тоже кстати немало плохих фидбеков
sand14 Автор
Есть такое. Ведь Kotlin тоже в какой-то степени "пионер", да и при его разработке приходилось учитывать совместимость с Java.
Но все равно, Kotlin это прямо отдохновение: куча вещей, которая раньше бойлерплейтилась, теперь встроена в модель — хотя бы такие очевидные вещи вещи, как backing fields, видимые внутри свойства, и delegated properties.
Причем именно эти вещи привел в пример, т.к. о них думалось при работе с C# до того, как узнал о выходе Kotlin.
Szer
В январе 2016 в F# впилили Null Safety примерно как это сделали позже в Kotlin. В самом языке и его стандартной либе нормальный null-safety (ни присвоить null, ни даже проверить на null), а интероп с C# (или с Java-либами в случае с Kotlin) уже может кидать NRE.
Да и F# не был первый. Так что Котлину было у кого подсмотреть.
Interreto
Ваши чистые примеры грязны как никогда, потому что если делать хелпер для энума то с возможностью закешить в статике результаты
А ещё непонятно какой уличной магией вы добавили статический метод в абстрактный класс Enum через экстеншен О_оmayorovp
Если Enum сделать параметризованным — получится просто
SomeEnemType.GetValues()
...Interreto
Речь о абстракции Enum в C#, referencesource.microsoft.com/#mscorlib/system/enum.cs
lair
… и что? Почему нельзя сделать
GetValues
у типов, унаследованных отSystem.Enum
?mayorovp
Вот про нее я и говорю. Если сделать ее параметризованной — то все ее методы автоматически попадут в производные классы. То есть в SomeEnemType.
a-tk
Есть ещё вариант как было сделано с делегатами. Метод Invoke автогенерируемый, аргументы и возвращаемое значение зависят от сигнатуры в объявлении. Аналогичный функционал мог бы быть выполнен компилятором и обходился бы действительно даром. Но завернуть в generic стало бы невозможно такое, как, впрочем, попытки параметризовать что-нибудь типом-делегатом.
Bonart
Да, такие вещи называются антипаттернами.
Нет, лично я не видел ни разу.
sand14 Автор
Вам повезло в хорошем смысле слова.
Но неужели ни разу не видели string.IsNullOrEmpty(string)?
Ведь это точно тот же антипаттерн, но включен в стандартную библиотеку и применяется еще шире.
mayorovp
Потому что отсутствующие строки могут в программе возникать из многих источников, а отсутствующие коллекции — обычно ошибка.
dmitry_dvm
Почему вдруг string.IsNullOrEmpty(string) антипаттерн? Кроме этого вашего бэкенда есть еще ui, где IsNullOrEmpty и IsNullOrWhiteSpace необходимы, как воздух. Вся эта якобы непродуманность вами видится потому что вы думаете только о своей предметной области, а проектировщики языка — о всех возможных.
sand14 Автор
Проверка WhiteSpace — да, очень нужна, и в первую очередь для UI.
Но IsNullOrXxx? — если писать код по хорошему, откуда на UI null-строки?
Как из поля ввода вам может придти null?
К примеру, есть три поля ФИО — если какое то их них не заполнено, то из него должна придти пустая строка, а не null.
Как раз в бек-енде, пока не появится not nullability, проверка строк на null актуальнее.
Да и после появления not nullability останутся внешние источники — JSON'ы из сети, строки из БД с null колонками.
lair
К сожалению, разработчики половины модел-биндеров забыли с вами посоветоваться, и там придет
null
.dmitry_dvm
Пока юзер не ввел что-то в поле связанная строка остается нетронутой, т.е. null-ом.
Bonart
Видел и использовал.
Не тот же и не антипаттерн.
Проверка длины строки не меняет ее состояния и имеет сложность O(1), в отличие от IEnumerable и IQueryable
mayorovp
Поправка: у нормальных IEnumerable и IQueryable состояние от вызова Any тоже не меняется...
Исключение — штуки вроде той которую возвращает GetConsumingEnumerable для BlockingCollection
Bonart
Грамотные разработчики IEnumerable два раза не перебирают, так как не надеются на «нормальность», а знают, что IEnumerable второй проход не гарантирует.