Я недавно начал работу по программированию на C #, но у меня есть немного опыта в Haskell.
Но я понимаю, что C # является объектно-ориентированным языком, я не хочу вбивать круглый колышек в квадратное отверстие.
Я прочитал статью « Исключение исключений» от Microsoft, в которой говорится:
НЕ возвращайте коды ошибок.
Но будучи привыкшим к Haskell, я использовал тип данных C # OneOf
, возвращая результат как «правильное» значение или ошибку (чаще всего перечисление) как «левое» значение.
Это очень похоже на соглашение Either
в Haskell.
Мне это кажется безопаснее, чем исключения. В C # игнорирование исключений не приводит к ошибке компиляции, а если они не перехвачены, они просто всплывают и приводят к краху вашей программы. Возможно, это лучше, чем игнорирование кода ошибки и создание неопределенного поведения, но сбой программного обеспечения вашего клиента все еще не очень хорошая вещь, особенно когда он выполняет много других важных бизнес-задач в фоновом режиме.
При OneOf
этом нужно быть совершенно недвусмысленным в отношении распаковки и обработки возвращаемого значения и кодов ошибок. И если кто-то не знает, как с этим справиться на этом этапе в стеке вызовов, его нужно поместить в возвращаемое значение текущей функции, чтобы вызывающие знали, что может произойти ошибка.
Но это не тот подход, который предлагает Microsoft.
Является ли использование OneOf
вместо исключений для обработки «обычных» исключений (таких как «Файл не найден» и т. Д.) Разумным подходом или это ужасная практика?
Стоит отметить, что я слышал, что исключения как поток управления считаются серьезным антипаттерном , поэтому, если «исключение» - это то, что вы обычно обрабатываете без завершения программы, разве это не «путь управления»? Я понимаю, что здесь есть немного серой области.
Примечание. Я не использую OneOf
такие вещи, как «Недостаточно памяти», условия, от которых я не ожидаю восстановления, будут по-прежнему вызывать исключения. Но я чувствую, что вполне разумные проблемы, например, пользовательский ввод, который не анализируется, по сути являются «потоком управления» и, вероятно, не должны вызывать исключения.
Последующие мысли:
Из этой дискуссии я забираю следующее:
- Если вы ожидаете, что непосредственный вызывающий объект будет
catch
обрабатывать исключение большую часть времени и продолжит его работу, возможно, по другому пути, он, вероятно, должен быть частью возвращаемого типа.Optional
илиOneOf
может быть полезным здесь. - Если вы ожидаете, что непосредственный вызывающий абонент не будет перехватывать исключение большую часть времени, сгенерируйте исключение, чтобы избежать глупости ручной передачи его в стек.
- Если вы не уверены, что будет делать непосредственный абонент, возможно, предоставьте и то,
Parse
и другоеTryParse
.
источник
Result
;-)FileNotFoundException
.Either
OneOf
Ответы:
Это, безусловно, хорошая вещь.
Вы хотите, чтобы все, что оставляет систему в неопределенном состоянии, могло остановить ее, потому что неопределенная система может делать такие неприятные вещи, как повреждение данных, форматирование жесткого диска и отправлять президенту электронные письма с угрозами. Если вы не можете восстановить систему и вернуть ее в определенное состояние, то ответственным за это является сбой. Именно поэтому мы строим системы, которые рушатся, а не тихо разрывают себя на части. Теперь мы уверены, что нам всем нужна стабильная система, которая никогда не выходит из строя, но мы действительно хотим этого, когда система остается в определенном предсказуемом безопасном состоянии.
Это абсолютно верно, но это часто неправильно понимают. Когда они изобрели систему исключений, они боялись, что нарушат структурированное программирование. Структурное программирование поэтому у нас есть
for
,while
,until
,break
, и ,continue
когда все , что нужно, чтобы сделать все , что естьgoto
.Дейкстра научил нас тому, что неформальное использование goto (то есть прыжки вокруг, где угодно) превращает чтение кода в кошмар. Когда они дали нам систему исключений, они боялись, что они изобретают goto. Поэтому они сказали нам не «использовать это для управления потоком», надеясь, что мы поймем. К сожалению, многие из нас этого не сделали.
Как ни странно, мы не часто злоупотребляем исключениями для создания спагетти-кода, как мы привыкли к goto. Сам совет, кажется, вызвал больше проблем.
Принципиально исключения касаются отклонения предположения. Когда вы просите сохранить файл, вы предполагаете, что файл может быть и будет сохранен. Исключение, которое вы получаете, когда оно не может быть связано с тем, что имя незаконно, HD заполнен, или потому что крыса прогрызла ваш кабель для передачи данных. Вы можете обрабатывать все эти ошибки по-разному, вы можете обрабатывать их одинаково, или вы можете позволить им остановить систему. В вашем коде есть удачный путь, где ваши предположения должны выполняться. Так или иначе, исключения сбивают вас с этого счастливого пути. Строго говоря, да, это своего рода «управление потоком», но это не то, о чем вас предупреждали. Они говорили о чепухе, как это :
«Исключения должны быть исключительными». Эта маленькая тавтология родилась потому, что разработчикам систем исключений нужно время для построения трассировок стека. По сравнению с прыжками это медленно. Кушает процессорное время. Но если вы собираетесь войти в систему и остановить систему или, по крайней мере, остановить текущую интенсивную обработку, прежде чем начинать следующую, у вас есть время для уничтожения. Если люди начинают использовать исключения «для управления потоком», все эти предположения о времени все уходят в окно. Таким образом, «Исключения должны быть исключительными» действительно были даны нам в качестве оценки производительности.
Гораздо важнее, чем то, что нас не смущает. Сколько времени вам понадобилось, чтобы обнаружить бесконечный цикл в приведенном выше коде?
... это хороший совет, когда вы находитесь в кодовой базе, которая обычно не использует коды ошибок. Почему? Потому что никто не забудет сохранить возвращаемое значение и проверить ваши коды ошибок. Это все еще хорошее соглашение, когда вы находитесь в C.
Вы используете еще одно соглашение. Это хорошо, если вы устанавливаете соглашение, а не просто сражаетесь с другим. Забавно иметь два соглашения об ошибках в одной кодовой базе. Если каким-то образом вы избавились от всего кода, который использует другое соглашение, тогда продолжайте.
Мне нравится конвенция сама. Одно из лучших объяснений этого я нашел здесь * :
Но, насколько мне это нравится, я все равно не собираюсь смешивать это с другими соглашениями. Выберите один и придерживайтесь его. 1
1: Под этим я подразумеваю, не заставляйте меня думать о более чем одном соглашении одновременно.
Это действительно не так просто. Одна из фундаментальных вещей, которую вы должны понять, это то, что такое ноль.
Сколько дней осталось в мае? 0 (потому что это не май. Это уже июнь).
Исключения - это способ отклонить предположение, но они не являются единственным. Если вы используете исключения, чтобы отклонить предположение, вы покидаете счастливый путь. Но если вы выбрали значения для отправки по счастливому пути, сигнализирующему о том, что все не так просто, как предполагалось, вы можете оставаться на этом пути, пока он может справиться с этими значениями. Иногда 0 уже используется для обозначения чего-либо, поэтому вы должны найти другое значение, чтобы сопоставить свое предположение с отклонением идеи. Вы можете узнать эту идею по ее использованию в старой доброй алгебре . Монады могут помочь с этим, но это не всегда должна быть монада.
Например 2 :
Можете ли вы придумать какую-либо вескую причину, по которой это должно быть разработано так, чтобы оно преднамеренно выбрасывало что-либо? Угадайте, что вы получаете, когда нет int может быть проанализирован? Мне даже не нужно тебе говорить.
Это признак хорошего имени. Извините, но TryParse - не моя идея хорошего имени.
Мы часто избегаем исключения, когда ничего не получаем, когда ответом может быть несколько вещей одновременно, но по какой-то причине, если ответом является либо одно, либо ничто, мы зацикливаемся на том, что он дает нам одну вещь или бросок:
Действительно ли параллельные линии должны вызывать здесь исключение? Неужели так плохо, если этот список никогда не будет содержать более одного пункта?
Может быть, семантически вы просто не можете принять это. Если так, то жаль. Но, может быть, монады, которые не имеют произвольного размера, как у
List
вас, заставят вас чувствовать себя лучше.Монады - это небольшие коллекции специального назначения, предназначенные для использования определенным образом, чтобы избежать необходимости их тестирования. Мы должны найти способы справиться с ними независимо от того, что они содержат. Таким образом, счастливый путь остается простым. Если вы взломаете и протестируете каждую монаду, которую вы касаетесь, вы используете их неправильно.
Я знаю, это странно. Но это новый инструмент (ну, для нас). Так что дайте ему немного времени. Молотки имеют больше смысла, когда вы перестаете использовать их на винтах.
Если вы будете баловать меня, я хотел бы обратиться к этому комментарию:
Это абсолютно верно. Монады гораздо ближе к коллекциям, чем исключения, флаги или коды ошибок. Они делают прекрасные контейнеры для таких вещей при разумном использовании.
источник
throw
самом деле не знаю / не говорю, куда мы перейдем;)C # не Haskell, и вы должны следовать консенсусу экспертов сообщества C #. Если вместо этого вы попытаетесь следовать практикам Haskell в проекте C #, вы оттолкнете всех остальных в вашей команде, и в конечном итоге вы, вероятно, обнаружите причины, по которым сообщество C # делает вещи по-другому. Одна большая причина в том, что C # удобно не поддерживает дискриминационные объединения.
Это не общепризнанная истина. Выбор в каждом языке, который поддерживает исключения, - это либо выбросить исключение (которое вызывающий может свободно не обрабатывать), либо вернуть какое-то составное значение (которое обработчик ДОЛЖЕН обрабатывать).
Для распространения условий ошибки вверх через стек вызовов требуется условие на каждом уровне, удваивающее цикломатическую сложность этих методов и, следовательно, удваивающее число случаев модульного тестирования. В типичных бизнес-приложениях многие исключения не подлежат восстановлению и могут быть оставлены для распространения на самый высокий уровень (например, точка входа службы веб-приложения).
источник
Вы пришли в C # в интересное время. До относительно недавнего времени язык твердо стоял в пространстве императивного программирования. Использование исключений для сообщения об ошибках было определенно нормой. Использование кодов возврата страдало от недостатка языковой поддержки (например, вокруг различного объединения и сопоставления с образцом). Вполне разумно, что официальное руководство от Microsoft состояло в том, чтобы избегать кодов возврата и использовать исключения.
Однако всегда были исключения из этого в форме
TryXXX
методов, которые возвращали быboolean
результат успеха и передавали результат второго значения черезout
параметр. Они очень похожи на шаблоны «try» в функциональном программировании, за исключением того, что результат приходит через выходной параметр, а не черезMaybe<T>
возвращаемое значение.Но все меняется. Функциональное программирование становится все более популярным, и такие языки, как C #, отвечают. C # 7.0 ввел некоторые базовые возможности сопоставления с образцом для языка; C # 8 представит гораздо больше, в том числе выражение-переключатель, рекурсивные шаблоны и т. Д. Наряду с этим наблюдается рост «функциональных библиотек» для C #, таких как моя собственная библиотека Succinc <T> , которая также обеспечивает поддержку различимых объединений. ,
Объединение этих двух вещей означает, что популярность кода, подобного следующему, постепенно растет.
На данный момент это все еще довольно ниша, хотя даже в последние два года я заметил заметное изменение от общей враждебности разработчиков на C # к такому коду до растущего интереса к использованию таких методов.
Мы еще не сказали: « НЕ возвращайте коды ошибок». устарел и нуждается в уходе. Но путь к этой цели уже идет полным ходом. Итак, сделайте свой выбор: придерживайтесь старых способов создания исключений для геев, если вам нравится делать что-то; или начните исследовать новый мир, где соглашения из функциональных языков становятся все более популярными в C #.
источник
java.util.Stream
(наполовину IEnumerable-alike) и лямбды теперь, верно? :)Я думаю, что вполне разумно использовать DU для возврата ошибок, но тогда я бы сказал, что, как я написал OneOf :) Но я бы сказал, что в любом случае, так как это обычная практика в F # и т. Д. (Например, тип Result, как упоминалось другими ).
Я не думаю об ошибках, которые я обычно возвращаю, как об исключительных ситуациях, а как об обычных, но, надеюсь, редко встречающихся ситуациях, которые могут или не могут быть обработаны, но должны быть смоделированы.
Вот пример со страницы проекта.
Создание исключений для этих ошибок кажется излишним - они снижают производительность, кроме недостатков, заключающихся в том, что они не указаны явно в сигнатуре метода или обеспечивают полное сопоставление.
В какой степени OneOf идиоматичен в c #, это другой вопрос. Синтаксис действителен и достаточно интуитивно понятен. DU и Rail-ориентированное программирование являются хорошо известными концепциями.
Я бы сохранил исключения для вещей, которые указывают, что что-то сломано, где это возможно.
источник
OneOf
, чтобы сделать возвращаемое значение богаче, например, вернуть Nullable. Он заменяет исключения для ситуаций, которые не являются исключительными для начала.OneOf
примерно эквивалентно проверенным исключениям в Java. Есть некоторые отличия:OneOf
не работает с методами, производящими методы, вызываемые их побочными эффектами (так как они могут вызываться осмысленно, а результат просто игнорируется). Очевидно, что мы все должны пытаться использовать чистые функции, но это не всегда возможно.OneOf
не предоставляет информации о том, где произошла проблема. Это хорошо, так как сохраняет производительность (генерируемые исключения в Java являются дорогостоящими из-за заполнения трассировки стека, и в C # это будет то же самое) и вынуждает вас предоставлять достаточную информацию в самом элементе ошибкиOneOf
. Это также плохо, поскольку этой информации может быть недостаточно, и вам может быть трудно найти причину проблемы.OneOf
и проверенное исключение имеют одну важную «особенность»:Это предотвращает ваш страх
но, как уже говорилось, этот страх иррациональный. По сути, в вашей программе обычно есть единственное место, где вам нужно поймать все (скорее всего, вы уже используете фреймворк). Поскольку вы и ваша команда обычно проводите недели или годы, работая над программой, вы не забудете ее, не так ли?
Большим преимуществом игнорируемых исключений является то, что они могут обрабатываться там, где вы хотите их обрабатывать, а не везде в трассировке стека. Это устраняет кучу шаблонов (просто посмотрите на некоторый Java-код, объявляющий «throws ....» или обертывающий исключения), а также делает ваш код менее глючным: как любой метод может выдать, вам нужно знать об этом. К счастью, правильное действие обычно ничего не делает , т. Е. Позволяет ему пузыриться до места, где оно может быть разумно обработано.
источник
OneOf<SomeSuccess, SomeErrorInfo>
гдеSomeErrorInfo
приведены подробности ошибки.Ошибки имеют ряд измерений. Я выделяю три аспекта: можно ли их предотвратить, как часто они происходят и можно ли их восстановить. Между тем, сигнализация ошибок имеет в основном одно измерение: решение, в какой степени бить вызывающих абонентов по голове, чтобы заставить их обработать ошибку, или позволить ошибке «тихо» всплыть как исключение.
Не поддающиеся предотвращению, частые и восстанавливаемые ошибки - это те, которые действительно необходимо устранить на сайте вызова. Они лучше всего оправдывают принуждение звонящего противостоять им. В Java я делаю их проверенными исключениями, и аналогичный эффект достигается путем создания их
Try*
методов или возврата различенного объединения. Отличительные объединения особенно хороши, когда функция имеет возвращаемое значение. Это гораздо более изысканная версия возвращенияnull
. Синтаксисtry/catch
блоков невелик (слишком много скобок), поэтому альтернативы выглядят лучше. Кроме того, исключения немного медленные, потому что они записывают трассировки стека.Предотвратимые ошибки (которые не были предотвращены из-за ошибки / небрежности программиста) и невосстановимые ошибки действительно работают как обычные исключения. Редкие ошибки также работают лучше, чем обычные исключения, поскольку программист часто может их взвесить, не ожидая усилий (это зависит от цели программы).
Важным моментом является то, что часто именно сайт использования определяет, как режимы ошибок метода соответствуют этим измерениям, что следует учитывать при рассмотрении следующего.
По моему опыту, принуждение вызывающей стороны противостоять условиям ошибки прекрасно, когда ошибки не поддаются предотвращению, частым и восстанавливаемым, но становится очень раздражающим, когда вызывающая сторона вынуждена перепрыгивать через обручи, когда они не нуждаются или хотят этого . Это может быть потому, что вызывающий знает, что ошибки не произойдет, или потому, что он (пока) не хочет сделать код устойчивым. В Java это объясняет беспокойство против частого использования проверенных исключений и возврата Optional (который похож на Maybe и был недавно представлен на фоне множества споров). Если вы сомневаетесь, бросьте исключения и позвольте звонящему решить, как он хочет с ними справиться.
И, наконец, имейте в виду, что наиболее важная вещь в условиях ошибок, намного превышающая любые другие соображения, такие как то, как они сигнализируются, - это тщательно документировать их .
источник
В других ответах обсуждались исключения и коды ошибок достаточно подробно, поэтому я хотел бы добавить еще одну точку зрения, специфичную для вопроса:
OneOf это не код ошибки, это больше похоже на монаду
Как он используется,
OneOf<Value, Error1, Error2>
это контейнер, который представляет реальный результат или какое-то состояние ошибки. Это какOptional
, за исключением того, что, когда значение отсутствует, оно может предоставить более подробную информацию, почему это так.Основная проблема с кодами ошибок заключается в том, что вы забыли их проверить. Но здесь вы буквально не можете получить доступ к результату, не проверив ошибки.
Единственная проблема - когда тебя не волнует результат. Пример из документации OneOf дает:
... а затем они создают разные HTTP-ответы для каждого результата, что намного лучше, чем использование исключений. Но если вы вызываете метод, как это.
то вам действительно есть проблемы и исключение будет лучшим выбором. Сравните Rails
save
противsave!
.источник
Одна вещь, которую вы должны учитывать, это то, что код на C # обычно не является чистым. В кодовой базе, полной побочных эффектов, и с компилятором, который не заботится о том, что вы делаете с возвращаемым значением некоторой функции, ваш подход может вызвать у вас значительное горе. Например, если у вас есть метод, который удаляет файл, вы хотите убедиться, что приложение замечает, когда метод потерпел неудачу, даже если нет причин проверять любой код ошибки «на счастливом пути». Это значительное отличие от функциональных языков, которые требуют явного игнорирования возвращаемых значений, которые вы не используете. В C # возвращаемые значения всегда неявно игнорируются (единственным исключением является метод получения свойства IIRC).
Причина, по которой MSDN говорит вам «не возвращать коды ошибок», заключается именно в этом - никто не заставляет вас даже читать код ошибки. Компилятор вам совсем не поможет. Однако, если ваша функция не имеет побочных эффектов, вы можете безопасно использовать что-то вроде
Either
- дело в том, что даже если вы игнорируете результат ошибки (если есть), вы можете сделать это, только если вы не используете «правильный результат» или. Есть несколько способов, как вы можете сделать это - например, вы можете разрешить «чтение» значения только путем передачи делегата для обработки случаев успеха и ошибок, и вы можете легко объединить это с подходами, такими как железнодорожно-ориентированное программирование (это отлично работает в C #).int.TryParse
где-то посередине. Это чисто. Он всегда определяет значения двух результатов - поэтому вы знаете, что если возвращаемое значение равноfalse
, выходной параметр будет установлен в0
. Это по-прежнему не мешает вам использовать выходной параметр без проверки возвращаемого значения, но, по крайней мере, вы гарантируете результат, даже если функция завершится ошибкой.Но одна абсолютно важная вещь здесь - последовательность . Компилятор не спасет вас, если кто-то изменит эту функцию для получения побочных эффектов. Или бросать исключения. Таким образом, хотя этот подход идеально подходит для C # (и я его использую), вы должны убедиться, что все в вашей команде понимают и используют его . Многие из инвариантов, необходимых для нормальной работы функционального программирования, не применяются компилятором C #, и вам нужно убедиться, что они соблюдаются сами. Вы должны держать себя в курсе вещей, которые ломаются, если вы не следуете правилам, которые Haskell применяет по умолчанию - и об этом вы должны помнить для любых функциональных парадигм, которые вы вводите в свой код.
источник
Правильное утверждение : не используйте коды ошибок для исключений! И не используйте исключения для неисключений.
Если происходит что-то, что ваш код не может восстановить (т.е. заставить его работать), это исключение. Ваш непосредственный код не имеет ничего общего с кодом ошибки, кроме как передать его вверх для регистрации или, возможно, каким-то образом предоставить код пользователю. Однако это именно то, для чего нужны исключения - они имеют дело со всей передачей и помогают анализировать то, что на самом деле произошло.
Однако, если что-то происходит, например, неверный ввод, который вы можете обработать, либо переформатировав его автоматически, либо попросив пользователя исправить данные (и вы подумали об этом случае), это на самом деле не исключение, поскольку вы ожидайте это. Вы можете свободно обрабатывать эти случаи с помощью кодов ошибок или любой другой архитектуры. Просто не исключения.
(Обратите внимание, что я не очень разбираюсь в C #, но мой ответ должен быть в общем случае основан на концептуальном уровне того, что такое исключения.)
источник
catch
ключевое слово не существовало бы. И поскольку мы говорим в контексте C #, стоит отметить, что философия, поддерживаемая здесь, не придерживается .NET Framework; возможно, самый простой вообразимый пример «недопустимого ввода, который вы можете обработать», - это пользователь, набравший не число в поле, где ожидается число ... иint.Parse
выдает исключение.Это «ужасная идея».
Это ужасно, потому что, по крайней мере в .net, у вас нет исчерпывающего списка возможных исключений. Любой код может вызвать любое исключение. Особенно если вы выполняете ООП и можете вызывать переопределенные методы для подтипа, а не для объявленного типа
Таким образом, вам придется изменить все методы и функции на
OneOf<Exception, ReturnType>
, а затем, если вы хотите обрабатывать различные типы исключений, вам придется изучить тип иIf(exception is FileNotFoundException)
т. Д.try catch
является эквивалентомOneOf<Exception, ReturnType>
Редактировать ----
Я знаю, что вы предлагаете вернуться,
OneOf<ErrorEnum, ReturnType>
но я чувствую, что это различие без разницы.Вы комбинируете коды возврата, ожидаемые исключения и переходы по исключениям. Но общий эффект тот же.
источник
Exception
с.