Це свого роду загальне запитання (але я використовую 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-го видання Framework Design Guidelines (стор. 256):
НЕ повертайте нульові значення з властивостей колекції або з методів повернення колекцій. Натомість поверніть порожню колекцію чи порожній масив.
Ось ще одна цікава стаття про переваги неповернення нулів (я намагався щось знайти у блозі Бреда Абрама, і він посилався на цю статтю).
Правка - як Ерік Ліпперт зараз прокоментував оригінальне запитання, я також хотів би посилання на його чудову статтю .
Залежить від вашого контракту та конкретного випадку . Як правило , найкраще повертати порожні колекції , але іноді ( рідко ):
null
може означати щось більш конкретне;null
.Деякі конкретні приклади:
null
означає, що елемент відсутній, тоді як порожня колекція буде надлишковою (і, можливо, неправильною)<collection />
Є ще один момент, який ще не згадувався. Розглянемо наступний код:
public static IEnumerable<string> GetFavoriteEmoSongs()
{
yield break;
}
Мова C # поверне порожній нумератор при виклику цього методу. Тому, щоб бути узгодженим з мовним дизайном (і, таким чином, очікуваннями програміста), порожню колекцію слід повернути.
Empty набагато привабливіший для споживачів.
Існує чіткий метод складання порожнього переліку:
Enumerable.Empty<Element>()
Мені здається, ви повинні повернути значення, яке є семантично правильним у контексті, яким би воно не було. Правило, яке говорить "завжди повертай порожню колекцію", здається мені трохи спрощеним.
Припустимо, у системі лікарні у нас є функція, яка повинна повертати список усіх попередніх госпіталізацій за останні 5 років. Якщо клієнт не був у лікарні, має сенс повернути порожній список. Але що робити, якщо замовник залишив цю частину форми прийому пустим? Нам потрібно інше значення, щоб відрізнити "порожній список" від "немає відповіді" або "не знаю". Ми можемо кинути виняток, але це не обов'язково умова помилки, і це не обов'язково виганяє нас із нормального потоку програми.
Мене часто засмучують системи, які не можуть розрізнити нуль і немає відповіді. У мене не раз траплялося, коли система запитувала мене ввести якесь число, я вводить нуль, і я отримую повідомлення про помилку, яке говорить мені, що я повинен ввести значення в цьому полі. Я щойно зробив: я ввійшов до нуля! Але він не прийме нуля, тому що не може відрізнити його від жодної відповіді.
Відповідь Saunders:
Так, я припускаю, що існує різниця між "Особа не відповіла на запитання" та "Відповідь була нульовою". Це був пункт останнього пункту моєї відповіді. Багато програм не в змозі відрізнити "не знаю" від порожнього або нуля, що мені здається потенційно серйозним недоліком. Наприклад, я купував будинок рік або близько того. Я зайшов на веб-сайт з нерухомістю, і там було багато будинків, перелічених із запитаною ціною 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 ...
}
Це насправді рядок або два менше коду з нульовим поверненням, тому це не більше навантаження на абонента, тим менше.
Я не бачу, як це створює проблеми DRY. Це не так, як ми повинні виконати дзвінок двічі. Якщо ми завжди хотіли зробити те ж саме, коли список не існує, можливо, ми могли б натиснути обробку вниз на функцію get-list, а не на те, щоб абонент це робив, і тому введення коду в абонента буде сухим порушенням. Але ми майже напевно не хочемо завжди робити те саме. У функціях, де ми повинні мати список для обробки, відсутній список - це помилка, яка могла б зупинити обробку. Але на екрані редагування ми, звичайно, не хочемо зупиняти обробку, якщо вони ще не ввели дані: ми хочемо дозволити їм вводити дані. Тож обробка "немає списку" повинна здійснюватися на рівні абонента так чи інакше. І чи будемо ми це робити з нульовим поверненням або окремою функцією, не має значення для більшого принципу.
Звичайно, якщо абонент не перевіряє на null, програма може вийти з ладу за виключенням null-pointer. Але якщо є окрема функція "отримав будь-яку", і абонент не викликає цю функцію, а сліпо викликає функцію "отримати список", то що відбувається? Якщо він викидає виняток або не вдається іншим чином, це майже те саме, що було б, якби він повернувся до нуля і не перевірив його. Якщо він повертає порожній список, це просто неправильно. Ви не можете розрізнити "У мене список з нульовими елементами" та "У мене немає списку". Це як повернення нуля за ціною, коли користувач не вводив жодної ціни: це просто неправильно.
Я не бачу, як допомагає додавання додаткового атрибуту до колекції. Абонент ще повинен це перевірити. Як це краще, ніж перевірка нуля? Знову ж таки, найгірше, що може статися, - це програміст забути перевірити це та дати неправильні результати.
Функція, яка повертає null, не є сюрпризом, якщо програміст знайомий з поняттям null, що означає "не мають значення", про який я думаю, що будь-який компетентний програміст повинен був почути, вважає він це гарною ідеєю чи ні. Я думаю, що окрема функція - це скоріше проблема "сюрпризу". Якщо програміст не знайомий з API, під час запуску тесту без даних він швидко виявить, що іноді він отримує нуль. Але як би він виявив існування іншої функції, якщо йому не прийшло в голову, що може існувати така функція, і він перевіряє документацію, а документація є повною і зрозумілою? Я набагато скоріше мати одну функцію, яка завжди дає мені змістовну відповідь, а не дві функції, які я повинен знати і пам'ятати, щоб називати обидві.
Якщо порожня колекція має сенс семантично, це я вважаю за краще повернути. Повертаючи порожню колекцію для GetMessagesInMyInbox()
повідомлень, "у вас дійсно немає повідомлень у папці" Вхідні ", тоді як повернення 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, щоб вказати порожній результат, абонент повинен додатково перерахувати нульовий адаптер перевірки. Потім цей код дублюється у різних абонентів, то чому б не помістити цей адаптер всередину методу, щоб його можна було повторно використовувати.
Дійсне використання 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. Повернення клієнта нуля - це як повернення їм вартості циклічної складності іншої гілки коду.
(Це відповідає навантаженню тесту одиниці. Вам потрібно написати тест для нульового випадку повернення, крім порожнього випадку повернення колекції.)