Исключения, коды ошибок и дискриминационные союзы

80

Я недавно начал работу по программированию на C #, но у меня есть немного опыта в Haskell.

Но я понимаю, что C # является объектно-ориентированным языком, я не хочу вбивать круглый колышек в квадратное отверстие.

Я прочитал статью « Исключение исключений» от Microsoft, в которой говорится:

НЕ возвращайте коды ошибок.

Но будучи привыкшим к Haskell, я использовал тип данных C # OneOf, возвращая результат как «правильное» значение или ошибку (чаще всего перечисление) как «левое» значение.

Это очень похоже на соглашение Eitherв Haskell.

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

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

Но это не тот подход, который предлагает Microsoft.

Является ли использование OneOfвместо исключений для обработки «обычных» исключений (таких как «Файл не найден» и т. Д.) Разумным подходом или это ужасная практика?


Стоит отметить, что я слышал, что исключения как поток управления считаются серьезным антипаттерном , поэтому, если «исключение» - это то, что вы обычно обрабатываете без завершения программы, разве это не «путь управления»? Я понимаю, что здесь есть немного серой области.

Примечание. Я не использую OneOfтакие вещи, как «Недостаточно памяти», условия, от которых я не ожидаю восстановления, будут по-прежнему вызывать исключения. Но я чувствую, что вполне разумные проблемы, например, пользовательский ввод, который не анализируется, по сути являются «потоком управления» и, вероятно, не должны вызывать исключения.


Последующие мысли:

Из этой дискуссии я забираю следующее:

  1. Если вы ожидаете, что непосредственный вызывающий объект будет catchобрабатывать исключение большую часть времени и продолжит его работу, возможно, по другому пути, он, вероятно, должен быть частью возвращаемого типа. Optionalили OneOfможет быть полезным здесь.
  2. Если вы ожидаете, что непосредственный вызывающий абонент не будет перехватывать исключение большую часть времени, сгенерируйте исключение, чтобы избежать глупости ручной передачи его в стек.
  3. Если вы не уверены, что будет делать непосредственный абонент, возможно, предоставьте и то, Parseи другое TryParse.
Клинтон
источник
13
Используйте F # Result;-)
Астринус
8
Несколько не по теме, но обратите внимание, что подход, который вы рассматриваете, имеет общую уязвимость, которая не позволяет гарантировать, что разработчик правильно обработал ошибку. Конечно, система ввода может выступать в качестве напоминания, но вы теряете значение по умолчанию, которое приводит к разрыву программы из-за невозможности обработки ошибки, что повышает вероятность того, что вы окажетесь в каком-то неожиданном состоянии. Стоит ли напоминание о потере этого дефолта - вопрос открытый. Я склонен думать, что лучше всего написать весь ваш код, предполагая, что исключение прекратит его в какой-то момент; тогда напоминание имеет меньшее значение.
jpmc26
9
Связанная статья: Эрик Липперт о четырех «ведрах» исключений . Пример , который он дает на экзогенные eceptions показывает , что сценарии , в которых вы просто должны иметь дело с исключениями, то есть FileNotFoundException.
Сорен Д. Птюс
7
Я могу быть один в этом, но я думаю, что сбой это хорошо . Нет лучшего доказательства того, что в вашем программном обеспечении есть проблема, чем журнал с соответствующим отслеживанием. Если у вас есть команда, которая может исправить и развернуть патч за 30 минут или меньше, то появление этих уродливых друзей может быть не плохой вещью.
Т. Сар - Восстановить Монику
9
Как же ни один из ответов не проясняет , что монада не код ошибки , и ни ? Они принципиально разные, и, следовательно, вопрос, похоже, основан на недоразумении. (Хотя в измененной форме это все еще актуальный вопрос.)EitherOneOf
Конрад Рудольф

Ответы:

131

но сбой программного обеспечения вашего клиента все еще не очень хорошая вещь

Это, безусловно, хорошая вещь.

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

Я слышал, что исключения как поток управления считаются серьезным антипаттерном.

Это абсолютно верно, но это часто неправильно понимают. Когда они изобрели систему исключений, они боялись, что нарушат структурированное программирование. Структурное программирование поэтому у нас есть for, while, until, break, и , continueкогда все , что нужно, чтобы сделать все , что есть goto.

Дейкстра научил нас тому, что неформальное использование goto (то есть прыжки вокруг, где угодно) превращает чтение кода в кошмар. Когда они дали нам систему исключений, они боялись, что они изобретают goto. Поэтому они сказали нам не «использовать это для управления потоком», надеясь, что мы поймем. К сожалению, многие из нас этого не сделали.

Как ни странно, мы не часто злоупотребляем исключениями для создания спагетти-кода, как мы привыкли к goto. Сам совет, кажется, вызвал больше проблем.

Принципиально исключения касаются отклонения предположения. Когда вы просите сохранить файл, вы предполагаете, что файл может быть и будет сохранен. Исключение, которое вы получаете, когда оно не может быть связано с тем, что имя незаконно, HD заполнен, или потому что крыса прогрызла ваш кабель для передачи данных. Вы можете обрабатывать все эти ошибки по-разному, вы можете обрабатывать их одинаково, или вы можете позволить им остановить систему. В вашем коде есть удачный путь, где ваши предположения должны выполняться. Так или иначе, исключения сбивают вас с этого счастливого пути. Строго говоря, да, это своего рода «управление потоком», но это не то, о чем вас предупреждали. Они говорили о чепухе, как это :

введите описание изображения здесь

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

Гораздо важнее, чем то, что нас не смущает. Сколько времени вам понадобилось, чтобы обнаружить бесконечный цикл в приведенном выше коде?

НЕ возвращайте коды ошибок.

... это хороший совет, когда вы находитесь в кодовой базе, которая обычно не использует коды ошибок. Почему? Потому что никто не забудет сохранить возвращаемое значение и проверить ваши коды ошибок. Это все еще хорошее соглашение, когда вы находитесь в C.

OneOf

Вы используете еще одно соглашение. Это хорошо, если вы устанавливаете соглашение, а не просто сражаетесь с другим. Забавно иметь два соглашения об ошибках в одной кодовой базе. Если каким-то образом вы избавились от всего кода, который использует другое соглашение, тогда продолжайте.

Мне нравится конвенция сама. Одно из лучших объяснений этого я нашел здесь * :

введите описание изображения здесь

Но, насколько мне это нравится, я все равно не собираюсь смешивать это с другими соглашениями. Выберите один и придерживайтесь его. 1

1: Под этим я подразумеваю, не заставляйте меня думать о более чем одном соглашении одновременно.


Последующие мысли:

Из этой дискуссии я забираю следующее:

  1. Если вы ожидаете, что непосредственный вызывающий объект будет перехватывать и обрабатывать исключение большую часть времени и продолжать его работу, возможно, по другому пути, он, вероятно, должен быть частью возвращаемого типа. Необязательно или OneOf может быть полезным здесь.
  2. Если вы ожидаете, что непосредственный вызывающий абонент не будет перехватывать исключение большую часть времени, сгенерируйте исключение, чтобы избежать глупости ручной передачи его в стек.
  3. Если вы не уверены, что будет делать непосредственный абонент, возможно, предоставьте и то, и другое, например Parse и TryParse.

Это действительно не так просто. Одна из фундаментальных вещей, которую вы должны понять, это то, что такое ноль.

Сколько дней осталось в мае? 0 (потому что это не май. Это уже июнь).

Исключения - это способ отклонить предположение, но они не являются единственным. Если вы используете исключения, чтобы отклонить предположение, вы покидаете счастливый путь. Но если вы выбрали значения для отправки по счастливому пути, сигнализирующему о том, что все не так просто, как предполагалось, вы можете оставаться на этом пути, пока он может справиться с этими значениями. Иногда 0 уже используется для обозначения чего-либо, поэтому вы должны найти другое значение, чтобы сопоставить свое предположение с отклонением идеи. Вы можете узнать эту идею по ее использованию в старой доброй алгебре . Монады могут помочь с этим, но это не всегда должна быть монада.

Например 2 :

IList<int> ParseAllTheInts(String s) { ... }

Можете ли вы придумать какую-либо вескую причину, по которой это должно быть разработано так, чтобы оно преднамеренно выбрасывало что-либо? Угадайте, что вы получаете, когда нет int может быть проанализирован? Мне даже не нужно тебе говорить.

Это признак хорошего имени. Извините, но TryParse - не моя идея хорошего имени.

Мы часто избегаем исключения, когда ничего не получаем, когда ответом может быть несколько вещей одновременно, но по какой-то причине, если ответом является либо одно, либо ничто, мы зацикливаемся на том, что он дает нам одну вещь или бросок:

IList<Point> Intersection(Line a, Line b) { ... }

Действительно ли параллельные линии должны вызывать здесь исключение? Неужели так плохо, если этот список никогда не будет содержать более одного пункта?

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

Maybe<Point> Intersection(Line a, Line b) { ... }

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

Я знаю, это странно. Но это новый инструмент (ну, для нас). Так что дайте ему немного времени. Молотки имеют больше смысла, когда вы перестаете использовать их на винтах.


Если вы будете баловать меня, я хотел бы обратиться к этому комментарию:

Почему ни один из ответов не объясняет, что монада Either не является кодом ошибки, и при этом OneOf не является? Они принципиально разные, и, следовательно, вопрос, похоже, основан на недоразумении. (Хотя в измененной форме это все еще актуальный вопрос.) - Конрад Рудольф 4 июня 18 в 14:08

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

candied_orange
источник
9
Разве не было бы более уместно, если бы красная дорожка была ниже ящиков, и поэтому не проходила через них?
Флатер
4
По логике вы правы. Однако каждый блок на этой конкретной диаграмме представляет монадическую операцию связывания. bind включает (возможно, создает!) красную дорожку. Я рекомендую посмотреть полную презентацию. Это интересно, и Скотт отличный оратор.
Гусдор
7
Я думаю об исключениях больше как к инструкции COMEFROM, а не к GOTO, поскольку на throwсамом деле не знаю / не говорю, куда мы перейдем;)
Warbo
3
Нет ничего плохого в соглашениях о смешивании, если вы можете установить границы, что и является инкапсуляцией.
Роберт Харви
4
Весь первый раздел этого ответа звучит строго, как будто вы перестали читать вопрос после цитируемого предложения. Этот вопрос признает, что сбой программы лучше, чем переход к неопределенному поведению: они контрастируют во время сбоев во время выполнения не с продолжительным, неопределенным выполнением, а скорее с ошибками во время компиляции, которые мешают вам даже построить программу без учета потенциальной ошибки. и иметь дело с этим (который может закончиться крахом, если больше ничего не будет сделано, но это будет крах, потому что вы этого хотите, а не потому, что вы забыли это).
KRyan
35

C # не Haskell, и вы должны следовать консенсусу экспертов сообщества C #. Если вместо этого вы попытаетесь следовать практикам Haskell в проекте C #, вы оттолкнете всех остальных в вашей команде, и в конечном итоге вы, вероятно, обнаружите причины, по которым сообщество C # делает вещи по-другому. Одна большая причина в том, что C # удобно не поддерживает дискриминационные объединения.

Я слышал, что исключения как поток управления считаются серьезным антипаттерном,

Это не общепризнанная истина. Выбор в каждом языке, который поддерживает исключения, - это либо выбросить исключение (которое вызывающий может свободно не обрабатывать), либо вернуть какое-то составное значение (которое обработчик ДОЛЖЕН обрабатывать).

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

Кевин Клайн
источник
9
Размножение монад ничего не требует.
Basilevs
23
@Basilevs Требуется поддержка распространения монад при сохранении читабельности кода, которого нет в C #. Вы либо получаете повторные инструкции if-return, либо цепочки лямбд.
Себастьян Редл
4
@SebastianRedl лямбды выглядят хорошо в C #.
Basilevs
@Basilevs С синтаксической точки зрения да, но я подозреваю, что с точки зрения производительности они могут быть менее привлекательными.
Pharap
5
В современном C # вы, вероятно, можете частично использовать локальные функции, чтобы вернуть некоторую скорость. В любом случае, большая часть программного обеспечения для бизнеса гораздо медленнее, чем другие.
Турксарама
26

Вы пришли в C # в интересное время. До относительно недавнего времени язык твердо стоял в пространстве императивного программирования. Использование исключений для сообщения об ошибках было определенно нормой. Использование кодов возврата страдало от недостатка языковой поддержки (например, вокруг различного объединения и сопоставления с образцом). Вполне разумно, что официальное руководство от Microsoft состояло в том, чтобы избегать кодов возврата и использовать исключения.

Однако всегда были исключения из этого в форме TryXXXметодов, которые возвращали бы booleanрезультат успеха и передавали результат второго значения через outпараметр. Они очень похожи на шаблоны «try» в функциональном программировании, за исключением того, что результат приходит через выходной параметр, а не через Maybe<T>возвращаемое значение.

Но все меняется. Функциональное программирование становится все более популярным, и такие языки, как C #, отвечают. C # 7.0 ввел некоторые базовые возможности сопоставления с образцом для языка; C # 8 представит гораздо больше, в том числе выражение-переключатель, рекурсивные шаблоны и т. Д. Наряду с этим наблюдается рост «функциональных библиотек» для C #, таких как моя собственная библиотека Succinc <T> , которая также обеспечивает поддержку различимых объединений. ,

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

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

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

Мы еще не сказали: « НЕ возвращайте коды ошибок». устарел и нуждается в уходе. Но путь к этой цели уже идет полным ходом. Итак, сделайте свой выбор: придерживайтесь старых способов создания исключений для геев, если вам нравится делать что-то; или начните исследовать новый мир, где соглашения из функциональных языков становятся все более популярными в C #.

Дэвид Арно
источник
22
Вот почему я ненавижу C # :) Они продолжают добавлять способы для кожи кошки, и, конечно, старые способы никогда не уходят. В конце концов, нет никаких соглашений для чего-либо, программист-перфекционист может часами решать, какой метод «лучше», и новый программист должен изучить все, прежде чем сможет читать чужой код.
Александр Дубинский
24
@AleksandrDubinsky, вы столкнулись с основной проблемой языков программирования в целом, а не только C #. Новые языки созданы, скудны, свежи и полны современных идей. И очень немногие люди используют их. Со временем они получают пользователей и новые функции, прикрепленные к старым функциям, которые нельзя удалить, потому что это нарушит существующий код. Раздутие растет. Таким образом, люди создают новые языки, которые скудны, свежи и полны современных идей ...
Дэвид Арно
7
@AleksandrDubinsky не ненавидите, когда они добавляют новые функции из 1960-х.
Ctrl-Alt-Delor
6
@AleksandrDubinsky Да. Поскольку Java однажды пыталась добавить что- то (универсальные), они потерпели неудачу, теперь мы должны смириться с ужасным беспорядком, который возник в результате, чтобы они выучили урок и вообще прекратили добавлять что-либо
:)
3
@Agent_L: Вы знаете, что Java добавила java.util.Stream(наполовину IEnumerable-alike) и лямбды теперь, верно? :)
cHao
5

Я думаю, что вполне разумно использовать DU для возврата ошибок, но тогда я бы сказал, что, как я написал OneOf :) Но я бы сказал, что в любом случае, так как это обычная практика в F # и т. Д. (Например, тип Result, как упоминалось другими ).

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

Вот пример со страницы проекта.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

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

В какой степени OneOf идиоматичен в c #, это другой вопрос. Синтаксис действителен и достаточно интуитивно понятен. DU и Rail-ориентированное программирование являются хорошо известными концепциями.

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

mcintyre321
источник
То, что вы говорите, заключается в том OneOf, чтобы сделать возвращаемое значение богаче, например, вернуть Nullable. Он заменяет исключения для ситуаций, которые не являются исключительными для начала.
Александр Дубинский
Да - и предоставляет простой способ сделать исчерпывающее сопоставление в вызывающей стороне, что невозможно при использовании иерархии результатов на основе наследования.
mcintyre321
4

OneOfпримерно эквивалентно проверенным исключениям в Java. Есть некоторые отличия:

  • OneOfне работает с методами, производящими методы, вызываемые их побочными эффектами (так как они могут вызываться осмысленно, а результат просто игнорируется). Очевидно, что мы все должны пытаться использовать чистые функции, но это не всегда возможно.

  • OneOfне предоставляет информации о том, где произошла проблема. Это хорошо, так как сохраняет производительность (генерируемые исключения в Java являются дорогостоящими из-за заполнения трассировки стека, и в C # это будет то же самое) и вынуждает вас предоставлять достаточную информацию в самом элементе ошибки OneOf. Это также плохо, поскольку этой информации может быть недостаточно, и вам может быть трудно найти причину проблемы.

OneOf и проверенное исключение имеют одну важную «особенность»:

  • они оба заставляют вас обращаться с ними повсюду в стеке вызовов

Это предотвращает ваш страх

В C # игнорирование исключений не приводит к ошибке компиляции, а если они не перехвачены, они просто всплывают и приводят к краху вашей программы.

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

Большим преимуществом игнорируемых исключений является то, что они могут обрабатываться там, где вы хотите их обрабатывать, а не везде в трассировке стека. Это устраняет кучу шаблонов (просто посмотрите на некоторый Java-код, объявляющий «throws ....» или обертывающий исключения), а также делает ваш код менее глючным: как любой метод может выдать, вам нужно знать об этом. К счастью, правильное действие обычно ничего не делает , т. Е. Позволяет ему пузыриться до места, где оно может быть разумно обработано.

maaartinus
источник
+1 но почему OneOf не работает с побочными эффектами? (Я полагаю, это потому, что он пройдет проверку, если вы не назначите возвращаемое значение, но поскольку я не знаю ни OneOf, ни C #, я не уверен - уточнение улучшит ваш ответ.)
dcorking
1
Вы можете вернуть информацию об ошибке с помощью OneOf, сделав возвращаемый объект ошибки содержащим полезную информацию, например, OneOf<SomeSuccess, SomeErrorInfo>где SomeErrorInfoприведены подробности ошибки.
mcintyre321
@dcorking Отредактировано. Конечно, если вы проигнорировали возвращаемое значение, то ничего не знаете о том, что случилось. Если вы делаете это с чистой функцией, это нормально (вы просто потратили время).
Маартин
2
@ mcintyre321 Конечно, вы можете добавлять информацию, но вы должны делать это вручную, и это может быть лень и забывание. Я предполагаю, что в более сложных случаях трассировка стека более полезна.
Маартин
4

Является ли использование OneOf вместо исключений для обработки «обычных» исключений (таких как «Файл не найден» и т. Д.) Разумным подходом или это ужасная практика?

Стоит отметить, что я слышал, что исключения как поток управления считаются серьезным антипаттерном, поэтому, если «исключение» - это то, что вы обычно обрабатываете без завершения программы, разве это не «путь управления»?

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

Не поддающиеся предотвращению, частые и восстанавливаемые ошибки - это те, которые действительно необходимо устранить на сайте вызова. Они лучше всего оправдывают принуждение звонящего противостоять им. В Java я делаю их проверенными исключениями, и аналогичный эффект достигается путем создания их Try*методов или возврата различенного объединения. Отличительные объединения особенно хороши, когда функция имеет возвращаемое значение. Это гораздо более изысканная версия возвращения null. Синтаксис try/catchблоков невелик (слишком много скобок), поэтому альтернативы выглядят лучше. Кроме того, исключения немного медленные, потому что они записывают трассировки стека.

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

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

По моему опыту, принуждение вызывающей стороны противостоять условиям ошибки прекрасно, когда ошибки не поддаются предотвращению, частым и восстанавливаемым, но становится очень раздражающим, когда вызывающая сторона вынуждена перепрыгивать через обручи, когда они не нуждаются или хотят этого . Это может быть потому, что вызывающий знает, что ошибки не произойдет, или потому, что он (пока) не хочет сделать код устойчивым. В Java это объясняет беспокойство против частого использования проверенных исключений и возврата Optional (который похож на Maybe и был недавно представлен на фоне множества споров). Если вы сомневаетесь, бросьте исключения и позвольте звонящему решить, как он хочет с ними справиться.

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

Александр Дубинский
источник
2

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

OneOf это не код ошибки, это больше похоже на монаду

Как он используется, OneOf<Value, Error1, Error2>это контейнер, который представляет реальный результат или какое-то состояние ошибки. Это как Optional, за исключением того, что, когда значение отсутствует, оно может предоставить более подробную информацию, почему это так.

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

Единственная проблема - когда тебя не волнует результат. Пример из документации OneOf дает:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

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

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

то вам действительно есть проблемы и исключение будет лучшим выбором. Сравните Rails saveпротив save!.

Cephalopod
источник
1

Одна вещь, которую вы должны учитывать, это то, что код на C # обычно не является чистым. В кодовой базе, полной побочных эффектов, и с компилятором, который не заботится о том, что вы делаете с возвращаемым значением некоторой функции, ваш подход может вызвать у вас значительное горе. Например, если у вас есть метод, который удаляет файл, вы хотите убедиться, что приложение замечает, когда метод потерпел неудачу, даже если нет причин проверять любой код ошибки «на счастливом пути». Это значительное отличие от функциональных языков, которые требуют явного игнорирования возвращаемых значений, которые вы не используете. В C # возвращаемые значения всегда неявно игнорируются (единственным исключением является метод получения свойства IIRC).

Причина, по которой MSDN говорит вам «не возвращать коды ошибок», заключается именно в этом - никто не заставляет вас даже читать код ошибки. Компилятор вам совсем не поможет. Однако, если ваша функция не имеет побочных эффектов, вы можете безопасно использовать что-то вроде Either- дело в том, что даже если вы игнорируете результат ошибки (если есть), вы можете сделать это, только если вы не используете «правильный результат» или. Есть несколько способов, как вы можете сделать это - например, вы можете разрешить «чтение» значения только путем передачи делегата для обработки случаев успеха и ошибок, и вы можете легко объединить это с подходами, такими как железнодорожно-ориентированное программирование (это отлично работает в C #).

int.TryParseгде-то посередине. Это чисто. Он всегда определяет значения двух результатов - поэтому вы знаете, что если возвращаемое значение равно false, выходной параметр будет установлен в 0. Это по-прежнему не мешает вам использовать выходной параметр без проверки возвращаемого значения, но, по крайней мере, вы гарантируете результат, даже если функция завершится ошибкой.

Но одна абсолютно важная вещь здесь - последовательность . Компилятор не спасет вас, если кто-то изменит эту функцию для получения побочных эффектов. Или бросать исключения. Таким образом, хотя этот подход идеально подходит для C # (и я его использую), вы должны убедиться, что все в вашей команде понимают и используют его . Многие из инвариантов, необходимых для нормальной работы функционального программирования, не применяются компилятором C #, и вам нужно убедиться, что они соблюдаются сами. Вы должны держать себя в курсе вещей, которые ломаются, если вы не следуете правилам, которые Haskell применяет по умолчанию - и об этом вы должны помнить для любых функциональных парадигм, которые вы вводите в свой код.

Luaan
источник
0

Правильное утверждение : не используйте коды ошибок для исключений! И не используйте исключения для неисключений.

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

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

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

Фрэнк Хопкинс
источник
7
Я принципиально не согласен с предпосылкой этого ответа. Если бы разработчики языка не намеревались использовать исключения для исправляемых ошибок, catchключевое слово не существовало бы. И поскольку мы говорим в контексте C #, стоит отметить, что философия, поддерживаемая здесь, не придерживается .NET Framework; возможно, самый простой вообразимый пример «недопустимого ввода, который вы можете обработать», - это пользователь, набравший не число в поле, где ожидается число ... и int.Parseвыдает исключение.
Марк Амери
@MarkAmery Для int.Parse это ничего не может восстановить, ни чего-либо, что он ожидал бы. Предложение catch обычно используется, чтобы избежать полного отказа от внешнего вида, оно не будет запрашивать у пользователя новые учетные данные базы данных и т. П., Оно будет предотвращать сбой программы. Это технический сбой, а не поток управления приложением.
Фрэнк Хопкинс
3
Вопрос о том, лучше ли функции генерировать исключение при возникновении условия, зависит от того, может ли непосредственный вызывающий объект эффективно обработать это условие. В тех случаях, когда это возможно, создание исключения, которое должен поймать непосредственный вызывающий абонент, представляет дополнительную работу для автора вызывающего кода и дополнительную работу для машины, обрабатывающей его. Однако, если вызывающая сторона не будет готова обработать условие, исключения исключают необходимость в явном кодировании вызывающей стороны.
Суперкат
@Darkwing "Вы можете свободно обрабатывать эти случаи с помощью кодов ошибок или любой другой архитектуры". Да, вы можете это сделать. Однако вы не получаете никакой поддержки от .NET для этого, так как .NET CL сам использует исключения вместо кодов ошибок. Таким образом, вы не только во многих случаях вынуждены отлавливать исключения .NET и возвращать соответствующий код ошибки вместо того, чтобы допустить всплывание исключения, но и вынуждаете других в вашей команде использовать другую концепцию обработки ошибок, которую они должны использовать. параллельно с исключениями, стандартная концепция обработки ошибок.
Александр
-2

Это «ужасная идея».

Это ужасно, потому что, по крайней мере в .net, у вас нет исчерпывающего списка возможных исключений. Любой код может вызвать любое исключение. Особенно если вы выполняете ООП и можете вызывать переопределенные методы для подтипа, а не для объявленного типа

Таким образом, вам придется изменить все методы и функции на OneOf<Exception, ReturnType>, а затем, если вы хотите обрабатывать различные типы исключений, вам придется изучить тип и If(exception is FileNotFoundException)т. Д.

try catch является эквивалентом OneOf<Exception, ReturnType>

Редактировать ----

Я знаю, что вы предлагаете вернуться, OneOf<ErrorEnum, ReturnType>но я чувствую, что это различие без разницы.

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

Ewan
источник
2
«Нормальные» исключения - тоже ужасная идея. ключ к названию
Ewan
4
Я не предлагаю возвращать Exceptionс.
Клинтон
Вы предлагаете, чтобы альтернативой одному из них был поток управления через исключения
Ewan