Проверки на пустые перечисления

Автор оригинала: Phil Haack
  • Перевод
Недавно, во время разбора кода одной программы я заметил метод, который выглядел примерно так:

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



Метод принимает дженерик-перечисление и проверяет, пустое ли оно. Видите ли вы тут потенциальную проблему? Я намекну, проблема в этой строчке:

items.Count() == 0


И в чем же тут проблема? Проблема в том, что эта строчка может оказаться очень неэффективной.

Если вызвать наш метод, передав ему перечисление, которое не реализует ICollection<T> (например, IQueryable результат запроса к Entity Framework или LINQ to SQL), метод Count() будет перебирать всю коллекцию ради выполнения этой проверки.

В случае, когда перечисление реализует ICollection<T>, все в порядке. Метод Count() оптимизирован для таких случаев и проверит свойство Count.

Если говорить человеческим языком, то наша строчка отвечает на вопрос «Количество в перечислении равно нулю?». Но это вовсе не то, что нас интересует. На самом деле нам нужно ответить на вопрос «Есть ли в перечислении хотя бы один элемент?».

Если подойти к задаче таким образом, то решение станет очевидным: использовать расширение Any из пространства имен System.Linq.

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



Красота такого метода в том, что ему достаточно вызвать MoveNext интерфейса IEnumerable только один раз! У вас может быть бесконечно большое перечисление, но Any вернет результат немедленно.

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

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



Теперь, с таким методом, исходный код выглядит еще проще:

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



С таким методом-расширением в своем инструментарии, вы никогда снова не проверите пустоту перечисления неэффективно.
Поделиться публикацией

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

    0
    Спасибо, возьму на заметку
      –16
      Забавно то, что если items == null, то items.Count() == 0 — не возникнет NullReferenceException, что выглядет как-то слегка неестественно :-)
        +4
        Компилятор оптимизирует.
        Первое сравнение выдает true
        true || что-угодно = true
        поэтому items.Count() == 0 не вызывается.
          –20
          Я не писал, что не знаю почему, я говорил о том, как это выглядет в коде. Я знаю что такое extension methods.
            +7
            Причем здесь extension methods? Если в бинарной операции `A || B` значение первого операнда A истино, значение B заведомо не будет вычисляться, это специфика оператора `||`, что нельзя сказать об операторе `|`, в котором будут вычислены значения обоих операндов вне зависимости от значения первого, то есть следующее выражение породит исключение в случае когда items = null:
            if(items == null | !items.Any()) { /*… */ }
              +1
              Попробуйте перечитать комментарии, может до вас дойдет. (где вы там увидели то, что я не понимаю как работает оператор || — для меня загадка).Не моя вина, что для вас совершенно нормально, что у null можно вызвать метод и не получить NullReferenceException (из-за «сахарного» синтаксиса расширяющих методов)
              Попытка номер 4:
              1) Utility.IsNullOrEmpty(null as List<int>) — нормально
              2) (null as List<int>).IsNullOrEmpty() — выглядет ненормально
              Хотя в IL обе эти строчки будут выглядеть _абсолютно_ одинаково.
                0
                Простите за безграмотность, расстроился из-за налетевших школьников.
                  0
                  Да я понял, что вы хотели изложить, но получился обратный эффект, потому как даже если бы в примерах поста стоял бы одинарный «или», NullReferenceException в .Count() произошел бы все равно (вы написали, что исключения не возникнет, отсюда и минусы), но уже внутри метода расширения Count по проверке входящего аргумента, а не непосредственно перед попыткой вызова метода объекта null.
                    0
                    >>NullReferenceException в .Count() произошел бы все равно
                    Там возник бы ArgumentNullException, но возможно вы и правы — слишком коротко выразил мысль.
                      0
                      Да действительно ошибся, суббота вечер, все дела )
              –9
              Другими словами большинство программистов увидя такой код:
              List items = null;
              bool r = items.IsNullOrEmpty();

              скажут, что тут возникнет исключение (если не знать, про extension method)
                +3
                Вообще-то, было бы очень странно, если бы метод с названием IsNullOrEmpty() выкидывал исключение в такой ситуации.
                  –1
                  как раз для обычных методов это совсем не странно
                  если в данном случае метод IsnullOrEmpty() на самом деле не относится к экземпляру IEnumerable, а является статическим методом-расширением, у которого просто вызов удобен для пользователя, то у обычного класса такое не сработает
                  например, метод IsNullOrEmpty у класса string вызывается не у экземпляра класса string, а как статический string.isNullOrEmpty(someString)
                    –1
                    Не совсем понял Ваш комментарий. Т.е. это совсем не странно, что метод IsNull..() в случае null не возвращает true, а выкидывает исключение?
                    Вы хотите сказать, что написать такой метод для экземпляра невозможно? Естественно. Но при чем тут IsNullOrEmpty()?
                      +1
                      при том, что IsNullOrEmpty для экземпляра тоже нельзя написать
                      именно поэтому строчка
                      bool r = items.IsNullOrEmpty();
                      выглядит нелогично для тех, кто не знает extension-методы
                      и для них же фраза «было бы очень странно, если бы метод с названием IsNullOrEmpty() выкидывал исключение в такой ситуации» некорректна

                      вот все, что я хотел сказать
                        –1
                        Мое мнение заключается вот в чем: абсолютно логично, когда метод с названием IsNull...() возвращает true у null. И абсолютно нелогично, если метод с таким названием не может обработать null. Если он так делает, он не может так называться. И неважно, как этот метод реализован (очевидно, что как extension, но дело не в этом).

                        Вы рассматриваете программиста, который вообще не знает об extension-методах, но при этом догадывается, что у экземпляра не написать проверку на null и не поверит названию? Это очень странный .NET-разработчик, по-моему.
                          +3
                          ок, переформулирую: для .NET-программиста это выглядит нормально, но для программирования в целом это выглядит нелогично, потому что экземпляр сам себя на null проверить не может
                          и теоретически, даже для .NET-программиста данная строчка будет первые несколько раз сильно привлекать к себе внимание
                            –5
                            «Программирование в целом»? Лол. О чем Вы? Что это за «программирование»?
                            Думаю, дальше спорить бессмысленно.
                              –1
                              С точки зрения «программирования в целом» вызывается процедура объекта. В данном случае, объект не существует, а значит и процедура существовать не может.
                                +1
                                > программирования в целом
                                Что Вы имеете в виду? ООП?

                                > вызывается процедура объекта
                                Объекту посылается сообщение.

                                > В данном случае, объект не существует
                                В данном случае, объект — это null.
                              +1
                              Именно об этом я и писал, но большинство почему то подумало, что я не знаю как работает оператор ||… Видемо пишут быстрее чему думают или для них совершенно нормально, что у null можно вызвать какой-то метод (причем тут только IsNullOrEmpty ?? в любом extension method'е автор может реализовать проверку на null, в результате которой (null as Class1).SomeMethod(); не будет падать. к примеру с помощь. extension method можно написать вот так:
                              (null as string).Replace(); — и этот код нормально выполнится.
                              Жаль, что тут оказалось так мало адекватных людей.
                                +1
                                Ну согласитесь, что ваш первый комментарий как раз больше и похож на то, что вы как бы не знали про работу ||.
                                Всё это потому, что extension method был только вторичной темой топика, а вы его в своём сообщении забыли упомянуть, что и изменило смысл комментария.
                                Хотя последующий комментарий про items.IsNullOrEmpty() всё и поставил на свои места, обезьянкам, судя по всему, было всё равно что минусовать.
                                  +1
                                  Ух ты. я только сейчас заметил, что там items.Count(); в 1ом комментарии. Хотел же ведь IsNullOrEmpty написать…
                              0
                              Неправильно. Поведение и правда, не совсем стандартное.

                              // Нормальное поведение
                              string x = null;
                              x.Trim(); // Тут у нас NullReferenceException, т.к. Trim - экземплярный метод
                              
                              // Поведение описанного extension-метода
                              IEnumerable<object> y = null;
                              y.IsNullOrEmpty(); // никаких эксепшнов
                              
                              // Как правильно
                              string.IsNullOrEmpty(x); // И никаких эксепшнов, и выглядит логично
                              

                                0
                                Я с Вами полностью согласен. Я, пожалуй, не совсем правильно выразился.

                                Я говорю о том, что если уж есть метод «instance.IsNullOrEmpty()», то ждать от него NullReferenceException довольно странно. Представьте, что Вы вызываете number.IsZero(), а он возвращает ZeroArgumentException.

                                Писать такой метод неправильно (именно в данном контексте, хотя тут все делают вид, что это какой-то неписаный закон ООП) — но это другой вопрос.
                            0
                            Обычно, вызов любого не статического метода у null-reference выкидывает исключение.
                    +3
                    Советую капитанам-комментаторам и минусующим перечитать внимательно комментарий. || тут не при чем, вопрос в том, что [null].Count() не выкидывает NullReferenceException, а возвращает ноль. Тут я согласен, неочевидное поведение.
                      +3
                      Спасибо, Наконец-то хоть кто-то понял, что я хотел сказать :-)
                        0
                        Черт, я все перепутал. .Count() же выкинет исключение. Вы меня извините, в общем, но я беру свои слова назад. В этом вопросе я с Вами тоже не согласен :)
                          0
                          Сount — да, а я имел ввиду общий случай: (null as Class1).SomeMethod();
                            –2
                            Любой SomeMethod() кроме IsNull..() должен выкидывать исключение, да. Это очевидно.
                              0
                              Кому должен? если автор реализует проверку на null — он не упадет.
                      0
                      капец, short-circuit evaluation, первый класс вторая четверть
                      –3
                      // if(items.IsNullOrEmpty()) {
                      а вот есть items == null? не упадет ли NullPointerException? ;)
                        +5
                        нет
                          +4
                          по сути — вызов метода-расширение — это вызов статического метода с первым параметром, тем объектом над которым он вызывается.
                          я у себя использую такое:
                          public void NullableToString(this object obj) {
                          return obj == null? string.Empty: obj.ToString();
                          }
                          0
                          Если вызвать у IQueryable метод Count(), то в случае с EF и Linq2Sql выполнится запрос «select count(*) ...». Никаких пробеганий по всем элементам там нет.

                          А вот для коллекций, не поддерживающих свойство Count Ваш подход, конечно, даёт существенный прирост производительности.
                            +2
                            Способ не совсем мой, это — Phil Haack (перевод), но хотелось бы отметить, что при применении Any делается sql-запрос exists вместо count(*), что на больших БД и при сложных запросах тоже может оказаться очень полезно (СУБД внутри тоже проходит по элементам, чтобы их посчитать, и не всегда на это есть индексы).

                            С IQueryable вообще всегда приходится быть внимательным к мелочам.
                              –1
                              MySQL с движком MyISAM удалённые записи не удаляет физически, а помечает, как удалённые. Подсчёт count(*) может быть очень долгим :)
                                –1
                                Вы пишете запросы без индексов?
                                • НЛО прилетело и опубликовало эту надпись здесь
                              –3
                              Просто совет — уберите пожалуйста информацию о подсветке кода. Если есть желание — оставьте в одном блоке.
                                0
                                Подобный метод не использую, но для больших массивов всегда вызываю Any, а не Count, один раз уже убедился, что это очень важно.
                                  0
                                  Расскажите пожалуйста, откуда у Вас берутся переменные IEnumerable равные null? За всю свою практику ни разу с таки не сталкивался.
                                    +7
                                    Класс! Вы хороший программист!

                                    Эти переменные ничем не отличаются ото всех остальных, которые могут быть null.
                                      –2
                                      Хороший, не спорю :)
                                    +2
                                    Я пожалуй промолчу…
                                      0
                                      Да, любая переменная может быть null, но приведите пример когда вместо пустой коллекции Вы будете возвращать/передавать значение null. Я не вижу в этом никакого смысла.
                                        0
                                        Тут вопрос в перестраховке. Есть люди которые все входящие параметры проверяют. А есть те кторые нет. При правильном подходе оба варианта работают.
                                          0
                                          Давайте еще параметры типа int на null проверять, на всякий случай. :) В действительности, ни один из методов фреймворка, возвращающий коллекцию, не вазвращает null.
                                            +2
                                            int это валью тип, он не может быть нулл. И не может проверятся на нулл.
                                              0
                                              Я знаю :) Это была шутка, там смайлик стоял…

                                              Проверяться может, компилятор позволяет, попробуйте.
                                              0
                                              Первое что пришло в голову это ASP.NET MVC фреймворк, там если Акшен принимает IEnumerable, Array или ченить подобное. То будет null. Ну и сам фреймворк всегда проверяет на null.

                                              [ComVisible(false)]
                                              public static string Concat(IEnumerable values)
                                              {
                                              if (values == null)
                                              {
                                              throw new ArgumentNullException(«values»);
                                              }
                                                0
                                                Это не противоречит тому что «ни один из методов фреймворка, возвращающий коллекцию, не вазвращает null.»

                                                А вообще всегда можно найти исключения из правил. :)
                                            0
                                            Любой ValueType не может быть null.

                                            Что касается вашего вопроса — вы видимо никогда не писали чего-то рассчитанного на публичное использование, когда вы не можете делать предположения о корректности входных параметров.
                                              0
                                              Любой ValueType не может быть null.


                                              Я знаю :) Это была шутка, там смайлик стоял…

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


                                              И такое писали, только это частный случай.
                                                0
                                                глотать ошибки — не самая верная стратегия
                                                  0
                                                  Ну а кто, собственно, говорит, что это ошибки? null передать в некоторых случаях намного проще, чем какой-либо объект. Особенно когда этот объект не нужен.

                                                  Foo(new Foo(), new Bar());
                                                  Foo(null, null);
                                                    0
                                                    ещё проще — не передавать ничего
                                                    FooBar(new Foo(), new Bar());
                                                    Foo(new Foo());
                                                    а полиморфные методы — зло
                                              0
                                              Айенде расписал почему такой метод проверки не подходит для любых коллекций.
                                              ayende.com/Blog/archive/2010/06/10/checking-for-empty-enumerations.aspx
                                              Проблема решения предложенного Филом Хааком в том, что предпологается что всякая коллекция может быть итерирована дважды.
                                                +2
                                                Там надо коментарии читать. В них самое интересное.
                                                  0
                                                  Точно.

                                                  Я его пост читал, когда там было 0 комментов.
                                                  Что интересно пост Айенде появился минут через 30 после поста Хаака.

                                                  Спасибо.
                                                  0
                                                  chaliy верно заметил… там в комментариях верную штуку пишут, про то что Any() в любом случае создает новый Enumerator. И проблемы с потерей первого элемента коллекции — нет.

                                                  Видимо Айенде написал предположение, не потестировав сначала код. :)
                                                  0
                                                  В случае IQueryable и LINQ2SQL/EF перечисления не будет, а в СУБД уйдет запрос вида «SELECT COUNT(*) ..» и перечисления не будет. Могу предположить, что ANY здесь может работать менее эффективно.
                                                  • НЛО прилетело и опубликовало эту надпись здесь

                                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                    Самое читаемое