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

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

Ответ 1

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

Это отстой:

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>() можно считать более эффективным, чем возвращение, например, новая пустая коллекция или массив.

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

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

Ответ 5

Пустой гораздо более дружелюбный к потребителю.

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

Enumerable.Empty<Element>()

Ответ 6

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

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

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


Ответ на Saunders:

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

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

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


Ответ на Jammycakes:

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

С возвратом null:

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, программа может выйти из строя с исключением нулевого указателя. Но если есть отдельная функция "получил какую-либо", и вызывающий абонент не вызывает эту функцию, но слепо называет функцию "получить список", то что происходит? Если он выдает исключение или иным образом терпит неудачу, хорошо, что почти так же, как и то, что произойдет, если оно вернет null и не проверит его. Если он возвращает пустой список, это просто неправильно. Вы не можете отличить "У меня есть список с нулевыми элементами" и "У меня нет списка". Это как возврат нуля за цену, когда пользователь не вводил никакой цены: это просто неправильно.

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

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

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

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

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

Ответ 11

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

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

Ответ 12

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

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

Ответ 13

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

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

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() не будет  return null. Поэтому я могу использовать его напрямую.

Ответ 14

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

Ответ 15

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

Ответ 16

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

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

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

Действительное использование 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());
        }
    }
}

Ответ 17

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

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

Ответ 18

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

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