Должен ли я принимать пустые коллекции в моих методах, которые проходят по ним?

40

У меня есть метод, где вся логика выполняется внутри цикла foreach, который перебирает параметр метода:

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

В этом случае отправка пустой коллекции приводит к пустой коллекции, но мне интересно, если это неразумно.

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

Должен ли я поймать это поведение и выдать для него исключение, или лучше возвращать пустую коллекцию?

Ник Уделл
источник
43
Вы уверены, что ваше предположение «если кто-то вызывает этот метод, то он намеревается передать данные», верно? Возможно, они просто передают то, что у них есть, чтобы работать с результатом.
SpaceTrucker
7
Кстати, ваш метод «transform» обычно называется «map», хотя в .NET (и SQL, откуда и получил имя .NET), он обычно называется «select». «Преобразование» - это то, что C ++ называет, но это не общее имя, которое можно узнать.
Jörg W Mittag
25
Я бы бросил, если коллекция есть, nullно нет, если она пустая.
CodesInChaos
21
@NickUdell - я постоянно передаю пустые коллекции в своем коде. Для кода проще вести себя одинаково, чем писать специальную логику ветвления для случаев, когда я не мог ничего добавить в свою коллекцию, прежде чем продолжить с ней.
The Mayer
7
Если вы проверяете и выбрасываете ошибку, если коллекция пуста, то вы заставляете своего вызывающего абонента также проверять и не передавать пустую коллекцию вашему методу. Проверки должны проводиться на (и только на) границах системы. Если вас беспокоят пустые коллекции, вы должны были проверить их задолго до того, как они достигли каких-либо служебных методов. Теперь у вас есть две избыточные проверки.
Ли Райан

Ответы:

175

Служебные методы не должны выбрасывать пустые коллекции. Ваши клиенты API будут ненавидеть вас за это.

Коллекция может быть пустой; концептуально «коллекция, которая не должна быть пустой» - намного сложнее работать.

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

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

Служебные методы всегда должны стремиться быть либеральными по своим вкладам и консервативными по своим результатам.

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

Килиан Фот
источник
14
+1 - (больше, если бы я мог). Я мог бы даже пойти так далеко, чтобы также объединиться nullв пустую коллекцию. Функции с неявными ограничениями - это боль.
Теластин,
6
«Нет, ты не должен» ... не должен принимать их или не должен бросать исключение?
Бен Ааронсон
16
@ Энди - это не будут полезные методы. Это были бы бизнес-методы или какое-то подобное конкретное поведение.
Теластин
38
Я не думаю, что утилизация пустой коллекции для возвращения - это хорошая идея. Вы только экономите немного памяти; и это может привести к неожиданному поведению вызывающей стороны. Слегка надуманный пример: Populate1(input); output1 = TransformNodes(input); Populate2(input); output2 = TransformNodes(input); если Populate1 оставил коллекцию пустой и вы вернули ее для первого вызова TransformNodes, output1 и input будут одной и той же коллекцией, и при вызове Populaten2, если он поместит узлы в коллекцию, вы получите вторую набор ввода в output2.
Дэн Нили
6
@ Andy Несмотря на это, если аргумент никогда не должен быть пустым - если ошибка - отсутствие данных для обработки - тогда я чувствую, что вызывающая сторона несет ответственность за проверку этого, когда он сопоставляет этот аргумент. Он не только делает этот метод более элегантным, но и означает, что можно генерировать лучшее сообщение об ошибке (лучший контекст) и вызывать его безотказно. Для нижестоящего класса не имеет смысла проверять инварианты вызывающей стороны ...
Анджей Дойл
26

Я вижу два важных вопроса, которые определяют ответ на этот вопрос:

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

1. Значимое возвращение

По сути, если вы можете вернуть что-то осмысленное, то не выбрасывайте исключение. Позвольте звонящему разобраться с результатом. Так что если ваша функция ...

  • Подсчитывает количество элементов в коллекции, возвращает 0. Это было легко.
  • Поиск элементов, соответствующих определенным критериям, возвращает пустую коллекцию. Пожалуйста, не бросайте ничего. У вызывающей стороны может быть огромная коллекция коллекций, некоторые из которых пусты, некоторые нет. Вызывающая сторона хочет любые совпадающие элементы в любой коллекции. Исключения только усложняют жизнь звонящего.
  • Ищет самый большой / самый маленький / самый подходящий для критерия в списке. К сожалению. В зависимости от стиля вопроса вы можете сгенерировать исключение или вернуть null . Я ненавижу ноль (очень сильно человек FP), но это, возможно, более значимо здесь, и это позволяет вам зарезервировать исключения для непредвиденных ошибок в вашем собственном коде. Если вызывающая сторона не проверяет нулевое значение, в любом случае произойдет довольно четкое исключение. Но вы оставляете это им.
  • Запрашивает n-й элемент в коллекции или первый / последний n элементов. Это лучший случай для исключения и тот, который меньше всего может вызвать путаницу и трудности для звонящего. Случай все еще может быть выдан для нуля, если вы и ваша команда привыкли проверять это, по всем причинам, указанным в предыдущем пункте, но это самый правильный случай для создания исключения DudeYou Know YouShouldCheckTheSizeFirst . Если ваш стиль более функциональный, тогда либо ноль, либо читайте мой ответ по стилю .

В целом, мой уклон FP говорит мне «вернуть что-то значимое», и в этом случае значение null может иметь действительное значение.

2. Стиль

Ваш общий код (или код проекта или код команды) поддерживает функциональный стиль? Если нет, то исключения будут ожидаться и рассматриваться. Если да, то рассмотрите возможность возврата типа параметра . С типом параметра вы либо возвращаете значимый ответ, либо None / Nothing . В моем третьем примере, приведенном выше, ничего не будет хорошим ответом в стиле FP. Тот факт, что функция возвращает тип Option, сигнализирует вызывающей стороне явно, а не значимый ответ, может быть невозможным, и вызывающая сторона должна быть готова к этому. Я чувствую, что это дает звонящему больше возможностей (если вы простите за каламбур).

F #, где все прохладные .Net дети делают такие вещи , но C # делает поддержку этого стиля.

ТЛ; др

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

itsbruce
источник
«Лучшее соответствие» и т. Д. У вас может быть метод, который находит объект наилучшего соответствия в коллекции, который соответствует некоторому критерию. Этот метод будет иметь ту же проблему для непустых коллекций, потому что ничто не может соответствовать критерию, поэтому не нужно беспокоиться о пустых выборках.
gnasher729
1
Нет, именно поэтому я сказал «лучше подходит», а не «соответствует»; фраза подразумевает ближайшее приближение, а не точное совпадение. Самые большие / самые маленькие варианты должны были быть подсказкой. Если запрашивается только «наилучшее соответствие», то любая коллекция с 1 или более участниками должна иметь возможность вернуть участника. Если есть только один участник, это лучше всего подходит.
Брюс
1
Возвращать «ноль» в большинстве случаев хуже, чем выбрасывать исключение. Однако возвращение пустой коллекции имеет смысл.
Ян
1
Нет, если вам нужно вернуть один предмет (мин / макс / голова / хвост). Что касается нуля, как я уже сказал, я ненавижу его (и предпочитаю языки, у которых его нет), но там, где он есть, вы должны иметь возможность иметь дело с ним и кодировать его. В зависимости от местных соглашений о кодировании, это может быть уместно.
itbruce
Ни один из ваших примеров не имеет ничего общего с пустым вводом. Это просто особые случаи ситуаций, когда ответом может быть «ничего». Что, в свою очередь, должно ответить на вопрос ОП о том, как «обрабатывать» пустую коллекцию.
джечлин
18

Как всегда, это зависит.

Имеет ли значение, что коллекция пуста?
Большая часть кода обработки коллекции, вероятно, сказала бы «нет»; В коллекции может быть любое количество элементов, включая ноль.

Теперь, если у вас есть какая-то коллекция, в которой «недопустимо» не иметь элементов, то это новое требование, и вы должны решить, что с этим делать.

Возьмите некоторую логику тестирования из мира Базы данных: тестируйте ноль предметов, один предмет и два предмета. Это обслуживает наиболее важные случаи (вымывание любых плохо сформированных внутренних или декартовых условий соединения).

Фил В.
источник
И если недопустимо не иметь элементов в коллекции, то в идеале аргумент должен быть не классом, java.util.Collectionа пользовательским com.foo.util.NonEmptyCollectionклассом, который может последовательно сохранять этот инвариант и не позволять вам перейти в недопустимое состояние для начала.
Анджей Дойл
3
Этот вопрос с тегом [c #]
Ник Уделл
@NickUdell Разве C # не поддерживает объектно-ориентированное программирование или концепцию вложенных пространств имен? TIL.
Джон Дворжак
2
Оно делает. Мой комментарий был разъяснением, так как Анджей, должно быть, был сбит с толку (почему бы еще они не попытались указать пространство имен для языка, на который этот вопрос не ссылается?).
Ник Уделл,
-1 для подчеркивания «это зависит» больше, чем «почти наверняка да». Не шутка - общение не того, что наносит ущерб здесь.
Джехлин
11

Принимая во внимание хороший дизайн, примите как можно больше изменений в ваших входах. Исключения должны создаваться только тогда, когда (недопустимый ввод данных ИЛИ непредвиденные ошибки происходят во время обработки) И в результате программа не может продолжаться предсказуемым образом .

В этом случае следует ожидать, что будет представлена ​​пустая коллекция, и ваш код должен обработать ее (что он уже делает). Было бы нарушением всего, что было бы хорошо, если бы ваш код выдал исключение здесь. Это было бы сродни умножению 0 на 0 в математике. Это избыточно, но абсолютно необходимо, чтобы оно работало так, как оно работает.

Теперь перейдем к аргументу нулевой коллекции. В этом случае нулевая коллекция является ошибкой программирования: программист забыл присвоить переменную. Это тот случай, когда может быть выдано исключение, потому что вы не можете осмысленно обработать это в выводе, и попытка сделать это приведет к неожиданному поведению. Это было бы сродни делению на ноль в математике - это совершенно бессмысленно.

theMayer
источник
Ваше смелое утверждение - крайне плохой совет в полной общности. Я думаю, что это правда, но вам нужно объяснить, что особенного в этом обстоятельстве. (Это плохой совет в целом, потому что он способствует тихим сбоям.)
djechlin
@AAA - очевидно, мы не пишем книгу о том, как разрабатывать программное обеспечение здесь, но я не думаю, что это плохой совет, если вы действительно понимаете, что я говорю. Я хочу сказать, что если неправильный ввод приводит к неоднозначному или произвольному выводу, вам нужно создать исключение. В случаях, когда выходные данные полностью предсказуемы (как в данном случае), генерирование исключения будет произвольным. Ключ никогда не заключается в том, чтобы принимать произвольные решения в вашей программе, поскольку именно в этом и заключается непредсказуемое поведение.
The Mayer
Но это ваше первое и единственное выделенное предложение ... никто еще не понимает, что вы говорите. (Я не пытаюсь быть слишком агрессивным здесь, только одна вещь, которую мы здесь делаем, это учиться писать и общаться, и я делаю то, что потеря точности в ключевой точке вредна.)
djechlin
10

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

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

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

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

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

Карл Билефельдт
источник
+1 за чрезвычайно эффективный пример (не хватает других хороших ответов).
Джехлин
2

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

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

2) Метод должен перехватить исключение, когда он может продолжить работу, несмотря на ошибку.

Таким образом, ваш вспомогательный метод не должен быть «полезным» и генерировать исключение, если он не может выполнить свою работу с пустой коллекцией. Пусть звонящий определит, можно ли обработать результаты.

Возвращает ли он пустую коллекцию или ноль, немного сложнее, но не намного: обнуляемых коллекций следует избегать, если это возможно. Цель обнуляемой коллекции будет указывать (как в SQL), что у вас нет информации - например, коллекция детей может быть нулевой, если вы не знаете, есть ли у кого-то такая информация, но вы этого не делаете. знаю, что они этого не делают. Но если это важно по какой-то причине, возможно, стоит отслеживать дополнительную переменную.

jmoreno
источник
1

Метод назван TransformNodes. В случае пустой коллекции в качестве входных данных, возвращение пустой коллекции является естественным и интуитивно понятным, и имеет префект математический смысл.

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

Если метод был назван JoinSqlColumnNamesи предназначен для возврата строки, в которой элементы объединяются запятой для использования в запросах SQL, то имеет смысл добавить IllegalArgumentExceptionпустую коллекцию, так как вызывающий в конечном итоге получит ошибку SQL в любом случае, если он использует строка в запросе SQL напрямую без дальнейших проверок, и вместо проверки на возвращаемую пустую строку он действительно должен был вместо этого проверить пустую коллекцию.

Прекратить постоянный вред Монике
источник
Maxиз ничего часто бывает отрицательная бесконечность.
джечлин
0

Давайте вернемся назад и используем другой пример, который вычисляет среднее арифметическое для массива значений.

Если входной массив пуст (или равен нулю), можете ли вы разумно выполнить запрос вызывающей стороны? Нет. Какие у тебя варианты? Ну, вы могли бы:

  • представить / вернуть / выбросить ошибку. используя соглашение вашей кодовой базы для этого класса ошибок.
  • документ, что будет возвращено значение, такое как ноль
  • документ о том, что будет возвращено указанное недействительное значение (например, NaN)
  • документ о том, что будет возвращено магическое значение (например, минимальное или максимальное значение для типа или некоторое, возможно, ориентировочное значение)
  • объявить результат не определено
  • объявить действие не определено
  • и т.п.

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

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

Для вашего кода очень важно быть последовательным в том, как он обрабатывает эти классы ошибок.

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

джастин
источник
6
В последнем примере задача этой функции не состоит в том, чтобы определить, что это за ерунда. Следовательно, пустая строка должна возвращать false. Действительно, это точный подход File.Exists .
The Mayer
@ rmayer06 это его работа. указанная функция разрешает пустые пустые строки, проверяет наличие недопустимых символов в строке, выполняет усечение строки, может потребоваться выполнить вызовы файловой системы, может потребоваться выполнить запрос к среде и т. д., которые часто «избыточные» при выполнении «удобства» имеют высокая стоимость, и они определенно стирают границы правильности (IMO). я видел множество if (!FileExists(path)) { ...create it!!!... }ошибок - многие из которых были бы обнаружены до фиксации, если бы линии корректности не были размыты.
Джастин
2
Я бы согласился с вами, если бы имя функции было File.ThrowExceptionIfPathIsInvalid, но кто в здравом уме назвал бы такую ​​функцию?
TheMayer
В среднем 0 элементов уже собираются либо возвращать NaN, либо генерировать исключение деления на ноль, просто из-за того, как определена функция. Файл с именем, которое не может существовать, либо не будет существовать, либо уже вызовет ошибку. Там нет убедительных причин для рассмотрения этих случаев специально.
cHao
Можно даже утверждать, что в целом концепция проверки существования файла перед его чтением или записью ошибочна. (Есть неотъемлемое условие гонки.) Лучше просто пойти дальше и попытаться открыть файл, для чтения или с настройками только для создания, если вы хотите писать. Он уже потерпит неудачу, если имя файла неверно или не существует (или существует в случае записи).
cHao
-3

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

Клемент Марк-Ааба
источник
Для дизайнера абсолютно неверно предполагать, что коллекция будет всегда передаваться и идти прямо к итерации по упомянутой коллекции, необходимо выполнить проверку, чтобы убедиться, что полученный аргумент соответствует ожидаемому
типу
3
«широкий диапазон типов ввода» выглядит здесь неактуально. Вопрос помечен c # , строго типизированный язык. То есть компилятор гарантирует, что входные данные являются коллекцией
gnat
Пожалуйста, Google "правило большого пальца", мой ответ был обобщенным предупреждением, что, как программист, вы освобождаете место для непредвиденных или ошибочных вхождений, одному из них могут быть ошибочно переданы аргументы функции ...
Клемент Марк-Ааба
1
Это либо неправильно, либо тавтологически. writePaycheckToEmployeeне следует принимать отрицательное число в качестве входных данных ... но если «обратная связь» означает «делает что-то, что может произойти дальше», тогда да, каждая функция будет делать то, что она делает дальше.
djechlin