Как стать автором
Обновить

Комментарии 58

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

Верно.


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


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


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


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


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


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


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


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


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

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

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

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

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


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

Как из одного вытекает другое? Наличие 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>.

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

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


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

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


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

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


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

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

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


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

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

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

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


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

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


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

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

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

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

Я исхожу из того, что они подумали. Они могли ошибиться (все ошибаются), но они подумали. Иными словами, поддержки ограничения по 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.
Они сами часть вещей меняли с временем, причем даже ломая backward compatilility.

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

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


  • ковариантность 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. Ковариантность 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, но это тоже мимо кассы.
Ковариантность generic-интерфейсов и делегатов была введена путём расширения синтаксиса, а не путём изменения, ломающего код.

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

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

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

Полностью согласен.
Собственно, любое breaking change должно давать много плюсов, имея минимальную вероятность всё сломать.
  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#.

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

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

Хз, про GetValues я только знаю что он ОЧЕНЬ медленный, обернув его в дженерики, не меняя ИЛ под ним, это ситуации не спасает. Оба метода были бы ужасны. Но согласен довольно часто такой экстеншн пишут.
и да, как сейчас хорошо замечают нынешние дизайнеры C# — в 2000-2005-м C# писали С++ программисты, которые особо не пользовались сами шарпом. Плюс микрософт был полностью close source и никакого фидбека микрософт не спрашивал по ходу разработки. В принципе такой был весь ентерпрайз в те времена.
Вот это похоже на правду.
Т.е., на добавление весьма спорного метода Enum.HasFlag(Enum) (надеюсь, знаете, почему спорного) у производителя ресурсов хватило, а на добавление Enum.GetValue[TEnum] — нет

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

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

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

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

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

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


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


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


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

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

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


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

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


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

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

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

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


Помогает это очень просто и эффективно:


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

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


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

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


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

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

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

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


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

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


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

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


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

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

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


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

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


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

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

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

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

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

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


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

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

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

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


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

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


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

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

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

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


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

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

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


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

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

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

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

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


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

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

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

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

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

Но неужели ни разу не видели string.IsNullOrEmpty(string)?
Ведь это точно тот же антипаттерн, но включен в стандартную библиотеку и применяется еще шире.
Потому что отсутствующие строки могут в программе возникать из многих источников, а отсутствующие коллекции — обычно ошибка.
Почему вдруг string.IsNullOrEmpty(string) антипаттерн? Кроме этого вашего бэкенда есть еще ui, где IsNullOrEmpty и IsNullOrWhiteSpace необходимы, как воздух. Вся эта якобы непродуманность вами видится потому что вы думаете только о своей предметной области, а проектировщики языка — о всех возможных.
Проверка WhiteSpace — да, очень нужна, и в первую очередь для UI.
Но IsNullOrXxx? — если писать код по хорошему, откуда на UI null-строки?
Как из поля ввода вам может придти null?
К примеру, есть три поля ФИО — если какое то их них не заполнено, то из него должна придти пустая строка, а не null.

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

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

Пока юзер не ввел что-то в поле связанная строка остается нетронутой, т.е. null-ом.
Но неужели ни разу не видели string.IsNullOrEmpty(string)?

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


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

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

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


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

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

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

А вот тут снова появляется та самая тема "непродуманности".


Допустим, мы написали, даже не extension'ы, а просто некие методы, принимающие на вход IReadOnlyCollection(T), IReadOnlyList(T) и IReadOnlyDictionary(T).
Поскольку менять эти коллекции мы не собирается, в контракте запрашиваем readonly-версии интерфейсов.


public static void DoSomethingWithCollection(IReadOnlyCollection<T> items)
{
}

public static void DoSomethingWithList(IReadOnlyList<T> items)
{
}

public static void DoSomethingWithDictionary(IReadOnlyDictionary<T> items)
{
}

При вызове этих методов мы сможем передать в них как экземпляры непосредственно интерфейсов IReadOnlyCollection(T), IReadOnlyList(T) и IReadOnlyDictionary(T), так и экземпляры изменяемых реализаций List(T) (в первые два метода) и Dictionary(T), поскольку эти типы реализуют нужные readonly-интерфейсы.


А вот дальше интереснее — пусть у нас есть экземпляры ICollection(T), IList(T) и IDictionary(T).
И передать их в наши методы мы не сможем.
Вопрос, почему тип List(T) реализует интерфейсы IReadOnlyCollection(T) и IReadOnlyList(T), тип Dictionary(T) реализует интерфейс IReadOnlyDictionary(T),
а вот соответствующие этим типам интерфейсы ICollection(T), IList(T) и IDictionary(T) не реализуют свои readonly-варианты?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории