Это общий вопрос (но я использую C #), каков наилучший способ (лучшая практика), возвращаете ли вы нулевую или пустую коллекцию для метода, у которого коллекция является типом возвращаемого значения?
Это общий вопрос (но я использую C #), каков наилучший способ (лучшая практика), возвращаете ли вы нулевую или пустую коллекцию для метода, у которого коллекция является типом возвращаемого значения?
Ответы:
Пустая коллекция. Всегда.
Это отстой:
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>()
может рассматриваться как более эффективное, чем возвращение, например, новой пустой коллекции или массива.
IEnumerable
или ICollection
не имеет значения , что много. В любом случае, если вы выберете что-то типа, ICollection
они также вернутся null
... Я хотел бы, чтобы они вернули пустую коллекцию, но я столкнулся с ними, возвращая null
, так что я решил упомянуть об этом здесь. Я бы сказал, что по умолчанию для коллекции перечислимых пусто, а не пусто. Я не знал, что это был такой чувствительный предмет.
Из Руководства по разработке структуры, 2-е издание (стр. 256):
НЕ возвращайте нулевые значения из свойств коллекции или из методов, возвращающих коллекции. Вместо этого верните пустую коллекцию или пустой массив.
Вот еще одна интересная статья о преимуществах отсутствия возврата нулей (я пытался найти что-то в блоге Брэда Абрама, и он связался со статьей).
Отредактируйте - поскольку Эрик Липперт теперь прокомментировал оригинальный вопрос, я также хотел бы дать ссылку на его превосходную статью .
Зависит от вашего контракта и вашего конкретного случая . Обычно лучше возвращать пустые коллекции , но иногда ( редко ):
null
может означать что-то более конкретное;null
.Некоторые конкретные примеры:
null
будет означать, что элемент отсутствует, в то время как пустая коллекция приведет к избыточности (и, возможно, неправильно)<collection />
Есть еще один момент, который еще не был упомянут. Рассмотрим следующий код:
public static IEnumerable<string> GetFavoriteEmoSongs()
{
yield break;
}
Язык C # вернет пустой перечислитель при вызове этого метода. Поэтому, чтобы соответствовать структуре языка (и, следовательно, ожиданиям программиста), должна быть возвращена пустая коллекция.
Пусто намного удобнее для потребителей.
Существует понятный способ составления пустого перечислимого:
Enumerable.Empty<Element>()
Мне кажется, что вы должны вернуть значение, семантически правильное в контексте, что бы это ни было. Правило, которое гласит «всегда возвращать пустую коллекцию», кажется мне немного упрощенным.
Предположим, например, что в системе для больницы у нас есть функция, которая должна возвращать список всех предыдущих госпитализаций за последние 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, когда он запускает тест без данных, он быстро обнаружит, что иногда он возвращает ноль. Но как бы он обнаружил существование другой функции, если ему не пришло в голову, что такая функция может существовать, и он проверит документацию, и документация будет полной и понятной? Я бы предпочел иметь одну функцию, которая всегда дает мне значимый ответ, а не две функции, которые я должен знать и не забывать вызывать обе.
Если пустая коллекция имеет смысл семантически, это то, что я предпочитаю возвращать. Возврат пустой коллекции для GetMessagesInMyInbox()
сообщений «у вас действительно нет никаких сообщений во входящих», тогда как возврат null
может быть полезен для сообщения о том, что недостаточно данных, чтобы сказать, как должен выглядеть возвращаемый список.
null
значение определенно не выглядит разумным, я думал об этом в более общих чертах. Исключения также хороши для сообщения о том, что что-то пошло не так, но если «недостаточные данные», о которых идет речь, вполне ожидаемы, то исключение может привести к плохому дизайну. Я скорее думаю о сценарии, где это вполне возможно, и нет никакой ошибки для метода, чтобы иногда не иметь возможности рассчитать ответ.
Возвращение null может быть более эффективным, так как новый объект не создается. Однако это также часто требует null
проверки (или обработки исключений).
Семантически null
и пустой список не означают одно и то же. Различия невелики, и один выбор может быть лучше, чем другой в конкретных случаях.
Независимо от вашего выбора, документируйте это, чтобы избежать путаницы.
Можно утверждать, что аргументация в пользу Null Object Pattern аналогична аргументации в пользу возврата пустой коллекции.
Зависит от ситуации. Если это особый случай, вернуть null. Если функция просто возвращает пустую коллекцию, то очевидно, что это нормально. Однако возвращать пустую коллекцию в качестве особого случая из-за неверных параметров или по другим причинам НЕ является хорошей идеей, поскольку она маскирует условие особого случая.
На самом деле, в этом случае я обычно предпочитаю генерировать исключение, чтобы убедиться, что оно ДЕЙСТВИТЕЛЬНО не игнорируется :)
Сказать, что это делает код более устойчивым (возвращая пустую коллекцию), поскольку они не должны обрабатывать нулевое условие, плохо, поскольку это просто маскирует проблему, которая должна быть обработана вызывающим кодом.
Я бы сказал, что null
это не то же самое, что пустая коллекция, и вы должны выбрать, какая из них лучше всего отражает то, что вы возвращаете. В большинстве случаевnull
ничего (кроме SQL). Пустая коллекция - это что-то, хотя и пустое.
Если вам нужно выбрать один или другой, я бы сказал, что вы должны стремиться к пустой коллекции, а не к нулю. Но бывают случаи, когда пустая коллекция не совпадает с нулевым значением.
Всегда думайте в пользу ваших клиентов (которые используют ваш API):
Возвращение 'null' очень часто создает проблемы с клиентами, которые неправильно обрабатывают нулевые проверки, что вызывает исключение NullPointerException во время выполнения. Я видел случаи, когда такая пропущенная нулевая проверка приводила к возникновению приоритетной производственной проблемы (клиент использовал foreach (...) для нулевого значения). Во время тестирования проблемы не возникало, потому что данные оперировали немного иначе.
Я хотел бы дать объяснение здесь, с подходящим примером.
Рассмотрим случай здесь ..
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
. Поэтому я могу использовать ее напрямую.
Возвращать пустую коллекцию лучше в большинстве случаев.
Причиной этого является удобство реализации вызывающей стороны, согласованный контракт и более простая реализация.
Если метод возвращает нулевое значение для указания пустого результата, вызывающая сторона должна реализовать адаптер проверки на нулевое значение в дополнение к перечислению. Этот код затем дублируется в различных вызывающих программах, так почему бы не поместить этот адаптер в метод, чтобы его можно было использовать повторно.
Допустимое использование 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());
}
}
}
Я называю это своей ошибкой в миллиард долларов ... В то время я разрабатывал первую комплексную систему типов для ссылок на объектно-ориентированном языке. Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с проверкой, выполняемой автоматически компилятором. Но я не мог удержаться от соблазна вставить нулевую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и сбоям системы, которые, вероятно, причинили миллиард долларов боли и ущерба за последние сорок лет. - Тони Хоар, изобретатель ALGOL W.
Смотрите здесь для сложной дерьмовой бури null
в целом. Я не согласен с утверждением, которое undefined
является другим null
, но это все еще стоит прочитать. И это объясняет, почему вы должны избегать null
вообще, а не только в случае, который вы спросили. Суть в том, что null
на любом языке это особый случай. Вы должны думать null
как об исключении. undefined
отличается тем, что код, имеющий дело с неопределенным поведением, в большинстве случаев является просто ошибкой. C и большинство других языков также имеют неопределенное поведение, но большинство из них не имеют идентификатора для этого языка.
С точки зрения управления сложностью, основной целью разработки программного обеспечения, мы хотим избежать распространения ненужной цикломатической сложности на клиентов API. Возврат нулевого значения клиенту аналогичен возврату им стоимости цикломатической сложности другой ветви кода.
(Это соответствует бремени модульного тестирования. Вам потребуется написать тест для нулевого возвращаемого случая, в дополнение к пустому возвращаемому случаю коллекции.)