Чи слід приймати порожні колекції у своїх методах, які повторюють їх?


40

У мене є метод, коли вся логіка виконується всередині циклу foreach, який переходить через параметр методу:

public IEnumerable<TransformedNode> TransformNodes(IEnumerable<Node> nodes)
{
    foreach(var node in nodes)
    {
        // yadda yadda yadda
        yield return transformedNode;
    }
}

У цьому випадку надсилання порожньої колекції призводить до порожньої колекції, але мені цікаво, чи це нерозумно.

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

Чи варто вловлювати цю поведінку і кидати для неї виняток, або найкраще повернути порожню колекцію?


43
Ви впевнені, що ваше припущення "якщо хтось називає цей метод, то він має намір передавати дані", є правильним? Можливо, вони просто передають те, що їм під руку, щоб працювати з результатом.
SpaceTrucker

7
BTW: ваш метод "перетворення" зазвичай називають "map", хоча в .NET (і SQL, звідки .NET отримав ім'я), його зазвичай називають "select". "Трансформація" - це те, що називає C ++, але це не є загальним іменем, яке можна визнати.
Йорг W Міттаг

25
Я б кинув, якщо колекція є, nullале ні, якщо вона порожня.
CodesInChaos

21
@NickUdell - Я постійно передаю порожні колекції у своєму коді. Коду легше поводитись так само, ніж писати спеціальну логіку розгалуження для тих випадків, коли я не зміг додати нічого до своєї колекції, перш ніж продовжувати працювати з нею.
theMayer

7
Якщо ви перевіряєте та видаєте помилку, якщо колекція порожня, ви змушуєте свого абонента також перевіряти та не передавати порожню колекцію вашому методу. Перевірки слід проводити на (і тільки на) системних межах, якщо порожні колекції вас турбують, ви вже повинні перевірити їх задовго до того, як вони досягли будь-яких корисних методів. Тепер у вас є дві зайві перевірки.
Лежати Райан

Відповіді:


175

Корисні методи не повинні кидатися на порожні колекції. Ваші клієнти API ненавиджу вас за це.

Колекція може бути порожньою; концептуально набагато складніше працювати з "колекцією, що не повинна бути порожньою".

Трансформація порожньої колекції має очевидний результат: порожня колекція. (Ви навіть можете зберегти сміття, повернувши сам параметр.)

Існує багато обставин, коли модуль підтримує списки речей, які можуть бути, а можуть і не бути заповнені чимось. Необхідність перевіряти порожнечу перед кожним закликом transformдратує і має потенціал перетворити простий, елегантний алгоритм у некрасивий безлад.

Корисні методи завжди повинні прагнути бути ліберальними у своїх вкладах та консервативними у своїх результатах.

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


14
+1 - (більше, якщо я міг). Я можу навіть зайти так далеко, щоб також злитись nullу порожню колекцію. Функції з неявними обмеженнями - це біль.
Теластин

6
"Ні, ти не повинен" ... не повинен їх приймати або не повинен кидати виняток?
Бен Ааронсон

16
@Andy - це не були б корисними методами. Це були б бізнес-методи чи подібна конкретна поведінка.
Теластин

38
Я не думаю, що переробка порожньої колекції для повернення є хорошою ідеєю. Ви зберігаєте лише невеликий шматочок пам'яті; і це може призвести до несподіваної поведінки абонента. Злегка надуманий приклад: Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); Якщо Populate1 залишив колекцію порожньою, і ви повернули її для першого виклику TransformNodes, вихід1 та введення були б такою ж колекцією, і коли Populaten2 був викликаний, якщо він поставив вузли у колекції, ви закінчилися б другою набір вводу у вихід2.
Dan Neely

6
@Andy Навіть так, якщо аргумент ніколи не повинен бути порожнім - якщо це помилка, якщо дані не обробляються - тоді я відчуваю, що відповідальність абонента повинна перевірити це, коли він суперечить цей аргумент. Цей метод не тільки зберігає більш елегантність, але й означає, що можна створити краще повідомлення про помилку (кращий контекст) та підвищити його в невдалому вигляді. Немає сенсу для нижчого класу перевіряти інваріантів абонента ...
Анджей Дойл

26

Я бачу два важливі питання, які визначають відповідь на це:

  1. Чи може ваша функція повернути щось змістовне та логічне, коли пройде пусту колекцію (включаючи нульову )?
  2. Який загальний стиль програмування в цій програмі / бібліотеці / команді? (Конкретніше, як у вас FP?)

1. Змістовне повернення

По суті, якщо ви зможете повернути щось значиме, то не кидайте винятку. Нехай абонент погодиться з результатом. Тож якщо ваша функція ...

  • Підраховує кількість елементів у колекції, повертаючи 0. Це було просто.
  • Пошук елементів, які відповідають певним критеріям, повертає порожню колекцію. Будь ласка, нічого не кидайте. У абонента може бути величезна колекція колекцій, частина яких порожня, а деякі ні. Абонент бажає будь-яких відповідних елементів у будь-якій колекції. Винятки лише ускладнюють життя абонента.
  • Шукає найбільший / найменший / найкращий за критеріями у списку. На жаль Залежно від питання про стиль, ви можете кинути тут виняток або повернути нуль . Я ненавиджу null (дуже людина FP), але це, мабуть, важливіше, і це дозволяє вам залишати винятки для несподіваних помилок у власному коді. Якщо абонент не перевіряє нуль, все одно вийде досить чітке виключення. Але це ви залишаєте їм.
  • Запитує n-й елемент колекції або перший / останній n елементів. Це найкращий випадок для винятку і той, який найменш вірогідно може викликати плутанину та труднощі для абонента. Справа все ще може бути зведена на нуль, якщо ви та ваша команда звикли перевірити це з усіх причин, наведених у попередньому пункті, але це найбільш вірний випадок, поки викидаєте виняток DudeYou Know YouShouldCheckTheSizeFirst . Якщо ваш стиль є більш функціональним, то будь-що скасуйте чи прочитайте відповідь на мій стиль .

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

2. Стиль

Чи корисний ваш загальний код (або код проекту чи код команди) функціональному стилю? Якщо ні, винятки будуть очікуватися та їх вирішувати. Якщо так, то розгляньте можливість повернення типу опції . З типом опції ви або повертаєте змістовну відповідь, або Нічого / Нічого . У моєму третьому прикладі зверху Ніщо не буде гарною відповіддю у стилі FP. Факт, що функція повертає сигнал вибору Option явно абоненту, ніж змістовна відповідь, може виявитися неможливим і що абонент повинен бути готовий з цим впоратися. Я відчуваю, що це надає абоненту більше варіантів (якщо ви простите каламбур).

F #, де все прохолодні .Net діти роблять такі речі , але C # робить підтримку цього стилю.

тл; д-р

Зберігайте винятки для несподіваних помилок у власному кодовому шляху, не цілком передбачуваному (та законному) введенні з чужого.


"Найкраще" тощо. Можливо, ви маєте метод, який знайде об'єкт, що найкраще підходить, у колекції, який відповідає деякому критерію. Цей метод матиме ту саму проблему і для не порожніх колекцій, оскільки ніщо не може відповідати критерію, тому не потрібно турбуватися про порожні виділення.
gnasher729

1
Ні, саме тому я сказав "найкраще", а не "збіги"; словосполучення передбачає найближче наближення, а не точну відповідність. Найбільший / найменший варіант повинен був бути підказкою. Якщо було запропоновано лише "кращу придатність", то будь-яка колекція з 1 або більше членами повинна мати можливість повернути її. Якщо є лише один член, це найкраще підходить.
йогобрюс

1
Повернення "null" у більшості випадків гірше, ніж кидання виключення. Однак повернення порожньої колекції має сенс.
Ян

1
Ні, якщо вам доведеться повернути один елемент (хв / макс / голова / хвіст). Що стосується нуля, то, як я вже сказав, я ненавиджу його (і віддаю перевагу мовам, які його не мають), але там, де мова має його, ви мусите мати справу з цим кодом, який передає його. Залежно від локальних умов кодування, воно може бути доречним.
йогобурс

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

18

Як завжди, це залежить.

Чи має значення, що колекція порожня?
Більшість кодів поводження з колекцією, ймовірно, сказав би "ні"; у колекції може бути будь-яка кількість предметів, включаючи нуль.

Тепер, якщо у вас є якась колекція, де "недійсно" не мати в ній предметів, то це нова вимога, і ви повинні вирішити, що з цим робити.

Займіть деяку логіку тестування у світі баз даних: тест на нульові елементи, один предмет та два елементи. Це забезпечує найважливіші випадки (вимивання будь-яких погано сформованих внутрішніх або декартових умов приєднання).


І якщо в колекції недійсно відсутні елементи, тоді в ідеалі аргументом буде не java.util.Collectionіндивідуальний com.foo.util.NonEmptyCollectionклас, а власний клас, який може постійно зберігати цей інваріант і не дозволяти вам потрапити в недійсний стан для початку.
Анджей Дойл

3
Це питання позначено тегами [c #]
Nick Udell

@NickUdell чи C # не підтримує об'єктно-орієнтоване програмування або концепцію вкладених просторів імен? TIL.
Джон Дворак

2
Це робить. Мій коментар був уточненням, оскільки Анджей, мабуть, був розгублений (бо чому б інакше вони намагалися вказати простір імен для мови, на яке це питання не посилається?).
Нік Уделл

-1 для підкреслення "це залежить" більше, ніж "майже напевно так". Не жартуйте - повідомляти неправильну річ тут шкодить.
djechlin

11

Що стосується гарного дизайну, прийміть якомога більше варіацій ваших внесків, як це практично або можливо. Винятки слід викидати лише тоді, коли (подається неприйнятний вхід АБО трапляються несподівані помилки під час обробки) І програма не може продовжуватись передбачуваним чином .

У цьому випадку слід очікувати, що пуста колекція буде представлена, і ваш код повинен обробляти її (що вона вже робить). Було б порушення всього доброго, якби ваш код тут кинув виняток. Це було б подібним до множення 0 на 0 в математиці. Це зайве, але абсолютно необхідне, щоб він працював так, як це робиться.

Тепер перейдемо до аргументу нульового збору. У цьому випадку нульова колекція є помилкою програмування: програміст забув призначити змінну. Це випадок, коли викид може бути викинутий, тому що ви не зможете осмислено обробити це у висновку, а спроба зробити це призведе до несподіваної поведінки. Це було б подібне до ділення на нуль у математиці - це абсолютно безглуздо.


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

@AAA - явно ми не пишемо книги про те, як тут розробляти програмне забезпечення, але я не думаю, що це взагалі погана порада, якщо ти справді розумієш, що я говорю. Моя думка полягає в тому, що якщо поганий вхід видає неоднозначний або довільний вихід, вам потрібно кинути виняток. У випадках, коли вихід є повністю передбачуваним (як це відбувається у нас), викидання виключення було б довільним. Головне - ніколи не приймати довільних рішень у вашій програмі, оскільки саме звідси походить непередбачувана поведінка.
theMayer

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

10

Правильне рішення набагато важче зрозуміти, коли ти просто дивишся на свою функцію ізольовано. Розгляньте свою функцію як частину більшої проблеми . Одне можливе рішення цього прикладу виглядає приблизно так (у Scala):

input.split("\\D")
.filterNot (_.isEmpty)
.map (_.toInt)
.filter (x => x >= 1000 && x <= 9999)

Спочатку ви розділите рядок на нецифрові цифри, відфільтруйте порожні рядки, перетворіть рядки в цілі числа, потім фільтруйте, щоб зберегти лише чотиризначні числа. Ваша функція може бути map (_.toInt)в роботі.

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

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


+1 за надзвичайно ефективний приклад (бракує інших хороших відповідей).
djechlin

2

Це питання насправді стосується винятків. Якщо ви дивитесь на це таким чином і ігноруєте порожню колекцію як детальну інформацію про реалізацію, відповідь однозначна:

1) Метод повинен кидати виняток, коли він не може продовжуватись: або не в змозі виконати призначене завдання, або повернути відповідне значення.

2) Метод повинен зафіксувати виняток, коли він може продовжуватись, незважаючи на збій.

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

Повертає чи порожню колекцію чи нуль, трохи складніше, але не так багато: якщо можливо, слід уникати зведених колекцій. Мета колекції, що зводить нанівець, полягала б у тому, щоб вказати (як у SQL), що у вас немає такої інформації - наприклад, зібрання дітей може бути нульовим, якщо ви не знаєте, чи є хтось, але у вас немає знайте, що вони цього не роблять. Але якщо це важливо з якихось причин, напевно, варто відслідковувати додаткову змінну.


1

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

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

Якщо метод був названий JoinSqlColumnNamesі призначений для повернення рядка, де елементи з'єднуються комою для використання в SQL-запитах, то було б доцільно кинути IllegalArgumentExceptionна порожню колекцію, оскільки абонент в кінцевому підсумку отримає помилку SQL у будь-якому випадку, якщо він використовує рядок у SQL-запиті безпосередньо без додаткових перевірок, і замість того, щоб перевірити повернуту порожню рядок, він насправді замість цього повинен був перевірити порожню колекцію.


Maxніщо - це часто негативна нескінченність.
djechlin

0

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

Якщо вхідний масив порожній (або нульовий), чи можете ви обгрунтовано виконати запит абонента? Ні. Які варіанти? Ну, ви могли:

  • подати / повернути / кинути помилку. використовуючи умову вашої кодової бази для цього класу помилок.
  • документа, що таке значення, як нуль, буде повернуто
  • документ про повернення призначеного недійсного значення (наприклад, NaN)
  • документ, що магічне значення буде повернуто (наприклад, min або max для типу або якесь сподіваюче орієнтовне значення)
  • оголосити результат не визначеним
  • оголосити дію не визначеною
  • тощо.

Я кажу, дайте їм помилку, якщо вони дали вам недійсний вклад і запит не може бути виконаний. Я маю на увазі сильну помилку з першого дня, щоб вони зрозуміли вимоги вашої програми. Зрештою, ваша функція не в змозі відповісти. Якщо операція не змогла (наприклад, скопіювати файл), то ваш API повинен надати їм помилку, з якою вони можуть зіткнутися.

Таким чином, це може визначити, як ваша бібліотека обробляє помилкові запити та запити, які можуть не працювати.

Дуже важливо, щоб ваш код був послідовним у тому, як він обробляє ці класи помилок.

Наступна категорія - визначити, як ваша бібліотека обробляє запити дурниць. Повертаючись до прикладу подібного Вашим - давайте використовувати функцію , яка визначає , чи існує файл в дорозі: bool FileExistsAtPath(String). Якщо клієнт передає порожній рядок, як ви впораєтесь із цим сценарієм? Як щодо порожнього чи нульового масиву, переданого void SaveDocuments(Array<Document>)? Виберіть свою бібліотеку / базу коду та будьте послідовними. Мені трапляється вважати ці випадки помилками і забороняю клієнтам робити безглузді запити, позначаючи їх як помилки (через твердження). Деякі люди будуть сильно протистояти цій ідеї / дії. Я вважаю це виявлення помилок дуже корисним. Це дуже добре для розміщення питань у програмах - з хорошою локалізацією до програми, яка порушує правопорушення. Програми набагато чіткіші та правильніші (враховуйте еволюцію вашої кодової бази) і не записуйте цикли в межах функцій, які нічого не роблять. Таким чином, код менший / чистіший, і чеки, як правило, висуваються на місця, де може бути введена проблема.


6
У вашому останньому прикладі не завдання цієї функції визначати нісенітницю. Тому порожній рядок повинен повертати помилкове. Дійсно, саме такий підхід використовує File.Exists .
theMayer

@ rmayer06 - це його робота. функція, на яку посилається, дозволяє нульові, порожні рядки, перевіряє недійсні символи в рядку, виконує обрізання рядків, може знадобитися здійснювати виклики файлової системи, може знадобитися запитувати середовище тощо. висока вартість, і вони точно розмивають лінії коректності (ІМО). Я бачив безліч if (!FileExists(path)) { ...create it!!!... }помилок - багато з яких би потрапили до вчинення, якби правильність не розмита.
Джастін

2
Я би погодився з вами, якби назва функції була File.ThrowExceptionIfPathIsInvalid, але хто з їх розуму назвав би таку функцію?
theMayer

В середньому 0 елементів вже збираються повернути NaN або викинути виняток ділення на нуль, саме тому, як визначено функцію. Файл із іменем, яке, можливо, не існує, або не буде існувати, або вже спричинить помилку. Немає вагомих причин спеціально розбирати ці справи.
cHao

Можна навіть стверджувати, що вся концепція перевірки існування файлу перед читанням чи його написанням все-таки є хибною. (Існує притаманна умова перегонів.) Краще просто продовжувати і спробувати відкрити файл для читання або з налаштуваннями, створеними лише для створення, якщо ви пишете. Він уже вийде з ладу, якщо ім'я файлу недійсне або не існує (або в разі запису).
cHao

-3

Як правило, стандартна функція повинна мати можливість приймати найширший список входів та давати відгуки про неї. Є багато прикладів, коли програмісти використовують функції так, як дизайнер не планував, маючи це на увазі, я вважаю, що функція повинні вміти приймати не тільки порожні колекції, але й широкий спектр типів введення та витончено повертати зворотній зв'язок, будь то навіть об’єкт помилки з будь-якої операції, яка виконувалася на вході ...


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

3
"Широкий спектр типів введення" тут виглядає неактуальним. Питання позначено тегом c # , сильно набраною мовою. Тобто компілятор гарантує, що вхід є збіркою
gnat

Люб'язно google "велике правило", моя відповідь була узагальненою застереженням, що як програміст ви створюєте місце для непередбачених або помилкових подій, один з них може помилково передавати аргументи функції ...
Климент Марк-Аба

1
Це або неправильно, або тавтологічно. writePaycheckToEmployeeне повинен приймати від'ємне число як вхідне ... але якщо "зворотній зв'язок" означає "робить щось, що може статися далі", то так, будь-яка функція буде робити щось, що робить далі.
djechlin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.