Чи краще повернути нульову чи порожню колекцію?


420

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


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

3
@CPerkins - Так, є. Це чітко зазначено у власних інструкціях 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", нульове повернення було б краще (якщо ви не хочете обробляти цей виняток за винятком)
Patonza

30
не завжди виникає питання про те, "чи можна легко повернути порожній масив", а не те, чи може порожній масив вводити в оману в поточному контексті. Порожній масив насправді щось означає, як і null. Сказати, що ви завжди повинні повертати порожній масив, а не нуль, майже так само неправильно, як і те, що булевий метод завжди повинен повертати істину. Обидва можливі значення передають сенс.
Девід Хедлунд

31
Насправді слід віддати перевагу поверненню System.Linq.Enumerable.Empty <Foo> () замість нового Foo [0]. Це більш явно і економить один розподіл пам'яті (принаймні, у моїй встановленій .NET реалізації).
Триліан

4
@Will: OP: "чи повертаєте нульову чи порожню колекцію для методу, який має колекцію як тип повернення". Я думаю, що в цьому випадку це IEnumerableчи ICollectionне так важливо. У будь-якому випадку, якщо ви виберете щось із типу, ICollectionвони також повертаються null... Я хотів би, щоб вони повернули порожню колекцію, але я наткнувся на них, повертаючись null, тому я подумав, що тут просто згадаю. Я б сказав, що за замовчуванням колекція перелічених порожніх не є нульовими. Я не знав, що це така чутлива тема.
Matthijs Wessels


154

З 2-го видання Framework Design Guidelines (стор. 256):

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

Ось ще одна цікава стаття про переваги неповернення нулів (я намагався щось знайти у блозі Бреда Абрама, і він посилався на цю статтю).

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


33
+1. Це завжди найкраща практика дотримуватися керівних принципів дизайну рамок, якщо у вас є ДУЖЕ ДУЖОГО приводу цього не робити.

@Будь, абсолютно. Це я слідую. Я ніколи не знаходив жодної необхідності робити інше
RichardOD

Так, саме за цим ви йдете. Але у випадках, коли API, на які ви покладаєтесь, не дотримуються цього, ви потрапляєте у пастку;)
Божо

@ Божо-дріж. Ваша відповідь дає кілька приємних відповідей на ці випадки.
RichardOD

90

Залежить від вашого контракту та конкретного випадку . Як правило , найкраще повертати порожні колекції , але іноді ( рідко ):

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

Деякі конкретні приклади:

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

4
@ Bozho - кілька цікавих прикладів тут.
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

Empty набагато привабливіший для споживачів.

Існує чіткий метод складання порожнього переліку:

Enumerable.Empty<Element>()

2
Дякую за акуратну хитрість. Я створював порожній список <T> (), як ідіот, але це виглядає набагато чистіше і, мабуть, трохи ефективніше.
Jacobs Data Solutions

18

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

Припустимо, у системі лікарні у нас є функція, яка повинна повертати список усіх попередніх госпіталізацій за останні 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, під час запуску тесту без даних він швидко виявить, що іноді він отримує нуль. Але як би він виявив існування іншої функції, якщо йому не прийшло в голову, що може існувати така функція, і він перевіряє документацію, а документація є повною і зрозумілою? Я набагато скоріше мати одну функцію, яка завжди дає мені змістовну відповідь, а не дві функції, які я повинен знати і пам'ятати, щоб називати обидві.


4
Чому потрібно поєднувати відповіді "без відповіді" та "нуля" в одному поверненому значенні? Натомість, чи повернути метод "будь-які попередні госпіталізації за останні п’ять років", і окремий метод, який запитує, "чи був заповнений попередній список госпіталізацій?". Це передбачає, що існує різниця між списком, заповненим без попередніх госпіталізацій, і списком, не заповненим.
Джон Сондерс,

2
Але якщо ви повернетеся до нуля, ви все одно накладаєте додаткове навантаження на абонента! Абонент повинен перевірити кожне повернене значення на null - жахливе порушення DRY. Якщо ви хочете , щоб вказати «абонент не відповідає на питання" окремо, то це простіше створити додатковий виклик методу , щоб вказати цей факт. Або це, або використовувати похідний тип колекції з додатковою властивістю DeclinedToAnswer. Правило ніколи не повертати нуль зовсім не довільне, це Принцип найменшого сюрпризу. Крім того, тип повернення вашого методу повинен означати те, що його ім'я говорить, що це робить. Нуль майже точно не відповідає.
варення

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

3
ПОЗНАЧАЙТЕ назву: жодна назва функції не може повністю описати те, що функція виконує, якщо вона не є функцією і, таким чином, дико непрактично. Але в будь-якому випадку, якщо функція повертає порожній список, коли не було дано відповіді, то, з тих же міркувань, чи не слід це називати "getHospitalizationListOrEmptyListIfNoAnswer"? Але дійсно, чи наполягаєте ви, що функцію Java Reader.read слід перейменувати на readCharacterOrReturnMinusOneOnEndOfStream? Що ResultSet.getInt справді має бути "getIntOrZeroIfValueWasNull"? І т. Д.
Джей

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

10

Якщо порожня колекція має сенс семантично, це я вважаю за краще повернути. Повертаючи порожню колекцію для GetMessagesInMyInbox()повідомлень, "у вас дійсно немає повідомлень у папці" Вхідні ", тоді як повернення nullможе бути корисним для повідомлення про відсутність недостатньої кількості даних, щоб сказати, як повинен виглядати список, який може бути повернутий.


6
У прикладі, який ви надаєте, це здається, що метод, ймовірно, повинен викинути виняток, якщо він не може виконати запит, а не просто повернути null. Винятки набагато корисніші для діагностики проблем, ніж нулі.
Грег Бук

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

6

Повернення нуля може бути ефективнішим, оскільки не створюється новий об’єкт. Однак, це також часто вимагає 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

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


2

Порожня колекція. Якщо ви використовуєте C #, припущення полягає в тому, що максимізація системних ресурсів не є істотним. Хоча менш ефективно, повернення порожньої колекції набагато зручніше для залучених програмістів (з причини, яку Вілл виклав вище).


2

Повернення порожньої колекції в більшості випадків краще.

Причиною цього є зручність виконання абоненту, послідовний контракт та легша реалізація.

Якщо метод повертає 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());
        }
    }
}

2

Я називаю це моєю помилкою в мільярд доларів ... У той час я розробляв першу комплексну систему для посилань на об'єктно-орієнтованій мові. Моя мета полягала в тому, щоб будь-яке використання посилань було абсолютно безпечним, а перевірка виконувалась автоматично компілятором. Але я не витримав спокуси ввести нульову посилання просто тому, що це було так просто здійснити. Це призвело до незліченних помилок, вразливості та збоїв у системі, які, ймовірно, заподіяли біль і шкоду в мільярд доларів за останні сорок років. - Тоні Хоаре, винахідник ALGOL W.

Ознайомтеся тут із детальною бурею лайна про nullзагалом. Я не згоден із твердженням, яке undefinedє іншим null, але його все одно варто прочитати. І це пояснює, чому вам взагалі слід уникати, nullа не лише у тому випадку, про який ви просили. Суть у тому, що nullв будь-якій мові особливий випадок. Ви повинні думати nullяк про виняток. undefinedВ іншому спосіб відрізняється тим, що код, що стосується невизначеної поведінки, в більшості випадків є лише помилкою. C та більшість інших мов також мають не визначене поведінку, але більшість з них не мають ідентифікатора для цієї мови.


1

З точки зору управління складністю, основного завдання інженерного забезпечення програмного забезпечення, ми хочемо уникати поширення непотрібних цикломатичних складностей клієнтам API. Повернення клієнта нуля - це як повернення їм вартості циклічної складності іншої гілки коду.

(Це відповідає навантаженню тесту одиниці. Вам потрібно написати тест для нульового випадку повернення, крім порожнього випадку повернення колекції.)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.