Лучше вернуть нулевую или пустую коллекцию?


420

Это общий вопрос (но я использую C #), каков наилучший способ (лучшая практика), возвращаете ли вы нулевую или пустую коллекцию для метода, у которого коллекция является типом возвращаемого значения?


5
Хм, не совсем CPerkins. rads.stackoverflow.com/amzn/click/0321545613

3
@Perkins - Да, есть. Это четко указано в собственных рекомендациях Microsoft по разработке структуры .NET. Смотрите ответ RichardOD для деталей.
Грег Бук

51
ТОЛЬКО если значение «Я не могу вычислить результаты», вы должны возвращать ноль. Нуль никогда не должен иметь семантику «пустой», только «отсутствующий» или «неизвестный». Больше подробностей в моей статье на эту тему: blogs.msdn.com/ericlippert/archive/2009/05/14/…
Эрик Липперт,

3
Божо: "любой" - это очень много языков. Как насчет Common Lisp, где пустой список точно равен нулевому значению? :-)
Кен,

9
На самом деле, «дубликат» - это методы, которые возвращают объект, а не коллекцию. Это другой сценарий с разными ответами.
GalacticCowboy

Ответы:


499

Пустая коллекция. Всегда.

Это отстой:

if(myInstance.CollectionProperty != null)
{
  foreach(var item in myInstance.CollectionProperty)
    /* arrgh */
}

Рекомендуется НИКОГДА не возвращаться nullпри возврате коллекции или перечисления. ВСЕГДА возвращайте пустой перечисляемый / коллекцию. Это предотвращает вышеупомянутую ерунду и предотвращает появление вашего автомобиля среди коллег и пользователей ваших классов.

Говоря о свойствах, всегда устанавливайте свою собственность один раз и забудьте о ней

public List<Foo> Foos {public get; private set;}

public Bar() { Foos = new List<Foo>(); }

В .NET 4.6.1 вы можете сжать это довольно много:

public List<Foo> Foos { get; } = new List<Foo>();

Говоря о методах, которые возвращают перечислимые значения, вы можете легко вернуть пустое перечисляемое вместо null...

public IEnumerable<Foo> GetMyFoos()
{
  return InnerGetFoos() ?? Enumerable.Empty<Foo>();
}

Использование Enumerable.Empty<T>()может рассматриваться как более эффективное, чем возвращение, например, новой пустой коллекции или массива.


24
Я согласен с Уиллом, но я думаю, что «всегда» немного чрезмерно. В то время как пустая коллекция может означать «0 элементов», возвращение Null может означать «нет коллекции вообще», например. если вы разбираете HTML, ища <ul> с id = "foo", <ul id = "foo"> </ ul> может вернуть пустую коллекцию; если нет <ul> с id = "foo", лучше будет нулевой возврат (если вы не хотите обрабатывать этот случай с исключением)
Патонза

30
вопрос не в том, можно ли «легко вернуть пустой массив», а в том, может ли пустой массив вводить в заблуждение в текущем контексте. Пустой массив на самом деле что-то значит, как и ноль. Сказать, что вы всегда должны возвращать пустой массив, а не ноль, почти так же ошибочно, как сказать, что логический метод всегда должен возвращать true. Оба возможных значения передают смысл.
Дэвид Хедлунд

31
На самом деле вы должны предпочесть возврат System.Linq.Enumerable.Empty <Foo> () вместо нового Foo [0]. Это более явно и экономит вам одно выделение памяти (по крайней мере, в моей установленной реализации .NET).
Trillian

4
@Will: OP: «вы возвращаете пустую или пустую коллекцию для метода , у которого коллекция является типом возврата». Я думаю , что в данном случае , является ли он IEnumerableили ICollectionне имеет значения , что много. В любом случае, если вы выберете что-то типа, ICollectionони также вернутся null... Я хотел бы, чтобы они вернули пустую коллекцию, но я столкнулся с ними, возвращая null, так что я решил упомянуть об этом здесь. Я бы сказал, что по умолчанию для коллекции перечислимых пусто, а не пусто. Я не знал, что это был такой чувствительный предмет.
Маттис Вессельс


154

Из Руководства по разработке структуры, 2-е издание (стр. 256):

НЕ возвращайте нулевые значения из свойств коллекции или из методов, возвращающих коллекции. Вместо этого верните пустую коллекцию или пустой массив.

Вот еще одна интересная статья о преимуществах отсутствия возврата нулей (я пытался найти что-то в блоге Брэда Абрама, и он связался со статьей).

Отредактируйте - поскольку Эрик Липперт теперь прокомментировал оригинальный вопрос, я также хотел бы дать ссылку на его превосходную статью .


33
+1. Всегда лучше следовать рекомендациям по разработке структуры, если у вас нет ОЧЕНЬ веских причин не делать этого.

@ Конечно. Вот за чем я следую. Я никогда не видел необходимости делать иначе
RichardOD

да, это то, что вы следуете. Но в тех случаях, когда API-интерфейсы, на которые вы полагаетесь, не следуют ему, вы «в ловушке»;)
Божо

@ Божо-ага. Ваш ответ дает несколько хороших ответов на эти крайние случаи.
RichardOD

90

Зависит от вашего контракта и вашего конкретного случая . Обычно лучше возвращать пустые коллекции , но иногда ( редко ):

  • null может означать что-то более конкретное;
  • Ваш API (контракт) может заставить вас вернуться null.

Некоторые конкретные примеры:

  • компонент пользовательского интерфейса (из библиотеки вне вашего контроля) может отображать пустую таблицу, если передана пустая коллекция, или вообще нет таблицы, если передан null.
  • в объекте в XML (JSON / что угодно), где nullбудет означать, что элемент отсутствует, в то время как пустая коллекция приведет к избыточности (и, возможно, неправильно)<collection />
  • вы используете или реализуете API, который явно заявляет, что null должен быть возвращен / передан

4
@ Божо - вот несколько интересных примеров.
RichardOD

1
Я бы хотел увидеть вашу точку зрения, хотя я не думаю, что вы должны строить свой код вокруг другой ошибки, и по определению 'null' в c # никогда не означает что-то конкретное. Значение null определяется как «нет информации» и, следовательно, сказать, что оно несет конкретную информацию, является оксимороном. Вот почему в руководствах .NET указано, что вы должны возвращать пустой набор, если набор действительно пустой. возвращение null говорит: «Я не знаю, куда ушел ожидаемый сет»
Rune FS

13
нет, это означает «нет набора», а не «набор не имеет элементов»
Божо

Раньше я в это верил, потом мне пришлось написать много TSQL и я понял, что это не всегда так, хе-хе.

1
Часть с Object-To-Xml находится на месте
Mihai Caracostea

36

Есть еще один момент, который еще не был упомянут. Рассмотрим следующий код:

    public static IEnumerable<string> GetFavoriteEmoSongs()
    {
        yield break;
    }

Язык C # вернет пустой перечислитель при вызове этого метода. Поэтому, чтобы соответствовать структуре языка (и, следовательно, ожиданиям программиста), должна быть возвращена пустая коллекция.


1
Технически я не думаю, что это возвращает пустую коллекцию.
FryGuy

3
@FryGuy - Нет, это не так. Он возвращает объект Enumerable, метод которого GetEnumerator () возвращает Enumerator, который (по аналогии с коллекцией) пуст. То есть метод MoveNext () перечислителя всегда возвращает false. Вызов метода-примера не возвращает null и не возвращает объект Enumerable, метод GetEnumerator () которого возвращает null.
Джеффри Л Уитледж

31

Пусто намного удобнее для потребителей.

Существует понятный способ составления пустого перечислимого:

Enumerable.Empty<Element>()

2
Спасибо за изящный трюк. Я создавал пустой List <T> () как идиот, но это выглядит намного чище и, вероятно, немного более эффективно.
Jacobs Data Solutions

18

Мне кажется, что вы должны вернуть значение, семантически правильное в контексте, что бы это ни было. Правило, которое гласит «всегда возвращать пустую коллекцию», кажется мне немного упрощенным.

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

Меня часто разочаровывают системы, которые не могут различить ноль и отсутствие ответа. У меня было несколько раз, когда система просила меня ввести какое-то число, я вводил ноль и получал сообщение об ошибке, сообщающее, что я должен ввести значение в это поле. Я только что сделал: я вошел в ноль! Но он не примет ноль, потому что не может отличить его от ответа.


Ответ Сондерсу:

Да, я предполагаю, что есть разница между «Человек не ответил на вопрос» и «Ответ был нулевым». Это было пунктом последнего параграфа моего ответа. Многие программы не могут отличить «не знаю» от пустого или нулевого, что кажется мне потенциально серьезным недостатком. Например, я покупал дом год назад или около того. Я зашел на сайт по недвижимости, и там было много домов по цене $ 0. Звучит довольно хорошо для меня: они раздают эти дома бесплатно! Но я уверен, что печальная реальность состояла в том, что они просто не вошли в цену. В этом случае вы можете сказать: «Ну, очевидно, ноль означает, что они не указали цену - никто не собирается сдавать дом бесплатно». Но на сайте также указаны средние цены продажи и продажи домов в разных городах. Я не могу не задаться вопросом, не содержит ли среднее значение нули, что дает неверно низкое среднее значение для некоторых мест. то есть, что в среднем составляет 100 000 долларов США; $ 120 000; и "не знаю"? Технически ответ «не знаю». То, что мы, вероятно, действительно хотим увидеть, - это 110 000 долларов. Но то, что мы, вероятно, получим, составляет 73 333 доллара, что было бы совершенно неправильно. Кроме того, что, если у нас возникла эта проблема на сайте, где пользователи могут сделать заказ онлайн? (В отличие от недвижимости, но я уверен, что вы видели, что это было сделано для многих других продуктов.) Хотели бы мы, чтобы «цена, не указанная еще», была интерпретирована как «бесплатная»? таким образом, давая неправильно низкое среднее значение для некоторых мест. то есть, что в среднем составляет 100 000 долларов США; $ 120 000; и "не знаю"? Технически ответ «не знаю». То, что мы, вероятно, действительно хотим увидеть, - это 110 000 долларов. Но то, что мы, вероятно, получим, составляет 73 333 доллара, что было бы совершенно неправильно. Кроме того, что, если у нас возникла эта проблема на сайте, где пользователи могут сделать заказ онлайн? (В отличие от недвижимости, но я уверен, что вы видели, что это было сделано для многих других продуктов.) Хотели бы мы, чтобы «цена, не указанная еще», была интерпретирована как «бесплатная»? таким образом, давая неправильно низкое среднее значение для некоторых мест. то есть, что в среднем составляет 100 000 долларов США; $ 120 000; и "не знаю"? Технически ответ «не знаю». То, что мы, вероятно, действительно хотим увидеть, - это 110 000 долларов. Но то, что мы, вероятно, получим, составляет 73 333 доллара, что было бы совершенно неправильно. Кроме того, что, если у нас возникла эта проблема на сайте, где пользователи могут сделать заказ онлайн? (В отличие от недвижимости, но я уверен, что вы видели, что это было сделано для многих других продуктов.) Хотели бы мы, чтобы «цена, не указанная еще», была интерпретирована как «бесплатная»? что было бы совершенно неправильно. Кроме того, что, если у нас возникла эта проблема на сайте, где пользователи могут сделать заказ онлайн? (В отличие от недвижимости, но я уверен, что вы видели, что это было сделано для многих других продуктов.) Хотели бы мы, чтобы «цена, не указанная еще», была интерпретирована как «бесплатная»? что было бы совершенно неправильно. Кроме того, что, если у нас возникла эта проблема на сайте, где пользователи могут сделать заказ онлайн? (В отличие от недвижимости, но я уверен, что вы видели, что это было сделано для многих других продуктов.) Хотели бы мы, чтобы «цена, не указанная еще», была интерпретирована как «бесплатная»?

RE, имеющий две отдельные функции, "есть ли?" и "если так, что это?" Да, вы, конечно, можете это сделать, но зачем вам это? Теперь вызывающая программа должна сделать два вызова вместо одного. Что произойдет, если программист не сможет назвать «любой»? и идет прямо к "что это?" ? Будет ли программа возвращать ошибочный ноль? Бросить исключение? Вернуть неопределенное значение? Это создает больше кода, больше работы и больше потенциальных ошибок.

Единственное преимущество, которое я вижу, заключается в том, что оно позволяет вам соблюдать произвольное правило. Есть ли какое-то преимущество в этом правиле, которое стоит того, чтобы соблюдать его? Если нет, зачем?


Ответ на Jammycakes:

Посмотрите, как будет выглядеть реальный код. Я знаю, что вопрос сказал C #, но извините, если я пишу Java. Мой C # не очень острый и принцип тот же.

С нулевым возвратом:

HospList list=patient.getHospitalizationList(patientId);
if (list==null)
{
   // ... handle missing list ...
}
else
{
  for (HospEntry entry : list)
   //  ... do whatever ...
}

С отдельной функцией:

if (patient.hasHospitalizationList(patientId))
{
   // ... handle missing list ...
}
else
{
  HospList=patient.getHospitalizationList(patientId))
  for (HospEntry entry : list)
   // ... do whatever ...
}

На самом деле это код на строку или два меньше с нулевым возвратом, так что это не больше нагрузки для вызывающего, а меньше.

Я не вижу, как это создает сухую проблему. Мы не должны выполнять вызов дважды. Если бы мы всегда хотели делать то же самое, когда список не существует, возможно, мы могли бы отодвинуть обработку до функции get-list вместо того, чтобы вызывающая сторона делала это, и поэтому помещение кода в вызывающую функцию было бы СУХИМЫМ нарушением. Но мы почти наверняка не хотим всегда делать одно и то же. В функциях, где у нас должен быть список для обработки, пропущенный список является ошибкой, которая вполне может остановить обработку. Но на экране редактирования мы, конечно, не хотим останавливать обработку, если они еще не вводили данные: мы хотим, чтобы они вводили данные. Таким образом, обработка «без списка» должна выполняться на уровне вызывающей стороны, так или иначе. И то, делаем ли мы это с нулевым возвратом или отдельной функцией, не имеет значения для большего принципа.

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

Я не вижу, как помогает добавление дополнительного атрибута в коллекцию. Звонящий еще должен проверить это. Как это лучше, чем проверка на ноль? Опять же, самое худшее, что может случиться, это то, что программист забудет проверить это и дать неверные результаты.

Функция, которая возвращает ноль, не является сюрпризом, если программист знаком с понятием ноль, означающим «не иметь значения», о котором, я думаю, должен был услышать любой компетентный программист, считает ли он, что это хорошая идея или нет. Я думаю, что наличие отдельной функции - это скорее «неожиданная» проблема. Если программист не знаком с API, когда он запускает тест без данных, он быстро обнаружит, что иногда он возвращает ноль. Но как бы он обнаружил существование другой функции, если ему не пришло в голову, что такая функция может существовать, и он проверит документацию, и документация будет полной и понятной? Я бы предпочел иметь одну функцию, которая всегда дает мне значимый ответ, а не две функции, которые я должен знать и не забывать вызывать обе.


4
Почему необходимо объединять ответы «нет ответа» и «ноль» в одном возвращаемом значении? Вместо этого пусть метод возвращает «все предыдущие госпитализации за последние пять лет» и содержит отдельный метод, который спрашивает: «Был ли заполнен список предыдущих госпитализаций?». Это предполагает, что есть разница между списком, заполненным без предыдущих госпитализаций, и списком, не заполненным.
Джон Сондерс

2
Но если вы вернете ноль, вы все равно возлагаете на вызывающего абонента дополнительное бремя! Вызывающий должен проверить каждое возвращаемое значение на ноль - ужасное СУХОЕ нарушение. Если вы хотите , чтобы указать «абонент не отвечает на вопрос» отдельно, то это проще создать дополнительный вызов метода , чтобы указать этот факт. Либо так, либо используйте производный тип коллекции с дополнительным свойством DeclinedToAnswer. Правило никогда не возвращать ноль вовсе не произвольно, это принцип наименьшего сюрприза. Кроме того, тип возврата вашего метода должен означать, что его имя говорит, что делает. Нуль почти наверняка нет.
jammycakes

2
Вы предполагаете, что ваш getHospitalizationList вызывается только из одного места и / или что все его вызывающие абоненты захотят различить случаи "нет ответа" и "ноль". Там будет иметь место случаи (почти наверняка большинство) , где ваш абонент не должен делать это различие, и таким образом вы заставляете их , чтобы добавить проверку нулевых в тех местах , где это не должно быть необходимым. Это добавляет значительный риск для вашей кодовой базы, потому что люди слишком легко могут забыть сделать это - и поскольку существует очень мало законных причин для возврата null вместо коллекции, это будет гораздо более вероятным.
jammycakes

3
RE the name: никакое имя функции не может полностью описать, что делает функция, если только оно не имеет такой длины, как функция, и, следовательно, крайне непрактично. Но в любом случае, если функция возвращает пустой список, когда не было дано никакого ответа, то по той же причине не следует ли ее называть "getHospitalizationListOrEmptyListIfNoAnswer"? Но действительно, вы бы настаивали на том, чтобы функция Java Reader.read была переименована в readCharacterOrReturnMinusOneOnEndOfStream? Что ResultSet.getInt действительно должен быть "getIntOrZeroIfValueWasNull"? И т.д.
Джей

4
RE каждый звонок хочет различать: ну да, я предполагаю, что, по крайней мере, автор звонящего должен принять осознанное решение, которое ему безразлично. Если функция возвращает пустой список для «не знаю», и вызывающие абоненты слепо обрабатывают это «нет», это может дать серьезные неточные результаты. Представьте, была ли функция "getAllergicReactionToMedicationList". Программа, которая слепо трактовала «список не был введен», поскольку «у пациента нет известных аллергических реакций», может буквально привести к убийству пациента. Вы получите аналогичные, хотя и менее впечатляющие результаты, во многих других системах. ...
Джей

10

Если пустая коллекция имеет смысл семантически, это то, что я предпочитаю возвращать. Возврат пустой коллекции для GetMessagesInMyInbox()сообщений «у вас действительно нет никаких сообщений во входящих», тогда как возврат nullможет быть полезен для сообщения о том, что недостаточно данных, чтобы сказать, как должен выглядеть возвращаемый список.


6
В приведенном вами примере звучит так, что метод, вероятно, должен выдать исключение, если он не может выполнить запрос, а не просто вернуть ноль. Исключения гораздо более полезны для диагностики проблем, чем нули.
Грег Бук

ну да, в примере с входящим почтовым ящиком nullзначение определенно не выглядит разумным, я думал об этом в более общих чертах. Исключения также хороши для сообщения о том, что что-то пошло не так, но если «недостаточные данные», о которых идет речь, вполне ожидаемы, то исключение может привести к плохому дизайну. Я скорее думаю о сценарии, где это вполне возможно, и нет никакой ошибки для метода, чтобы иногда не иметь возможности рассчитать ответ.
Дэвид Хедлунд

6

Возвращение null может быть более эффективным, так как новый объект не создается. Однако это также часто требует nullпроверки (или обработки исключений).

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

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


8
Эффективность почти никогда не должна быть фактором при рассмотрении правильности разработки API. В некоторых очень специфических случаях, таких как графические примитивы, тогда это может быть так, но при работе со списками и большинством других высокоуровневых вещей я очень сильно сомневаюсь в этом.
Грег Бук

Согласитесь с Грегом, особенно учитывая, что код, который пользователь API должен написать, чтобы компенсировать эту «оптимизацию», может быть более неэффективным, чем если бы сначала использовался лучший дизайн.
Крейг Штунц

Согласились, и в большинстве случаев это просто не стоит оптимизации. Пустые списки практически бесплатны с современным управлением памятью.
Джейсон Бейкер

6

Можно утверждать, что аргументация в пользу Null Object Pattern аналогична аргументации в пользу возврата пустой коллекции.


4

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

На самом деле, в этом случае я обычно предпочитаю генерировать исключение, чтобы убедиться, что оно ДЕЙСТВИТЕЛЬНО не игнорируется :)

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


4

Я бы сказал, что nullэто не то же самое, что пустая коллекция, и вы должны выбрать, какая из них лучше всего отражает то, что вы возвращаете. В большинстве случаевnull ничего (кроме SQL). Пустая коллекция - это что-то, хотя и пустое.

Если вам нужно выбрать один или другой, я бы сказал, что вы должны стремиться к пустой коллекции, а не к нулю. Но бывают случаи, когда пустая коллекция не совпадает с нулевым значением.


4

Всегда думайте в пользу ваших клиентов (которые используют ваш API):

Возвращение 'null' очень часто создает проблемы с клиентами, которые неправильно обрабатывают нулевые проверки, что вызывает исключение NullPointerException во время выполнения. Я видел случаи, когда такая пропущенная нулевая проверка приводила к возникновению приоритетной производственной проблемы (клиент использовал foreach (...) для нулевого значения). Во время тестирования проблемы не возникало, потому что данные оперировали немного иначе.


3

Я хотел бы дать объяснение здесь, с подходящим примером.

Рассмотрим случай здесь ..

int totalValue = MySession.ListCustomerAccounts()
                          .FindAll(ac => ac.AccountHead.AccountHeadID 
                                         == accountHead.AccountHeadID)
                          .Sum(account => account.AccountValue);

Здесь рассмотрим функции, которые я использую ..

1. ListCustomerAccounts() // User Defined
2. FindAll()              // Pre-defined Library Function

Я могу легко использовать ListCustomerAccountи FindAllвместо.,

int totalValue = 0; 
List<CustomerAccounts> custAccounts = ListCustomerAccounts();
if(custAccounts !=null ){
  List<CustomerAccounts> custAccountsFiltered = 
        custAccounts.FindAll(ac => ac.AccountHead.AccountHeadID 
                                   == accountHead.AccountHeadID );
   if(custAccountsFiltered != null)
      totalValue = custAccountsFiltered.Sum(account => 
                                            account.AccountValue).ToString();
}

ПРИМЕЧАНИЕ. Так как AccountValue - нет null, функция Sum () не вернется null. Поэтому я могу использовать ее напрямую.


2

У нас было это обсуждение среди команды разработчиков на работе около недели назад, и мы почти единогласно пошли на пустую коллекцию. Один человек хотел вернуть null по той же причине, что и Майк, указанный выше.


2

Пустая коллекция. Если вы используете C #, предполагается, что увеличение системных ресурсов не является обязательным. В то время как менее эффективный, возврат Пустой Коллекции намного более удобен для вовлеченных программистов (по причине, описанной выше).


2

Возвращать пустую коллекцию лучше в большинстве случаев.

Причиной этого является удобство реализации вызывающей стороны, согласованный контракт и более простая реализация.

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

Допустимое использование null для IEnumerable может указывать на отсутствие результата или сбой операции, но в этом случае следует рассмотреть другие методы, такие как создание исключения.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

namespace StackOverflow.EmptyCollectionUsageTests.Tests
{
    /// <summary>
    /// Demonstrates different approaches for empty collection results.
    /// </summary>
    class Container
    {
        /// <summary>
        /// Elements list.
        /// Not initialized to an empty collection here for the purpose of demonstration of usage along with <see cref="Populate"/> method.
        /// </summary>
        private List<Element> elements;

        /// <summary>
        /// Gets elements if any
        /// </summary>
        /// <returns>Returns elements or empty collection.</returns>
        public IEnumerable<Element> GetElements()
        {
            return elements ?? Enumerable.Empty<Element>();
        }

        /// <summary>
        /// Initializes the container with some results, if any.
        /// </summary>
        public void Populate()
        {
            elements = new List<Element>();
        }

        /// <summary>
        /// Gets elements. Throws <see cref="InvalidOperationException"/> if not populated.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>.</returns>
        public IEnumerable<Element> GetElementsStrict()
        {
            if (elements == null)
            {
                throw new InvalidOperationException("You must call Populate before calling this method.");
            }

            return elements;
        }

        /// <summary>
        /// Gets elements, empty collection or nothing.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with zero or more elements, or null in some cases.</returns>
        public IEnumerable<Element> GetElementsInconvenientCareless()
        {
            return elements;
        }

        /// <summary>
        /// Gets elements or nothing.
        /// </summary>
        /// <returns>Returns <see cref="IEnumerable{T}"/> of <see cref="Element"/>, with elements, or null in case of empty collection.</returns>
        /// <remarks>We are lucky that elements is a List, otherwise enumeration would be needed.</remarks>
        public IEnumerable<Element> GetElementsInconvenientCarefull()
        {
            if (elements == null || elements.Count == 0)
            {
                return null;
            }
            return elements;
        }
    }

    class Element
    {
    }

    /// <summary>
    /// http://stackoverflow.com/questions/1969993/is-it-better-to-return-null-or-empty-collection/
    /// </summary>
    class EmptyCollectionTests
    {
        private Container container;

        [SetUp]
        public void SetUp()
        {
            container = new Container();
        }

        /// <summary>
        /// Forgiving contract - caller does not have to implement null check in addition to enumeration.
        /// </summary>
        [Test]
        public void UseGetElements()
        {
            Assert.AreEqual(0, container.GetElements().Count());
        }

        /// <summary>
        /// Forget to <see cref="Container.Populate"/> and use strict method.
        /// </summary>
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void WrongUseOfStrictContract()
        {
            container.GetElementsStrict().Count();
        }

        /// <summary>
        /// Call <see cref="Container.Populate"/> and use strict method.
        /// </summary>
        [Test]
        public void CorrectUsaOfStrictContract()
        {
            container.Populate();
            Assert.AreEqual(0, container.GetElementsStrict().Count());
        }

        /// <summary>
        /// Inconvenient contract - needs a local variable.
        /// </summary>
        [Test]
        public void CarefulUseOfCarelessMethod()
        {
            var elements = container.GetElementsInconvenientCareless();
            Assert.AreEqual(0, elements == null ? 0 : elements.Count());
        }

        /// <summary>
        /// Inconvenient contract - duplicate call in order to use in context of an single expression.
        /// </summary>
        [Test]
        public void LameCarefulUseOfCarelessMethod()
        {
            Assert.AreEqual(0, container.GetElementsInconvenientCareless() == null ? 0 : container.GetElementsInconvenientCareless().Count());
        }

        [Test]
        public void LuckyCarelessUseOfCarelessMethod()
        {
            // INIT
            var praySomeoneCalledPopulateBefore = (Action)(()=>container.Populate());
            praySomeoneCalledPopulateBefore();

            // ACT //ASSERT
            Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
        }

        /// <summary>
        /// Excercise <see cref="ArgumentNullException"/> because of null passed to <see cref="Enumerable.Count{TSource}(System.Collections.Generic.IEnumerable{TSource})"/>
        /// </summary>
        [Test]
        [ExpectedException(typeof(ArgumentNullException))]
        public void UnfortunateCarelessUseOfCarelessMethod()
        {
            Assert.AreEqual(0, container.GetElementsInconvenientCareless().Count());
        }

        /// <summary>
        /// Demonstrates the client code flow relying on returning null for empty collection.
        /// Exception is due to <see cref="Enumerable.First{TSource}(System.Collections.Generic.IEnumerable{TSource})"/> on an empty collection.
        /// </summary>
        [Test]
        [ExpectedException(typeof(InvalidOperationException))]
        public void UnfortunateEducatedUseOfCarelessMethod()
        {
            container.Populate();
            var elements = container.GetElementsInconvenientCareless();
            if (elements == null)
            {
                Assert.Inconclusive();
            }
            Assert.IsNotNull(elements.First());
        }

        /// <summary>
        /// Demonstrates the client code is bloated a bit, to compensate for implementation 'cleverness'.
        /// We can throw away the nullness result, because we don't know if the operation succeeded or not anyway.
        /// We are unfortunate to create a new instance of an empty collection.
        /// We might have already had one inside the implementation,
        /// but it have been discarded then in an effort to return null for empty collection.
        /// </summary>
        [Test]
        public void EducatedUseOfCarefullMethod()
        {
            Assert.AreEqual(0, (container.GetElementsInconvenientCarefull() ?? Enumerable.Empty<Element>()).Count());
        }
    }
}

2

Я называю это своей ошибкой в ​​миллиард долларов ... В то время я разрабатывал первую комплексную систему типов для ссылок на объектно-ориентированном языке. Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с проверкой, выполняемой автоматически компилятором. Но я не мог удержаться от соблазна вставить нулевую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и сбоям системы, которые, вероятно, причинили миллиард долларов боли и ущерба за последние сорок лет. - Тони Хоар, изобретатель ALGOL W.

Смотрите здесь для сложной дерьмовой бури nullв целом. Я не согласен с утверждением, которое undefinedявляется другим null, но это все еще стоит прочитать. И это объясняет, почему вы должны избегать nullвообще, а не только в случае, который вы спросили. Суть в том, что nullна любом языке это особый случай. Вы должны думать nullкак об исключении. undefinedотличается тем, что код, имеющий дело с неопределенным поведением, в большинстве случаев является просто ошибкой. C и большинство других языков также имеют неопределенное поведение, но большинство из них не имеют идентификатора для этого языка.


1

С точки зрения управления сложностью, основной целью разработки программного обеспечения, мы хотим избежать распространения ненужной цикломатической сложности на клиентов API. Возврат нулевого значения клиенту аналогичен возврату им стоимости цикломатической сложности другой ветви кода.

(Это соответствует бремени модульного тестирования. Вам потребуется написать тест для нулевого возвращаемого случая, в дополнение к пустому возвращаемому случаю коллекции.)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.