Наверное, почти каждый .NET-разработчик сталкивался со случаями, когда для удобства кодирования рутинных действий и сокращения boilerplate-кода при работе со стандартными типами данных не хватает возможностей стандартной же библиотеки. И практически в каждом проекте появляются сборки и пространства имен вида Common, ProjectName.Common и т.д., содержащие дополнения для работы со стандартными типами данных: перечислениями Enums, Nullable-структурами, строками и коллекциями — перечислениями IEnumerable<T>, массивами, списками и собственно коллекциями. Как правило, эти дополнения реализуются с помощью механизма extension methods (методов расширения). Часто можно наблюдать наличие реализаций монад, также построенных на механизме методов расширения. (Забегая вперед — рассмотрим и вопросы, неожиданно возникающие, и которые можно не заметить, когда созданы свои расширения для IEnumerable<T>, а работа ведется с IQueryable<T>). Написание этой статьи инспирировано прочтением давней статьи-перевода Проверки на пустые перечисления и развернувшейся дискуссии к ней. Статья давняя, но тема по-прежнему актуальная, тем более, что код, похожий на пример из статьи, приходилось встречать в реальной работе от проекта к проекту. В исходной статье поднят вопрос, по своей сути касающийся в целом Common-библиотек, добавляемых в рабочие проекты. Проблема в том, что подобные расширения в продуктовых проектах добавляются наспех, т.к. разработчики занимаются созданиям новых фич, а на создание, продумывание и отладку базовой инфраструктуры времени и ресурсов не выделяется. Кроме того, как правило, разработчик, добавляя нужное ему Common-дополнение, создает его так, чтобы это дополнение заточено под кейсы из его фичи, и не задумывается, что раз это дополнение общего характера, то оно должно быть максимально абстрагировано от предметной логики и иметь универсальный характер — как это сделано в стандартных библиотеках платформ. В результате в многочисленных Common-подпапках проектов получаются залежи кода, приведенного в исходной статье: Автор указал на проблему с методом Count() и предложил создать такой метод расширения: Но и наличие такого метода не решает все проблемы: А теперь обратим внимание, что все стандартные коллекции .NET, кроме, собственно "бесконечной" последовательности IEnumerable<T> — массивы, списки и непосредственно коллекции — реализуют стандартный интерфейс IReadOnlyCollection<T>, предоставляющий свойство Count — и не нужно никаких итераторов с накладными расходами. Таким образом, целесообразно создать два метода расширения: В таком, случае, при вызове 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>-входным параметром. Таким образом, нужно создать третий метод: или В итоге, для реализации корректной для всех случаев (для всех ли?) простой null-безопасной проверки коллекций на "пустоту", нам пришлось провести небольшое исследование и реализовать три метода расширения. А если на начальном этапе создать только часть методов, например, только первые два (не нужны эти методы; нужно делать продуктовые фичи), то может получиться вот что: По этой же причине, все эти методы желательно реализовать в одной сборке и одном пространстве имен (можно в разных классах, например, EnumerableExtensions и QueryableExtensions), чтобы при случайном отключении пространства имен или сборки мы не возвратились к ситуации, когда с IQueryable<T>-коллекциями происходит работа с помощью обычных Enumerable-расширений. На мой взгляд, обилие подобных расширений практически в каждом проекте говорит о недостаточной проработанности стандартной библиотеки и в целом модели платформы. Часть проблем автоматически снялась бы при наличии поддержки Not Nullability в платформе, другая часть — наличием в стандартной библиотеке большего количества учитывающих более широкий спектр кейсов расширений для работы со стандартными типами данных. Причем, реализованные на современный лад — именно в виде расширений с использованием обобщений (Generics). Дополнительно поговорим об этой в следующей статье. P.S. Что интересно, если посмотреть на Kotlin и его стандартную библиотеку, при разработке которого явно был внимательно изучен опыт других языков, в первую очередь, на мой взгляд — Java, C# и Ruby, то можно легко обнаружить как раз эти вещи — Not Nullability и обилие extensions, при наличии которых не возникает необходимости добавлять свои "велосипедные" реализации микробиблиотек для работы со стандартными типами. Software Developer