Pull to refresh

Comments 478

Тю, а классический Null Object Pattern в шарпе не принято использовать? Просто интересно

Можно, но как в таком случае передать причину ошибки?

if (result is NullObject) {
    switch (result.GetType()) {
        case "NullBecauseNotExists": //...
        case "NullBecauseFriday": //...
        //...
    }
}
Думаю, тут лучше nameof использовать:
nameof(NullBecauseNotExists)

Дык паттерн матчинг же завезли:

switch (result)
{
    case NullBecauseNotExists nullBecauseNotExists:
        //...
}

Проблема: никакого статического контроля за этим делом нет, в отличие от обычного enum. Плюс проблема с default.

У меня нет проблемы, я так не делаю. Просто показал «более лучший» вариант свитча.
Это чтобы имя класса было не в строке, а в коде, доступное для автоматического переименования? (я ненастоящий сварщик)
Как-то вы не последовательны…
Это все ещё конвенция. Никто не мешает пользователю кода просто вызвать метод, не проверять возвращаемое значение, и начать использовать out-параметр.
Это конвенция. Люди могут ей не следовать, или даже не знать о ней.
Есть только один способ обойти нашу защиту — взять, и руками скастить результат к Success. Но это я не знаю, кем надо быть.

Я не знаю кем надо быть чтоб Task.GetAwaiter().GetResult() из UI потока дергать. Но ведь дёргают же.

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

А чтобы понять, что Maybe не гарантирует наличие данных, нужно знать значение этого слова на английском

Маленький минусик для шарпера — большой плюсище для одинэсника.

UFO just landed and posted this here
Потому что
Для каждого Сотрудник из Сотрудники Цикл
  // какой-то код
КонецЦикла;

Можно их эмулировать через алиасы:


Пусть Сотрудников = Сотрудники;
Для каждого Сотрудника из Сотрудников Цикл
  Пусть Сотрудник = Сотрудника;
  // какой-то код
КонецЦикла;

Незнакомых букв меньше:


Функция ВернутьПользователяИлиФигню(НомерПользователя)

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

UFO just landed and posted this here
Когда я решил стать программистом, я тоже не знал слова «maybe». Но к счастью, в английском они там почти все слова из Паскаля взяли, поэтому английский потом я подтянул.
UFO just landed and posted this here

Что-то в минусах исключений что-то странное написано...


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


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

Напротив, именно в этом случае они замечательно подходят.


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

Не вполне понятно какой поддержки они требуют кроме устранения дублирования...


Лучше всего понять, чем плохи исключения, можно, когда используешь чужой, плохо задокументированный код. Есть условный метод GetById, а что он станет делать, если не найдет — ну ты понятия не имеешь. Вернет null? Выбросит какое-то исключение?

На самом деле это проблема null, а не исключений.

Не, ну почему. Вот в java есть checked исключения, и они должны быть в сигнатуре, и должны быть как-то обработаны. Ну то есть, часть минусов они снимают (должны, как бы). Но на самом деле — они создают другие.

P.S. Я надеюсь тут понятно, что речь про «из любого метода может вывалиться любое исключение».

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


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


Напротив, именно в этом случае они замечательно подходят.

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


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


На самом деле это проблема null, а не исключений.
Это проблема нулл, и это проблема исключений. Метод у меня идёт в базу искать юзера, и я понимаю, что он может и не найти. Но у него в документации нет типа исключения, я что, я заворачиваю его в catch(Exception). Метод отрабатывает, у меня отвалилась база, а мой код это не обработает, он обработает сценарий с ненайденным юзером. Плохо.
А штатная ошибка — я хочу точно знать, какие штатные ошибки может отдавать метод, потому что рассчитываю обработать их на месте.

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

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

О, эта информация обычно очень нужна. Но далеко не везде.
Допустим у вас есть какой-то сервис, предоставляющий некий АПИ другим сервисам. Тогда вам не надо везде по коду ловить исключения, всё, что вам надо, это какой-то мидлвейр, или интерцептор, который уже после всей бизнеслогики будет заниматься этим и если архитектура ваших исключений продумана и разработана правильно, то он будет вполне немногословно и лаконично обрабатывать их и в т.ч. преобразовывать в корректные ответы вашего АПИ. Вам нет нужды везде по коду, где встретиться GetUser() ловить исключения, что юзера нет, как и все исключения на тему, что сеть пропала, или все инстансы SQL сервера упали и негде взять этого юзера и миллион других ситуаций. Они все обрабатываются в одном месте. А код бизнеслогики должен оставаться чистым и читаемым, без единого try/cach (ну в идеале).

Так, является ли не найденный юзер нештатной ситуацией — зависит от контекста. Что касается нейминга — в статье, в вариантах, где мы можем вернуть нулл к имени добавлено OrDefault, где отдаем по out параметру, к имени добавлено Try, где монаду — ничего не добавлено, потому что там сам тип возвращаемого значения говорит о том, что юзера мы можем и не найти. К варианту с исключением можно добавить OrThrow — хуже не будет


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

Ну в общем так и есть, так что я не вижу никаких проблем с исключениями, если их применять именно по назначению. :)

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

Это печально.
Может тогда стоило делать акцент статьи в этом направлении, что исключения только для исключительных ситуаций, а для других есть много других способов.
А то я зашел в статью решив из заголовка, что с исключениями есть какие-то проблемы требующие решения, а на деле статья о другом немного.
Это общая проблема разработки софта под названием «Don't Use Exceptions For Flow Control». От конретного языка не зависит.
Это общая проблема разработки софта под названием «Don't Use Exceptions For Flow Control»

это отлично, только разделение нештатная/штатная ситуация субъективно

Кстати, по поводу нейминга. Понятно, что у каждого есть свои методы, но расскажу о своем подходе, чтоб было понятно, чтож меня зацепило в вашем примере. Для методов, которые возвращают какую-либо сущность по ее идентификатору ситуация когда вдруг сущность не найдена это, как правило, исключительная ситуация ибо идентификатор не берется ниоткуда, это ваш-же какой-то внутренний объект, который тот, кто его использует, до этого от вас же и получил ранее. В подавляющем большинстве случаев. Поэтому для меня естественно было, что метод GetUser(int id) бросает исключение, если вдруг юзер не найден.
Мы на своём проекте договорились следовать конвенции FindUser(int id) — поиск по идентификатору, если не найден — вернуть null; GetUser(int id) — получить юзера по известному идентификатору. И во втором случае уже при неудачной попытке будет выброшено исключение.
Но ведь в вашем примере для метода GetUser(int id) отсутствие юзера с таким айди это как-раз нештатная ситуация, исключение здесь вполне логично и оправданно.

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

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

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


Аутентификатор на следующем действии сотрудника пойдет проверять его права и получит обратно «User Not Found». Абсолютно штатно.

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

Я специально попросил обойти тему блокировки вместо удаления: пример синтетический, это может быть не пользователь, а ресурс, свойство, единица товара в корзине. Единицы товара в корзине можно удалять? Да, EventLog / EventSource хранилища примерно всем лучше реляционных БД, но они, к сожалению, не всегда применимы и гораздо сложнее архитектурно.


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

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

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

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

Какое окно? Какой API? При чем тут веб вообще?


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


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


Поэтому эта штатная ситуация (запрос отсутствующей сущности) должна быть обработана штатными методами. Но есть, конечно, и горе-разработчики, которые все пробрасывают наверх, и клиенту такого поделия приходится городить стейтфул запросы к якобы «рестфул» сервису, чтобы знать, к чему вообще относится этот «Error: Not found», что приобретает особенный аромат, когда ответы асинхронные (например, в частном случае простого HTTP — 202).


А теперь представьте себе, что HTTP нет, а есть брокер сообщений какой-нибудь. Который из очереди выгребает сообщение с невалидным JWT, например. Неважно, нет ли такого пользователя, или сессия протухла, или крысы кабель перегрызли. Если он бросит исключение, то его будет обрабатывать кто-то, кто понятия не имеет о природе проблемы. SRP сразу идет лесом, а за ним, скорее всего, и DRY. Зато появляется God Object, который знает всё про все возможные проблемы.


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

Поэтому эта штатная ситуация (запрос отсутствующей сущности) должна быть обработана штатными методами.

Вы ходите по кругу. С фига ли эта ситуация штатная-то?


А теперь представьте себе, что HTTP нет, а есть брокер сообщений какой-нибудь. Который из очереди выгребает сообщение с невалидным JWT, например.

Мы точно всё ещё метод GetUser(int id) обсуждаем?


Метод GetUser должен бросить исключение. А вот тот метод, который разбирает JWT, должен его перехватить и понять что токен-то невалиден, и именно такой ответ ("ошибка: токен не валиден") должен быть послан в ответ на запрос. Причина невалидности токена никому кроме техподдержки не важна, клиент её всё равно узнает когда попытается получить новый токен.

Вы ходите по кругу. С фига ли эта ситуация штатная-то?

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


Метод GetUser должен бросить исключение.

Обожаю взвешенные, аргументированные, доказуемые утверждения. Жаль, что конкретно это к их числу не относится. Исключения даже семантически намекают, что относятся к исключительным случаям. Протухший токен — случай не исключительный. Предложение выбросить эксепшн и тут же его перехватить — это предложение использовать goto там, где можно обойтись последовательным flow. Даже вернуть null — лучше. Вообще, если исключение ловится и обрабатывается непосредственно в вызывающей функции, без размотки стека — в 102% случаев означает, что исключение выбрано в качестве контроля управления ошибочно.


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


А вот «в базе нет таблицы users», например — ситуация нештатная. Вот тут действительно нужно бросить исключение.

Протухший токен — случай не исключительный

Обожаю взвешенные, аргументированные, доказуемые утверждения.


Предложение выбросить эксепшн и тут же его перехватить

А где вы увидели "тут же"? Вы когда научитесь читать что вам пишут?

А где вы увидели «тут же»?

Метод GetUser должен бросить исключение. А вот тот метод, который разбирает JWT, должен его перехватить [...]

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


Или между разбором JWT и вызовом GetUser есть еще пять middleware? — Ну тогда это никакими аргументами не вылечить, только лоботомией.

Ну так у вас два метода, один (GetUser) кидает исключение. другой (разбирающий JWT) — перехватывает. Где тут "тут же"? И где тут "без размотки стека"?


Или между разбором JWT и вызовом GetUser есть еще пять middleware?

Да, такой вариант допустим.


Ну тогда это никакими аргументами не вылечить, только лоботомией.

Обожаю взвешенные, аргументированные, доказуемые утверждения.

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

А вот с этим согласен на все 100%
Таймаут — ситуация тоже штатная, кстати. Если бросать эксепшн на каждый таймаут обращения к базе, ничем не обоснованные дорогие прыжки по стеку услужливо помогут положить сервис мизерной DOS-атакой.

Вы еще скажите, что отмена задачи — ситуация штатная, и метод CancellationToken. ThrowIfCancellationRequested следует запретить (в т.ч. в системной библиотеке!)


Ну не настолько исключения дорогие в C# же.

Вы правы. ThrowIfCancellationRequested лучше не использовать, если это возможно, по той же самой причине что и Thread.Abort(). В документации даже есть комментарий тут.

К слову, в своём проекте я действительно не использую исключения. Любой метод возвращает AsyncResult<T>, где статусом выполнения является Success, Canceled, Timeout, Error. А исключения используются там, где им место — для обработки нештатных ситуаций, не обработанных в коде.

По той же причине? Кажется, вы не в курсе почему вообще Thread.Abort объявили устаревшим...

Да от куда мне знать, человек я темный. Я имел ввиду вот этот комментарий:
The Thread.Abort method should be used with caution. Particularly when you call it to abort a thread other than the current thread, you do not know what code has executed or failed to execute when the ThreadAbortException is thrown. You also cannot be certain of the state of your application or any application and user state that it's responsible for preserving. For example, calling Thread.Abort may prevent the execution of static constructors or the release of unmanaged resources.
Используя _ThrowIfCancellationRequested _ вы получите непредсказуемое состояние — какой код у вас отработал, а какой нет? Но это не единственная проблема. При использовании _ThrowIfCancellationRequested _ вы затратите в 100 раз больше времени, чем с _IsCancellationRequested_.

Уточнение: это вы получите непредсказуемое состояние. А я получу предсказуемое, потому что я не забываю про using и finally.

Все так просто? А что если это не OperationCanceledException, а OutOfMemoryException или иное исключение? Вы их будете обрабатывать одинаково? Что вы будете делать с вызовами в библиотеки? Они обработают OperationCanceledException? На сколько корректно они это сделают?

Что-то я совсем перестал вас понимать. Как ThrowIfCancellationRequested может выкинуть OutOfMemoryException? Зачем библиотекам обрабатывать OperationCanceledException, если их задача — его выкинуть, а обрабатывать буду я?

Уточнение: это вы получите непредсказуемое состояние. А я получу предсказуемое, потому что я не забываю про using и finally

Что-то я совсем перестал вас понимать. Как ThrowIfCancellationRequested может выкинуть OutOfMemoryException?

Вы предполагаете поймать в using и try/finally только OperationCanceledException?

Зачем библиотекам обрабатывать OperationCanceledException, если их задача — его выкинуть

Используя _ThrowIfCancellationRequested _ вы получите непредсказуемое состояние

Я предлагаю поймать то, что ловится и обработать если получится. Конкретно OperationCanceledException ловится и успешно обрабатывается.

UFO just landed and posted this here

Что значит — "пусть"? Это уже совершенно другой пример.


Да, я не буду делать методы получения пользователя по id и по email одинаковым образом.

UFO just landed and posted this here
Есть такое хорошее мнение, с которым я согласен, что исключения — они для тех ситуаций, которые могли быть предотвращены при кодировании прямыми проверками или более мощной системой типов.

А я — не согласен.


Как на языках без исключений писать GetUser(int id)?

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


А вот для FindUser(string email) я выберу Maybe (на языке без исключений — Either<IoError, Maybe<User>>)

А теперь представьте себе, что HTTP нет, а есть брокер сообщений какой-нибудь.

А транспорт не важен в данном случае. Вы же не просто сообщения в очередь кидаете, а для того, чтоб кто-то их прочитал. То-есть, посылаете их в виде понятном читателю, или иными словами, предоставляете некое АПИ. И последним бастионом вашего АПИ будет try/catch, ибо вы же не хотите засирать эксепшенами очередь, а если вы работаете с базой, и/или еще чем-нибудь, то помимо not found вам может упасть что угодно, начиная от internal error до даже, прости господи, timeout, и надо их переделать в формат вашего АПИ.
Однако. Если в случае когда искомый объект не найден по айди у вас действительно есть какая-то дополнительная логика помимо сообщения об ошибке, то я бы рекомендовал не использовать GetUser(), ибо он, зараза, эксепшенами плюется, а использовать вместо него что-нибудь типа MaybeGetUser(), GetUserOrDefault(), GetUserIfExists(), или что-нибудь подобное.
Это не несогласованность данных.

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

Разумеется, если какой-то объект был получен одним потоком и собирается его как-то использовать, а в это время другой поток, пока первый собирается, этот объект уже удалил, это не несогласованность данных. Я просто с какого-то перепуга не правильно понял сообщение: «нельзя гарантировать, что данные по указанному id там есть на момент вызова, даже если мы его получили секунду назад из этой же базы» решил, что одна и та-же база создает какой-то объект и возвращает его одному потоку, а через секунду другому говорит, что такого не знаю, хотя в базе он на самом деле есть. Это да, какой-то косяк в голове был, не понял собеседника, напридумывал фигни.

Я к тому, что отсутствие в базе строки с указанным id это не нештатная ситуация, а самая что ни на есть обычная. Либо объект из базы удален только что, либо пользователь неправильно ввел, либо еще что-нибудь, что часто случается при взаимодействии 2 разных систем. Поэтому исключения для них применять нелогично. Null это по определению отсутствие значения, его для этого и придумали. А вот вызывающий код уже может бросить исключение, если ему так хочется.

UFO just landed and posted this here
Она будет нештатная для метода GetUser(int id) ибо название метода подразумевает, что он возвращает юзера, если метод должен возвращать что-то еще, какие-то состояния, то и называться должен по-другому. Например, как сделано в том-же linq: First() — плюет эксепшеном если элемент не найден, FirstOrDefault() — не плюет эксепшенами.
Для метода, которому понадобился юзер, чтоб совершить над ними какие-то действия, в большинстве случаев отсутствие юзера также будет нештатной ситуацией: нет юзера — нет действий, возвращаем ошибку.
И эксепшен в данном случае будет удобнее, ибо обработать его можно непосредственно перед формированием ответа АПИ, а не везде каждый раз перепроверять по цепочке вызовов чтож там такое случилось.

Нет, отсутствие юзера в хранилище данных это штатная ситуация. Потому что хранилище данных это сторонняя система. Так же как и GetUser(string email), нельзя из приложения гарантировать, что в базе есть пользователь с таким email. Ладно id обычно из выпадающщего списка берется, и еще можно обсудить, насколько неправильный id исключительная ситуация, а email вводится вручную.


Название метода подразумевает, что он получает откуда-то полные данные по неполным. Почему отсутствие данных в этом "откуда-то" это исключительная ситуация? Отсутствие данных надо обозначать значением отсутствия данных, это и есть null. Если в C# нельзя выразить значение User|null в сигнатуре метода, значит это проблема недостаточной системы типов в C#. А вот если мы считаем, что пользователь обязательно должен существовать, для этого и надо делать специальные методы, где это отражено в названии — GetExistingUser(), он пусть и кидается исключениями.

Название метода подразумевает, что он получает откуда-то полные данные по неполным. Почему отсутствие данных в этом «откуда-то» это исключительная ситуация?

Вообще, не обязательно. Достаточно популярный «негласный контракт» в библиотеках C# предполагает, что методы а-ля GetSomething(id) предполагают наличие соответствующего объекта, и его отсутствие — ситуация нештатная. Методы FindSomething(id) предполагают, что объекта может не быть, и могут возвращать null.
Это, как по мне, вполне логично, и бесплатно добавляет в приложение ещё один уровень самоконтроля.

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

Если контракт функции подразумевает, что она должна возвращать пользователя по его Id, то какой-нибудь UserNotFoundException там вполне логичен и уместен.
Есть принципиальная разница между задачами «найти пользователя по его айди» и «получить атрибуты пользователя по его айди», и для второго случая невалидный айди — вполне себе нештатная ситуация.

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

Контракт это гарантия.

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

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

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


значит, неверный Id — это недопустимое значение параметров

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


Это как раз и есть редкая нештатная ситуация.

Давайте я процитирую утверждение из начала этой ветки.
"А штатная ошибка — я хочу точно знать, какие штатные ошибки может отдавать метод, потому что рассчитываю обработать их на месте".


Если это редкая нештатная ситуация, тогда на месте обрабатывать ее не надо. Надо упасть с записью в лог и отдачей Server error, а например никакой не 404 Not found.


"методы GetSomething(id) предполагают наличие соответствующего объекта, методы FindSomething(id) предполагают, что объекта может не быть, и могут возвращать null" — с таким соглашением я согласен, это удобно. Но это именно соглашение. А изначально по контексту говорилось про некий метод получения объекта из БД, имя которого условно и неважно, а разговор был про его логичное поведение. Логичное поведение метода получения объекта в случае отсутствия объекта — это вернуть признак отстутсвия объекта, а не менять поток выполнения.

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

Да можем, почему нет? Что хранится в базе данных, ответственность не её, но как она на это будет реагировать, целиком и полностью её ответственность.
А изначально по контексту говорилось про некий метод получения объекта из БД, имя которого условно и неважно, а разговор был про его логичное поведение.

Похоже, мы немного по-разному понимаем обсуждаемый вопрос. Как по мне, тема была более общей, есть ли смысл возвращать исключение, если объект не найден? Ну и в качестве примера взяли условный GetUser(Id). Я веду к тому, что это зависит от контракта и контракт, в котором GetЧего-то-там возвращает исключение, вполне уместный.
тема была более общей, есть ли смысл возвращать исключение, если объект не найден?

Ну да, я про то же. Про общие вопросы, а не конкретное соглашение.


Я веду к тому, что это зависит от контракта и контракт, в котором GetЧего-то-там возвращает исключение, вполне уместный.

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

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

Да я просто не считаю, что возвращать признак отсутствия объекта — это более логично. По одной простой причине: это всегда обязывает вызывающую сторону предпринимать какие-то завершающие действия. Исключения вам дают выбор: если у вас выход из такой ситуации штатно предусмотрен, вы его делаете. Если не предусмотрен, вы предсказуемо крашитесь. А признак отсутствия выбора вам не даёт. Будьте добры, обрабатывайте всегда, иначе оно отвалится с NullReferenceException.

А тут мы как раз приходим к выразительности системы типов языка. В моем понимании правильно, когда переменная, объявленная как User u, не может быть null.

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

Зависит от ЯП. Если это язык со статической типизацией, то информацию о семантике можно понять не только по имени функции, то и по типу возвращаемого значения.

Это не про шарп, но все же. Можно использовать Either<Exception, User>, где Either содержит только одно значение. Значение получается так: either.when(left: обрабатываем, right: результат)

Ну это и есть Result, про который говорит автор.
Про то и говорится, что автор зачем то переизобрел Either, приплетя сюда Maybe.
При том, что Maybe — самодостаточная монада и не нуждается ни в каком наследовании, а Either такая же самодостаточная монада, никак не связанная с Maybe.
Я уже молчу, что в контексте C# Maybe должна быть структурой, а не классом.

PS. Случайно минуснул коммент, а отменить нельзя
В нашей кодовой базе Option<> это структура, и мы с неё съезжаем на nullable references например, как раз из-за идиоматичности =)
Если идеоматичность поддерживается во всей кодовой базе — вполне норм.
Но если это библиотека для стороннего пользователя (или общая библиотека для нескольких сервисов) — то nullable reference types не подходит, потому что сторонние пользователи:
— могут забить на идеоматичность.
— библиотека может (и должна, если в ней нет чего то прям такого, что требует netstandard2.1) поддерживать старую версию фреймворка без поддержки nullable reference types.
Вот тут то и нужна Maybe/Optional именно как структура.

Я больше про контекст статьи, изобретать Either поверх Maybe — странное решение, когда все уже давно придумано, и это отдельные, не зависящие друг от друга монады
Про библиотеки для сторонних пользователей я согласен, это основная боль когда что-то делается такое, что нужно отдать «на сторону» (тот же опенсорс) и линковать с нашими внутренними библиотеками нельзя.

Для nullable reference types именно поддержка фреймворка не особо нужна. В netstandard2.0 нет разметки BCL, но это решается multitargeted билдом. У нас таргет для библиотек netstandard2.0, второй таргет netcoreapp3.1, а финальное приложение net472. Итого warnings/errors по поводу nullability мы видим от netcoreapp3.1, но всё что мы собираем отлично запускается под .NET 4.8 =)

Про то что Either и Maybe это разные вещи, я согласен.

Я ничего не изобретал, просто рассказал про концепт. На рынке есть библиотеки и для Maybe/Option, и для Either/Result. И на хабре про них нередко пишут материалы. Моя задача была — концептуально сравнить монадический подход с другими, которые используются в шарпе.

Окей, я вас понял.
Но нужно было явно указать, что:
— есть такая монада Either и объяснить ее назначение.
— не стоило Either делать на основе Maybe.
В целом, как сравнение подходов — статья неплохая, но, как всегда, есть нюанс. Опытные разработчики и так знают про монады, nullable reference types, try pattern и т.д.
А начинающие разработчики, на которых и ориентирована этат статья, узнают про Maybe (что очень хорошо), но не узнают про Either (плохо, но не очень — узнают позже). А еще увидят вашу реализацию с Result поверх Maybe (и начнут использовать в своем коде)- а вот так уже делать не стоит. Монады используют в «чистом» виде. Расширять их extension'ами — пожалуйста, но не наследоваться от них

К сожалению, это не эквивалентные вещи, когда null может быть вполне себе валидным значением.

никак не связанная с Maybe

Maybe<T> – это частный случай Either<E, T>, при котором E – unit-тип. Монадическое поведение у них абсолютно одинаковое.


не нуждается ни в каком наследовании

Наследование – лишь деталь реализации этой штуки на C#, не имеющем (пока) алгебраических типов. Нормальный подход, используется с вариациями в Scala (case-классы) и Kotlin (sealed-классы).

Maybe<T> – это частный случай Either<E, T>, при котором E – unit-тип. Монадическое поведение у них абсолютно одинаковое.

Структурно — да. А вот семантика там несколько отличается.

В случае Maybe, значение None означает отсутствие значения. В случае "стандартной интерпретации" Either, значение Left () означает ошибку без дополнительной информации (ну а вне интерпретаций у Either вообще нет семантики).

None точно так же может означать ошибку без дополнительной информации. Это вопрос конвенций, а не какая-то принципиальная семантическая разница.

В том то и суть, если мы вызываем некий метод
Maybe<User> FindUser(string id)
то метод либо возвратит нам пользователя, либо не возвратит (и только), причем результат выполнения будет предельно однозначным.
Считать это ошибкой или нет — дело того кода, который вызывает этот метод.
Инфраструктурные ошибки, типа отвалившейся БД — тут не в счет, дело только в логике метода
Считать это ошибкой или нет — дело того кода, который вызывает этот метод.
Так я про то же и говорю:
Это вопрос конвенций, а не какая-то принципиальная семантическая разница.
Наверное, мы друг друга недопоняли.
В статье обсуждаются способы прокидывания детализированной ошибки из метода в вызывающий код. Maybe для этого не предназначен, а Either — очень даже.
И учитывать вызывающий код в проблематике статьи — явно не стоит. Потому что можно проигнорировать как Maybe.None, так и Either.Left.
Устные конвенции и договоренности — такая себе штука, рано или поздно кто то ее нарушит, а компилятор такое отлавливать сейчас не может.
компилятор такое отлавливать сейчас не может

В Idris и Agda очень даже может, а в шарпе, джаве и хаскеле не сможет [без костылей] никогда.

UFO just landed and posted this here

Разве что в том смысле, что если мы не смогли получить значение из-за ошибки — то у нас его нет. Но обратное неверно: если у нас нет значения — это не значит что произошла какая-то ошибка!

(Either ()) так вообще естественно изоморфен Maybe в Hask. Но кого это волнует. Математика ведь не нужна… :-).
Впрочем, понесло меня куда-то не туда. Естественное преобразование, по ходу, не про это. Извините.

Давайте ещё раз: TimurNes заявляет, что Maybe и Either никак не связаны, я же пытаюсь показать эту (имхо, очевидную) связь.

> Когда метод потенциально выплевывает пять шесть типов исключений, код превращается в нечитаемое говно — и код метода, и код использования.

Наследуем эти 5-6 типов исключений от одного базового типа. Кому надо — обрабатывает нужные типы по разному. Кому не надо — обрабатывает один базовый тип.
Основная проблема с исключениями в том, что сигнатура метода класса (или ещё хуже — метода интерфейса) ничего не сообщает о том, какие вообще исключения могут быть. Документация этого не решает, плюс если это всё-таки интерфейс, то каждая новая его реализация потенциально эродирует контракт добавлением новых, ранее неизвестных «науке» исключений.
Исключения действительно хорошо подходят только для неожиданных, исключительных результатов исполнения. Эти исключения обычно нет никакого смысла обрабатывать, за исключением catch/log/rethrow, но во-первых это всё-таки cross-cutting concern, а если даже не получается, то всё равно влияния на поток управления никакого.
UFO just landed and posted this here

Потому что даже на jvm они не прижились и в альтернативах Java вроде Kotlin и Scala их нет. Причина проста — слишком часто исключение НА САМОМ ДЕЛЕ ловить не нужно. Потому что оно гарантированно не случится (например, парсинг константы), либо если оно случится, то логика программы не подразумевает ничего, кроме как вывести красивое сообщение об ошибке в uncaught exception handler. Условно, если у вас в вебприложении отвалился коннект к бд, то вам обычно остаётся только развести руками и выплюнуть 500 ошибку. С этим отлично справляется try… catch (Throwable) где-то в дебрях фреймворка. Ситуаций, когда исключение требует особой обработки не очень много и они заранее известны. Ещё checked исключения несовместимы с функциональным стилем программирования, который сейчас очень популярен — сигнатуры лябмд для обратных вызовов не содержат исключений (иначе их придётся добавлять во все map, reduce, filter и т. д. и это заразит весь код). Как следствие всего этого, большинство исключений либо оборачиваются в RuntimeException, либо просто подавляются. Первое путает код (потому что теперь нельзя так просто понять, что же случилось в обработчике верхнего уровня), второе часто гораздо опаснее непойманного исключения, если применено не к делу. И параллельно с этим код распухает и теряет читабельность (что также повышает вероятность ошибок).


Таким образом несмотря на красивую теорию, на практике checked exception скорее приведут к увеличению количества ошибок, чем к их уменьшению.

UFO just landed and posted this here

В случае с функциями обратного вызова для функциональных примитивов вроде map, filter, reduce и т. д. добавить сигнатуру в метод нельзя.


Ну и как я уже сказал, проблема в том, что необходимость обработки исключения очень контекстнозависима. Взять даже ошибку парсинга числа. Если это пользовательский ввод, то, вероятно, мы хотим это обработать особо. Если это конфиг приложения, то мы скорее всего хотим фатально упасть, потому что больше делать нечего. Если это парсинг константы (например, для BigInteger), то мы уверены, что не упадём. В случае с тем же findUserById нам совершенно нет нужды ловить ошибки в SQL, например. Если они будут, это критический баг в программе и ничего сделать программно уже нельзя.


По сути дела всё зависит от того, есть ли в ТЗ указания, что делать при какой ошибке. А тз у каждого проекта своё

UFO just landed and posted this here

Так на что uncaught exception handler (либо try… Catch на корневой класс исключений где-то высоко в коде)? Как раз в общем виде залоггировать исключение и нарисовать красивую ошибку пользователю, если мы всё равно не знаем, что с этим делать.

UFO just landed and posted this here
То есть если вы например работатете с async/await или с COM'om/unmanaged code.

Для асинков есть TaskScheduler.UnobservedTaskException, в случае с COM/unmanaged там что угодно может быть, это правда. но там ни один из подходов не будет достаточно хорош
UFO just landed and posted this here
Но на мой взгляд многое было бы проще если в С# добавят пару фич для работы с исключениями. Даже если их добавят как опциональные.

Всё в ваших руках github.com/dotnet/csharplang
UFO just landed and posted this here
В случае с функциями обратного вызова для функциональных примитивов вроде map, filter, reduce и т. д. добавить сигнатуру в метод нельзя.

Так это проблема конкретно жавы, а не checked exceptions как таковых.

Если бы checked exceptions реально попадали бы в сигнатуру и обрабатывались бы дженериками, я бы тоже за них ратовал:


IEnumerable<U> Select<T, U, E>(this IEnumerable<T> source, FallibleFunc<T, U, E> selector) throws E {...}

Но в итоге это практически эквивалентно предлагаемому в статье решению, мы даже можем оставить обычную сигнатуру LINQ:


IEnumerable<U> Select<T, U>(this IEnumerable<T> source, Func<T, U> selector) {...}

Просто принимаем тип U за Result<RealU, E>.

Неа, не эквивалентно. Потому что первый Select возвращает "эквивалент" Result<IEnumerable<U>, E>, а второй IEnumerable<Result<RealU, E>>.


Правда, в реальности у вас throws E будет не у Select, а у IEnumerable<U> (ленивость же!), так что сигнатуры станут более похожими — но поведение всё равно останется разным: первый вариант останавливается при первом же исключении, в то время как второй всегда проходит до конца.

Потому что оно гарантированно не случится (например, парсинг константы)

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


checked исключения несовместимы с функциональным стилем программирования

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


public <E extends Exception> void doSomething(String s) throws E {
  ...
}

В дополнение хочется вариабельных списков (как в темплейтах в C++), чтобы можно было перечислять их, если нужно несколько исключений, или указывать список нулевой длины, когда они не нужны, но такого в джаве нет. Тогда можно было бы починить имеющиеся map/reduce/filter/etc.

Достаточно было бы добавить автовывод типов исключений. Сделали же автовывод типов переменных, не сломались.

Слишком сложно. Тот же OutOfMemoryException или StackOverflowException может вылезти где удобно. Я пробовал ставить плагин, который шерстит по функции и собирает все возможные исключения — их получалось просто адовое количество.

Думаю такие, которые возможны в любом методе, можно и опустить.

К слову, достаточно ведь очевидная штука. Не могу понять, почему так не сделано

UFO just landed and posted this here
Идея простая. Есть отдельная сборка, в ней лежит абстрактный класс Maybe, у него два наследника, Success и Failure. Отдельная сборка и интёрнал конструктор нужны, чтобы гарантировать — наследников всегда будет только два.

А теперь возвращает вместо Maybe "горячо любимый" null, и...

UFO just landed and posted this here
А можно вспомнить опыт Golang и возвращать 2 значения, как именно — зависит от языка, но можно…
1 значение — success flow, 2 значение — Error

Бонусом: упростятся программы, исключения из недр фреймворков весом в тонну не будут летать десятки исключений и программа не будет обрастать списками отлавливаний
public (User user, Error error) GetUserById(int id)
{
    //...
    return (user, error);
}

Но кажется это лишь в некоторых случаях действительно органично подходит.

Нет уж, спасибо, if err != nil { return nil, err } — это то, что я бы не хотел видеть в коде вообще, ни в своём, ни в чужом.

да, посчитал строки в нескольких больших проектах с github.com/trending/go
получилось, что эта конструкция повторяется раз в 100-300 строк. ещё примерно так же часто ошибка декорируется.
через год обещают выкатить генерики в го, увидим, станет ли народ заворачивать ошибки в монады.
через год обещают выкатить генерики в го, увидим, станет ли народ заворачивать ошибки в монады.

Пытаться будет, но смысла нет без сумм-типов, а их введут неизвестно когда.

Технически это описанный в статье try pattern, только значение возвращается из функции как обычно вместо ref-параметра. Но вообще возвращать из функции значение И ошибку — костыль за невозможностью вернуть значение ИЛИ ошибку.
Про if err != nil (и возможность про err вообще забыть или забить) тут уже сказали.

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

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

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

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

С исключениями проблема в том, что зачастую их используют для сигнала об ожидаемом ошибочном поведении. Я скажу вероятно кощунственную вещь, но выбрасывать FileNotFoundException при отсутствии файла, который мы пытаемся открыть это ужасный дизайн. Особенно если потом там есть ещё DirectoryNotFoundException, PathTooLongException и т.д.
И всё это вместо Result<Stream, FileOpenError> Open(string fileName).
Тут конечно же есть вопрос удобства/идиоматичности работы с Result<TOk,TErr>, но тут придётся идти на компромисс, начиная от Unwrap, который-таки выбросит исключение (который можно замаскировать под explicit или даже implicit conversion operator к TOk), через map-методы типа And/Or и заканчивая реализацией через pattern matching (но тут value-типом не обойтись, придётся или боксить или на хипе выделять, это грустно если производительность не на последнем месте).

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

Файл может быть удалён между проверкой и открытием. атак что проверка от обработки исключений не защищает.

Файл не только может быть удалён между проверкой и открытием, как написали выше, но еще и может быть недоступен по правам, заблокирован другим процессом, и так далее.
Конечный итог один — во многих случаях мы можем понять, что что-то не так, только когда попытаемся выполнить операцию.
И тогда встаёт вопрос: а нужно ли городить отдельную методологию для обработки «ожидаемых» ошибок, когда есть неожиданные?
Ну собственно если в коде проверяется, что файл есть и доступен, а потом в момент открытия это неожиданно стало не так, то вот и ИСКЛЮЧИТЕЛЬНАЯ ситуация. Зачем ее обрабатывать как-то особенно, всё равно ничего не сделать, кроме как попытаться опять, и потом уже кидать ошибку наверх. Можно сразу кидать ошибку, пусть наверху разбираются с повтором.

Проблема тут в том, что знание о том, какие именно проверки необходимы и достаточны, инкапсулированы в функции, открывающей файл и ожидающий определённого состояния этого файла, но этой информации нет в сигнатуре этой функции. Поэтому чтобы написать проверки до вызова этой функции, нужно вначале как-то узнать, какие именно проверки нужны, чтобы не упустить ни одной. А стремлении сделать все проверки заранее приходим к эдаким pre-checked exceptions: вместо полного покрытия post-conditions — полное покрытие pre-conditions. Вот только теперь компилятор никак за этим не следит, и асинхронность реального мира всё равно требует проверять на исключительные ситуации, потому что заглянуть в будущее принципиально невозможно. Ну и happy path теперь обвешан проверками с обоих сторон — и перед, и после вызова.

Но ведь это же действительно исключительные ситуации, если вы пытаетесь открыть на чтение несуществующий файл. Зачем городить огород с Result<TOk,TErr>, когда достаточно перед открытием файла проверить, что он действительно существует? Будет сделано примерно то-же самое, что пытаться открыть, а потом уже проверять, открылось, или нет, но код будет более читаемый, в таком случае.

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


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

достаточно перед открытием файла проверить, что он действительно существует

Если проверили файл перед открытием, а потом между проверкой и самим открытием случилось удаление файла, то будут проблемы)
В этом случае нужно именно атомарное открытие с одновременной проверкой.

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

FileNotFoundException,… DirectoryNotFoundException, PathTooLongException и т.д.
И всё это вместо Result<Stream, FileOpenError> Open(string fileName).

Так все те эксепшены — это и есть ваш FileOpenError (или IOException, как это на самом деле назвали). Семантически одно и то же, просто синтаксически выглядит по-другому.

C#:


var file = File.Open("config.json");

Rust (в котором исключений нет):


let mut file = File::open("config.json")?;

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

А она там есть :) В Расте паники работают по тому же принципу, что и исключения, но они то точно используются для совсем уж плохих ситуаций.
А тут это просто монадический подход с сахаром, делающий его похожим на исключения, но этот подход производительнее и обладает плюсами от checked exceptions.

Ну это же будет работать только на одном уровне. То есть он через 5 вызовов это не прокинет, если по стеку выше нет аналогичного заворота в Error<> и такого же типа.
UFO just landed and posted this here
Исключения хороши тем, что а) их невозможно случайно проигнорировать

Наоборот же, их легко проигнорировать. Или что значит «случайно проигнорировать»?

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

Ну в таком ключе — да. Но таким же свойством и монадический подход обладает (придется делать unwrap чтобы достать результат без проверки)

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


Полностью этим свойством обладает полноценный и ленивый монадический подход. Полноценный — это без всяких unwrap, а с операцией bind. Ленивый — чтобы без проверки результата операция даже не запускалась.

Хм. А где собственно решение проблемы? Все эти способы известны и применяются.
Что-то мне это напоминает, эта «наивысшая надёжность»… Подумал и вспомнил.
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Это же Rust'овский метод работы с возвращаемыми значениями!
да, и именно про такие перечисления упоминает автор.
Это же Rust'овский метод работы с возвращаемыми значениями!

Вот только подобные методы работы с ошибками были ещё в Standard ML в аж 1984 году.

Можно имитировать АДТ без наследования, используя, например, методы типа And(Func). Но в целом nullable reference types более идиоматичны чем Maybe<>. Замены Either<>(Result) идиоматичной нет и скорее всего не будет, пока не будет АДТ. Tuple с двумя nullable references это беспомощно, потому что у него не два состояния, а 4, то есть правило про irrepresentable illegal state не работает.
С другой стороны, подход Марка Зимана с универсальными абстракциями показывает, что если немного отвлечься от идиоматичности, можно очень неплохо приобрести в композируемости, легче соблюдать open/closed principle.
Мой вариант.

public class BaseResultTwo<T> where T : class
{
    public BaseResultTwo()
    {
        IsSuccessful = true;
    }

    public bool IsSuccessful { get; set; }

    public string Message { get; set; }

    public T Result { get; set; }
}
public T Result { get; set; }

Ну вот зачем вы так? Теперь результат можно достать, полностью проигнорировав флаг IsSuccesful.

Как надо на C# не сделать, потому что у него для этого средств нет в системе типов.

…но можно приблизиться вот так:


private T result;
public T Result 
{
    get => IsSuccessful ? result : throw new InvalidOperationException();
    set // если вообще нужен
    {
        IsSuccessful = true;
        result = value;
        message = null;
    }
}
InvalidOperationException… который тоже чем-то ловить? :)))
Забавная идея…

Его не надо ловить. Это фатальная ошибка в коде.

Его не надо ловить. Это фатальная ошибка в коде.

Фатальная ошибка в коде зависящая от IsSuccessful ? result : throw new InvalidOperationException();?
Мы пишем обертку, которая должна принудить программиста проверить IsSuccessful перед тем как прочитать результат?
И у нас нигде в коде не будет ошибок вроде:
1. Упало при первом запуске, забыли проверить IsSuccessful
2. Упало — забыли указать IsSuccessful\ забыли передать значение\по умолчанию объект вернули.
При этом ничего не ловим, не логируем и просто даем ОС закрыть процесс?
Или мы с эти как-то планируем бороться?
Мы пишем обертку, которая должна принудить программиста проверить IsSuccessful перед тем как прочитать результат?

Да. Но в C# нет языковых средств, которые бы это могли обеспечить на этапе компиляции.


При этом ничего не ловим, не логируем и просто даем ОС закрыть процесс?

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


Можно поймать исключение на самом верхнем уровне. Можно вообще не ловить — тогда оно окажется в Event Log.

Во-первых, как уже написали выше, это уже фатальная ошибка.
Во-вторых, можно добавить методы Map/Select, Bind/SelectMany, Catch/OrElse...


Но в целом именно по этой причине выше и написано "для этого средств нет в системе типов"

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


Предпочитаю Maybe/Either-подобные подходы. Они получаются громоздкими в языках, не имеющих для этого специальных средств. Например, если иметь набор методов в стиле map, bind (а также ряд других, для сахара, такие unwrap_or_default и многие другие), то это резко снижает громоздкость кода, он становится зачастую даже более лаконичным, чем с try-catch.

Готовить их на самом деле просто. Есть нехитрое правило: исключения, про которые вы не в курсе, доверьте дефолтному обработчику. Если вы знаете, что метод N в каких-то случаях бросает исключение ESomething, и его обработка влияет на логику вызывающего метода, то вы его обрабатываете. Если вы не знаете про его существование, то не обрабатываете.
UFO just landed and posted this here

А разве если у вас есть "некритичный процесс" он не должен использоваться так чтобы не останавливать всю систему?
Он ведь может и outofmemory какой-нибудь выбросить....

UFO just landed and posted this here
Ну, тут под игнорированием явно подразумевалось отсутствие специальной обработки. Заворачиваете некритичный процесс в try catch Exception, складываете ошибки в лог и забываете.
Либо если это прям процесс, ловите необработанные исключения процесса и делаете всё то же.

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

В прикладном коде — можете. В инфраструктурном — да, иногда таки надо catch(Exception) написать.

Но если вы не знаете, что там может быть за исключение, и как восстанавливать работу в случае его возникновения, то откуда вы знаете, что процесс некритичный, а исключение глупое? Если этой информации нет в контракте метода, то полагаться на «авось» — тоже стрёмная стратегия.
UFO just landed and posted this here
Минуточку, но если вы знаете, что вот этот процесс некритичный и исключения от него нужно игнорировать, то вы знаете контракт.
UFO just landed and posted this here
Могу я никак не обрабатывать исключения которые в теории может кинуть эта библиотека?

«Никак не обрабатывать» — это значит, увалить приложение. Если вам это надо, можете.
Или мне как минимум надо завернуть её в общий try catch?

А если вам это надо, тоже можете. Я не понял, честно говоря, что вы хотите тут спросить. Я всего лишь написал, что не ловите исключения, если вы не знаете, как с ними поступать. Передавайте их в дефолтный или вышестоящий обработчик и падайте, или восстанавливайтесь в вышестоящем, если он там умеет, например, перезапускать весь аварийный процесс. А если знаете, как в вашем примере, так ловите себе на здоровье.
UFO just landed and posted this here

И всё таки, что делать с outofmemory которое может быть у кода который сам никаких исключений не выбрасывает (если рассматривать в контексте .net/c#)?

UFO just landed and posted this here

Т.е. по факту проблемы нет, мы обрабатываем все "знакомые" исключение а всё остальное (т.к. сервис "некритичных" падает в лог через catch (Excepton)

UFO just landed and posted this here

Есть ожидаемые ошибочные ситуации. Например, некорректный ввод от юзера. Всё остальное — нештатные ситуации. И это уже не ваша проблема. Упала БД — тухлые помидоры летят в девопса. Случился outofmemory — опять же виноват он. Ну и, как известно, быстро поднятое упавшим не считается.


Если вы не догадываетесь об исключении, скорее всего всё равно не существует нормального способа его обработки. Или у вас в каждом приложении есть обработка ситуаций вроде "юзер удалил половину файлов приложения"?

Если вы не догадываетесь об исключении, скорее всего всё равно не существует нормального способа его обработки

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

Я считаю, это ошибка создателей C#. Они наслушались жалоб про то, какие эксепшены неудобные и как их надо постоянно обрабатывать в Java, и сделали unchecked вариант, где обработчики не форсируются компилятором. В итоге получили, что всё пропало из сигнатур.
Стандартный вариант в Java (checked exception) аналогичен использованию maybe/either — при использовании в коде всегда нужно описывать как минимум два пути: один для положительного результата, другой для ошибки.

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

Эти сущности и есть исключения)
Checked exceptions — это ожидаемые ошибочные состояния. Если мы читаем из сети, то мы ожидаем или ошибку, или ответ. Если мы читаем из файла, то мы ожидаем или ошибку, или данные.
Unchecked exceptions — неожидаемые. Типа, закончилась оперативная память и не получилось аллоцировать объект. Или случился assertion, который ну никак не должен был случиться, но из-за бага кто-то его допустил. Или деление на 0 (которое по-хорошему надо делать checked exception, или оформлять в виде отдельной сущности — Either<Integer, DivisionByZeroError> result = 10 / 0;, — но это сильно замусорит код).

Unchecked exceptions — неожидаемые. Типа, закончилась оперативная память

На самом деле в джаве это Error, который вообще в другой ветке наследования, чем RuntimeException.

В джаве сложно с ветками наследования)
Есть базовый Throwable, у него подклассы Error и Exception. При этом Throwable — checked, а Error — unchecked.
Потом идёт Exception, у него подкласс RuntimeException. При этом Exception — checked, а RuntimeException — unchecked.
Сделали бы две базовые ветки (как вариант, просто переименовать RuntimeException в RuntimeError и запихнуть в Error) и было бы норм.

Проблема checked exception в том, что не совсем понятно их назначение.


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


Если их использовать для того, чтобы декларировать, какие исключения может выбрасывать конкретный метод — тоже сомнительная полезность. Почему одни исключения мы декларируем, а другие игнорируем? Разделяем опять же исключения на "бизнес-логику" и "ошибки платформы" — а почему они разные, они же не всегда должны по-разному обрабатываться?


Как быть с интерфейсами? Checked Exceptions — это свойство интерфейса или реализации?


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

UFO just landed and posted this here

А как потом происходит их обработка? Например, через несколько уровней?

UFO just landed and posted this here

Если мне не надо дифференцировать, у меня есть Exception

UFO just landed and posted this here
А если вам надо дифференцировать какое-то относительно небольшое конечное количество случаев?

То значит вы используете исключения для бизнес логики (ну либо это такой АПИ у библиотечного кода, как в случае с System.IO.*), и лучше бы их заменить на что-то другое (но в шарпе нет хороших альтернатив)


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

По-хорошему, количество возможных исключений для метода — это произведение количества всех возможных исключений для всех АПИ, которые вызываются методом + количество исключений, которые выбрасываются непосредственно. То есть оно достаточно большое.


Но поинт не в том. Если для нормального использования АПИ вам нужно знать типы исключений, которые в нем выбрасываются, значит (скорее всего), АПИ выбрасывает эти исключения в качестве некоего аналога возвращаемого значения (а не в случае исключительной ситуации). И это неправильно.

UFO just landed and posted this here
Если они нужны для того, чтобы декларировать какие вообще исключения могут выбрасываться методом — то их будут сотни в любом методе, пользоваться этим будет невозможно (даже с автоматическим выводом).

То же самое можно будет сказать про Either/Maybe из поста.
Допустим, у нас есть OneOf, который работает как Either с несколькими типами. И тогда у вас будут что сотни эксепшенов, что сотни видов результатов.


OneOf<GoodResult, OpenError, ReadError, FindError> result = getResult();
if (result.first()) {
  // good
} else if (result.second()) {
  // open error
} else if (result.third()) {
  // read error
} else if (result.fourth() {
  // find error
}

эквивалентно:


try {
  GoodResult result = getResult();
  // good
} catch (OpenError) {
  // open error
} catch (ReadError) {
  // read error
} catch (FindError) {
  // read error
}

На это вы можете возразить, что мы не хотим использовать OneOf<GoodResult, OpenError, ReadError>, а хотим использовать Either<GoodResult, BadResult> из только двух состояний.
В этом случае и с эксепшенами можно писать:


try {
  GoodResult result = getResult();
  // good
} catch (BadResult) {
  // some error
}
На это вы можете возразить, что мы не хотим использовать OneOf<GoodResult, OpenError, ReadError>, а хотим использовать Either<GoodResult, BadResult> из только двух состояний.
В этом случае и с эксепшенами можно писать:

Ну так BadResult точно так же может быть типом-суммой. И на уровне типов гарантируется, что все варианты будут обработаны.

Как быть с интерфейсами? Checked Exceptions — это свойство интерфейса или реализации?

Свойство интерфейса.
Как вы пишете


interface Foo {
  Either<Result, Error> getResult();
}

так можно и писать


interface Foo {
  Result getResult() throws Error;
}
А зачем для ненайденного пользователя вообще исключение?
Исключения, всё же, для более исключительных ситуаций, типа связь с сервером отвалилась, место кончилось.
А использовать их для передачи данных (факта отсутствия данного пользователя) — так себе идея.

Обычно у подобных функций есть две версии — одна возвращает какой-нибудь optional, а другая бросает исключение. Первую вызывают, когда в ТЗ бизнес логики есть слова "а если пользователь не найден, то делаем Х", во втором случае, когда такого нет, потому что эта ситуация сигнализирует о каком-то критическом сбое, когда останется только нарисовать красивое сообщение о фатальной ошибке, но с этим отлично справляется обработчик верхнего уровня.

А что тогда должен вернуть метод? Какой нить константный DefaultUser тоже так себе идея минимум в 30..40% случаев по моему опыту. За ним потянется цепочка всяких DefaultUserBlaBlaPropertiesRelationships и так далее. Нет серебрянной пули тут. Но на круг, ексепшен выходит лучше всего. Видишь сигнатуру NotFoundException, IllegialArgumentException и сразу понимаешь в каких случаях тебя пошлют в пеший эротический тур. Во всяком случае null уже не ждешь.

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

я понимаю, что пример выдуманный, но раз уж на нём обсуждаем… )
По сути тут одна проблема описана — лень обрабатывать какой-то «неожиданный» результат.
Какая разница, как эта «детская неожиданность» проявится, если «оно» уже попало в «вентилятор»? Приложение или идет по Success Flow или не идет по Success Flow и принимаются какие-то «аварийные меры».
Принципиальной разницы в обработке Exception или return resultCode — вообще никакой. в любом случае придется писать «простынку» кода, который будет разгребать «а что случилось ?!»
Есть два эмпирических правила:
1. пиши код\библиотеку так, как будто ей будет пользоваться «слюнявый идиот». Поэтому ты обязан сделать так, чтобы ошибку нельзя было игнорировать.
2. используй чужой код так, как будто его писал «слюнявый идиот». Поэтому код должен быть написан так, чтобы знать об ошибке.
Иногда, обнаруживается что этот «слюнявый идиот» — это ты сам и есть, с разницей в пару недель… месяцев.
Поэтому Exception «всплывает» на уровень среды исполнения приложения, как последний шанс разобраться, перед тем как ОС убъет приложение принудительно.
Формализованно, если мне не изменяет память, это описано в каком-то из стандартов ISO 2700x (не помню в каком именно).
Принципиальной разницы в обработке Exception или return resultCode

Исключение (также как return code и out-параметр) можно проигнорировать. Maybe/Either игнорировать не получится из-за несовпадения типов. Поэтому совсем уж проигнорировать ошибку не выйдет, пользователь кода увидит ошибку компиляции. Даже если везде вставлять условный unwrap(), это все равно более заметно, чем игнорирование ошибок. Можно даже линтер настроить какой-нибудь, чтобы ругался.

Исключение (также как return code и out-параметр) можно проигнорировать.

Исключение можно, но до первого обработчика — своего «адекватного», а для этого разработчику нужно спецально приложить усилия. Изначально среда исполнения заставляет делать «правильно», иначе просто убивает такой процесс.
return code и out-параметр

Тут как «дизайн» сделан будет. Если криво — сам себе злобный буратино. Например WinAPI так сделан — все «оборачивается» и на уровне рефлекса пишется обертка и обработка вызова таких функций.
Maybe/Either

Если я верно прочитал описания к Maybe/Either для Haskell — это такой же «синтаксический сахар» позволяющий пихать что угодно куда угодно и возвращать «по умолчанию» если «не получилось». По сути проглатывание ошибок. Но опять же, нужно ручками указать «что я сам себе злобный буратино» и хочу проигнорить ошибку.
А что будете делать для «не верный пароль», «нет памяти», «Timeout» и т.д.? Стратегии обработки ошибок разные для разных исключений.
По сути проглатывание ошибок.

Почему? Например, условный Maybe<T> не может быть применен там, где ожидается T.


А что будете делать для «не верный пароль», «нет памяти», «Timeout» и т.д.?

Either/Result хранят значением ошибки, если "не получилось". Которое может быть типом-суммой, по которому осуществляется match. К примеру.

Исключение (также как return code и out-параметр) можно проигнорировать. Maybe/Either игнорировать не получится из-за несовпадения типов.
Что вам помешает игнорировать несовпадение типа? TypeCastException?
приведите тогда код, я не понимаю о каком несовпадении типов речь. Если взять пример из статьи, то можно Maybe привести к Success и никакой ошибки компиляции не будет.

Простейший пример. Но я не затрагиваю проблему реализации самих Maybe/Either на C#, я давно не писал на C#.


// Сигнатуры
Either<Foo, Error>  getFoo(); 
Either<Bar, Error> processFoo(Foo foo);

var foo = getFoo();
var bar = processFoo(foo); // Ошибка: Either<Foo, Error>, expected Foo.
// Нужна обработка:
var bar = foo.bind(f => processFoo(f));
ну вот в реализации как раз таки и соль.

Либо я что-то не понимаю, либо безболезненно превратить C<T> в T нельзя в любом случае.

В реализации из статьи можно сделать так:
var user = ((Success<User>)service.GetUser()).Value;

Автор попытался имитировать тип-сумму через наследование. Я как-то делал Option, Result на Python, и сделал одним классом. Идея примерно такая (просите, я мог забыть C#):


class Option<T>
{
    private T _value;
    private bool _is_some;
    private Result() {}
    public static Result Some(value T)
    {
         var res = Result();
         res.v_value = value;
         res._is_some = true;
         return res;
     }
     static Option Nothing()
     {
          var res = Option();
          res._is_just = false;
          return res;
     }
     bool IsSome() => this._is_some;
     bool IsNothing() => this._is_nothing;
     T unwrap()
     {
          if (this._is_some) return this._value;
          raise NothingOptionUnwrapException();
     }
     // map, bind, unwrap_or_default и так далее

}

Состояние инкапсулировано в объекте и никакими кастами его не изменить. Попытка unwrap'а кидает эксепшн. Ну да, щито поделать. В Rust попытка unwrap'а пустого Option вызовет панику. Правда, у автора, вроде, был еще паттерн-матчинг, а тут не получится, вроде.

но тут у вас тоже есть исключение :)

В Rust есть panic, сделал аналог. Можете не делать unwrap, а оставить только другие методы. Так даже более правильно.

var bar = processFoo(foo); // Ошибка: Either<Foo, Error>, expected Foo.

Это ошибка времени исполнения или ошибка времени компиляции? Это два разных «уровня»
Если это ошибка времени компиляции, то это ошибка уровня дизайна решения.
Если это ошибка времени исполнения, то код
// Ошибка: Either<Foo, Error>, expected Foo.
// Нужна обработка:

это то же «велосипед» фильтра ошибок. Те же яйца только в профиль.
Определитесь со стратегией обработки ошибок на этапе дизайна решения.
Либо вы хотите их обрабатывать либо нет. В чем проблема?
Вы не знаете какие ошибки генерит библиотека стороння? Это не так. Вы их знаете, поскольку это будут такие же классы публичные xxxException с каким-то метаописанием, если это специальные классы если не хватило стандартных. Вы же не будете изобретать свой PuperOutOfRangeException делающий тоже самое, что и стандартный?
Сторонний код может сгенерировать не только «свои ошибки», но и среды исполненения (переполнение стека, деление на ноль, выход за переделы индекса и т.д.) Любая ошибка может быть в любое время и в любом месте, а не только те что «мне удобно» и «я тут ожидаю».
В любом месте кода у вас два состояния — или я знаю что могу сделать для исправления ошибки, или я не знаю, и передаю «проблему вышестоящему».
Но в любом случае должно быть: минимизация ущерба для клиента и его данных, вернуться в состояние «как было ДО...» и максимум информации, что бы как можно быстрее понять «что случилось» для максимально дешевого «исправления».
P.S.
Есть еще третье — а мне пофигу и у меня все хорошо. Но за такое, обычно, заставляют заниматься отладкой релизной версии без отладочной информации консольным отладчиком в машинных кодах, что бы не повадно было такую хрень больше делать.
Это ошибка времени исполнения или ошибка времени компиляции? Это два разных «уровня»

Компилятор защищает разработчика, если он забыл обработать значение, пришедшее откуда-то, на ошибки, тем самым "разыменовав" Maybe/Either в нормальный объект, с которым он хочет работать.


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

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

Вообще не вижу проблемы с исключением. Лучшие практики от МС говорят, что надо обрабатывать только те исключения, которые ты ожидаешь. Если ты получаешь объект по ID, и он не найден, то на самом деле ситуации с исключением или null равноправны и зависят от логики и здравого смысла. Например, я бы ожидал исключение для условного филиала компании и null для атрибута финансового актива.


Исключения хороши, потому что гарантированно прерывают выполнение при нарушении логики. Но они дорогие, поэтому есть есть еще один паттерн, который реализован, например, в asp.net core, и о котором вы не упомянули. Этот же паттерн, по сути, реализован в Regex тоже: в качестве результата всегда возвращается объект (не исключение и не null), в котором есть свойства такие как bool Success, object Value и string Error например.


Производительность не страдает, async/await поддерживается, строгая типизация присутствует, особенно при комбинации с дженериками, возможность использовать структуры для оптимизации памяти тоже есть.

Я несколько лет назад работал в роли QA (тестировщика, грубо говоря) по одному увесистому проекту на с#. Так вот там философия была проста как пять копеек — исключения выбрасывались везде где только можно, но нигде не обрабатывались. Вообще нигде. Программа валилась от любого чиха, я пачками репортил листинги цепочек исключений, они планомерно исправлялись, а я потом находил новые. Ну то есть такая «TDD» методология — нафига изначально раздувать код обработкой исключений, заткнём только то, что найдут тестировщики. Я ушёл потом оттуда, ибо после нескольких недель такой работы даже дома ещё долго вздрагивал при открытии любого окна или файла…

А мне кажется подход неплохим. Ибо какой смысл программисту писать код обработки фантастических ситуаций "приложение запустили на Марсе, предварительно удалив каждый второй файл".

UFO just landed and posted this here

Этим занимается глобальный обработчик исключения. А также всякие try with resources и finally. Нужно просто по умолчанию считать, что любой метод может бросить любое исключение и если в ТЗ не прописано особенное действие, то надо обспечивать лишь минимальное освобождение ресурсов).

UFO just landed and posted this here

Судя по тому, что разработчики таки получали репорты с цепочками исключений — этот обработчик был.

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

А, ну это уже и правда клиника.

Как оказалось, это не лишено смысла.

Мне грешным делом казалось, что «писать в логи/показывать пользователю (или не показывать, смотря какой пользователь)» — это настолько очевидное дефолтное поведение, что о нём вообще не нужно упоминать :) Ну и естественно, это обычно делается где-то в главном обработчике.

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

Система логирования, кстати, это одно из немногих мест, где бывает уместен знаменитый говнопаттерн
try {
    LogSomething();
}
catch()
{
   //ignore any errors
}

Не уместен, логгеру лучше в отдельном треде крутиться, а в основном не кидать никаких исключений.

Во-первых, в отдельном или не отдельном — это смотря какое у вас приложение. Во-вторых, логгеру во многих случаях вообще нельзя кидать никаких исключений, т.к. необработанное исключение логгера не должно валить программу (а оно в общем случае увалит даже из другого треда, по крайней мере, если это упоминаемый здесь дотнет), а обработанное… обработанное ведь скорее всего будет завернуто в тот же логгер, как в случае у dmitry_dvm.
Я тоже сторонник минимализма, но всё хорошо в меру. В данном случае, скажем, достаточно было открыть файл с флешки, затем вытащить флешку и вызвать диалог снова, и всё валилось в исключение, унося в нирвану уже открытые файлы. Попытки сохранения на заполненный диск, в файл, у которого «только для чтения» и т.п. — вот это вот всё крешилось. В принципе можно всё и вся аккуратно сделать на исключениях и оно будет работать, но в данном случае разработчики судя по всему даже не утруждали себя проверкой тривиальных ситуаций, поскольку сборка автоматически шла на сервере и я был единственный, кто эти сборки запускал. И ведь при этом код был покрыт тестами, и они удивительным образом проходили, поскольку выброшенное исключение было ожидаемым поведением. Я иногда смотрел код (который проходил ревью — там вообще весь процесс разработки как по учебнику шёл) и мог порой заранее сказать где оно рухнет вообще без запуска.
Лучше всего понять, чем плохи исключения, можно, когда используешь чужой, плохо задокументированный код. Есть условный метод GetById, а что он станет делать, если не найдет — ну ты понятия не имеешь. Вернет null? Выбросит какое-то исключение?

Как раз для разрешения таких вопросов в .NET Framework есть очень простая и хорошая традиция: если функция не смогла сделать то, что стоит в её имени, она должна бросать исключение. Именно поэтому в стандартной библиотеке есть Int32.Parse которая бросает исключение если «не шмогла» и Int32.TryParse котрая честно пытается и возвращает флаг ошибки при неудаче. Разработчик лучше знает логику приложения, ему видней является ли неверный ввод обыденностью валидации или действительно исключительной ситуацией — ему и выбирать какую из функций использовать.

Пользователю такое может и удобно, а вот копипастить эти методы-побратимы утомительно.

Зачем их копипастить? Один вызывает другой.

Ну, вот этот вызов и придётся копипастить:


public Foo GetFoo(int id) => TryGetFoo(id, out var foo) ? foo : throw new FooNotFoundException();

public Bar GetBar(int id) => TryGetBar(id, out var bar) ? bar : throw new BarNotFoundException();

public Baz GetBaz(int id) => TryGetBaz(id, out var baz) ? baz : throw new BazNotFoundException();
UFO just landed and posted this here
через кодогенерацию это надо решать.
UFO just landed and posted this here

Использую этот подход и на 4.5 с глобальным обработчиком исключений и на .net core (на любой версии). В основном подходит для WebApi
P.S. Приведенный пример естественно для .net core. Для 4.5+ через Global asax


1)Объявляем список возможных ошибок, пример:


public enum ErrorEnum 
{
ERR_OK = 0,
ERR_DB_CONNECTION,
ERR_SOME_EXTERNAL_API_CONNECTION,
...
// другие исключения
ERR_INTERNAL
}

Далее описываю кастомный Exception


public class ApiExc : Exception
    {
        public ErrorEnum ExcCode { get; set; }

        public ApiExc(ErrorEnum excCode, string message, Exception innerException = null) : base(message, innerException) => ExcCode = excCode;
    }

далее глобально юзаю ExceptionFilter


public void OnException(ExceptionContext context)
            {
                var e = context.Exception;

                ApiError error;

                if (e is ApiExc exc)
                    error = new ApiError { Code = exc.ExcCode, Desc = exc.Message };
                else
                    error = new ApiError { Code = ErrorEnum.ERR_INTERNAL, Desc =e.Message };

                context.Exception = null;

                ControllersHelper.ResponseError(error).ExecuteResultAsync(context);
            }

плюсы:


1)единый обработчик исключений на весь проект. Любое необрабатываемое исключение породит JSON с кодом ошибки ERR_INTERNAL
2)легко обрабатывать нудные исключения в пользовательском коде. Допустим в методе API надо проверить есть ли коннект к бд или нет (а метод возвращает exception с кодом ERR_DB_CONNECTION


Код превратится в


try

{
CheckConnect();
return true
}

catch (ApiExc e) when e.ExcCode == ErrorEnum.ERR_DB_CONNECTION
{
return false;
}
Интересно, а исключения, которые выбрасываются нижележащим кодом, вы тоже будете ловить и оборачивать? Или вы предлагает все ваши примеры ещё и в try...catch обернуть сверху для простоты и лаконичности?

Кстати, ещё вопрос, что хуже: засовывать голову в песок или решать (возможно даже некомпетентно) сторонние проблемы, для которых ваш класс не предназначен, и от него это не ожидается.

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

Мне кажется, наиболее элегантно эту проблему попробовали решить в Swift.

// Error это просто протокол, может быть чем угодно
enum TaskError: Error {
  case invalid(reason: String)
  case fatal
}

protocol Task {
  // в протоколе-интерфейсе мы можем явно указать, что функция кидает ошибку
  func run() throws -> Int
}

class ValidTask: Task {
  // можно не реализовывать метод как throws, даже если протокол требует
  func run() -> Int {
    return 42
  }
}

class InvalidTask: Task {
  // в обратную сторону не работает, протокол тоже должен объявлять throws
  func run() throws -> Int {
    throw TaskError.invalid(reason: "dunno")
  }
}

Ну и самое вкусное — обработка ошибок.

let task: Task = ValidTask()
// let task: Task = InvalidTask()

// 1
do {
  let result = try task.run()

  print("try succeeded \(result)")
} catch TaskError.invalid(let reason) {
  print("try failed \(reason)")
} catch {
  print("try unknown error \(error)")
}

// 2
if let result = try? task.run() {
  print("if succeeded \(result)")
} else {
  print("if failed")
}

// Можно даже в функциональном стиле
let opt: Optional<Int> = try? task.run()

opt.map({ v in print("map succeeded \(v)") })
// rethrows тоже крутая штука
try opt.map({ _ in throw TaskError.fatal })

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

А ещё в 5 версии в стандартную библиотеку языка включили Result (точно Rust повлиял). Кортежи так же в языке присутствуют, можно в Golang стиле писать.
Я слышал, что можно разрабатывать по SOLID'у и не нарушать правило зависимости. Более устойчивый модуль не должен зависеть от менее устойчивого, что касается и исключений. Реализуя интерфейс бизнес-логики, мы и выбрасываем исключения бизнес-логики и никакие другие, получая гарантию, что они будут обработаны. Это вопрос проектирования интерфейсами.

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


В то же время, условный


fn calc_improtant_thing() -> Result<ImportantThing, MyDomainError>

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

Что же вы не идеоматично обрабатываете Maybe?
Должна быть функция:
Maybe<V> Bind(this Maybe<T>, Func<T,V>)
Которая на самом деле очень напоминает оператор ?.. Это очень сокращает код, избавившись от управляющих конструкций. Их можно чейнить, а Nothing обрабатывается только один раз в конце цепочки. И не надо никаких switch, чтобы не возникало ошибок, что реализованы не все варианты наследников.
Ещё можно добавить проперти
bool IsSome { get; }
как в Nullable, чтобы можно было проверять с помощью конструкции if вместо switch.

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

По моему скромному опыту Rust куда не завезли do-нотацию, именно в функциях обработки Option/Result сила, потому что паттерн матчинг ошибок быстро превращается в вермишель. Более того, такой подход принуждает разработчика декомпозировать функции. Там могло быть большое полотно с разными try-catch, if, match итп, приходится писать маленькие функции по выполнению того или иного действия в цепочке операций, образованных map и bind.

есть сомнения, что в Rust другая проблема: преобразование Option в Result, и наоборот.
Как вариант решения надо было вместо Option сделать только Result, у которого есть None, типа такого:
enum Result<T, E>{
  Ok(T),
  Err(E),
  None
}

В таком случае Option не нужен, достаточно Result.

А если Err не нужен, то что делать? А если не нужен None?


Нет в Rust проблемы с преобразованием между Option и Result, потому что есть методы ok_or и ok.

А если Err не нужен, то что делать?


Result<T, ()>

Тогда уж Result<T, !>. Но всё ещё не понятно что с None делать когда он не нужен.

Должна быть функция:
Maybe<V> Bind<T, V>(this Maybe<T>, Func<T, V>)

Маленькое уточнение — функция Bind приведённая выше на самом деле называется Map.

Сигнатура для функции Bind выглядит вот так:
Maybe<V> Bind<T, V>(this Maybe<T>, Func<T, Maybe<V>>)
А если ещё точнее, то функция называется FMap. Map это для частного случая списка.
Да и эта функция тоже должна быть. Просто в C# неудобно каждый раз заворачивать Just-результат в контейнер. И чаще всего у нас уже есть функции, которые принимаю чистое значение и возвращают чистое значение. Функций которые возвращают значение в контейнере Maybe намного меньше.

fmap она называется из-за конфликтов имён в Хаскеле. Нет никаких причин добавлять букву f в обрыве от конкретного языка программирования.

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


GetUser( id: string, NotFound: ()=> null )

GetUser( id: string, NotFound: ()=> me )

try {
    GetUser( id: string, NotFound: ()=> { throw new MyError } )
} catch( e: MyError ) {}

Хороший подход, но он на самом деле мало где применим — особенно с учетом достаточно бедной системы типов в C#. Например в тайпскрипте можно сделать функцию, которая отдает мне User | T, где T — тип, который отдаёт моя стратегия обработки исключительных ситуаций. И вот это подойдет уже везде, потому что где-то я смогу отдавать например дефолтного юзера, где-то тот же undefined, а где-то — объект с информацией о деталях ошибки.


А в C# есть опция только с дефолтным значением, потому что типов объединений там пока нет

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


GetUser( id: string, ConnectionRefused: ()=> secondaryConnection )

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

Ну да, чаще это кастомный эксепшен, который сам и кидаешь и ловишь, так что и неопределнности с прилетающими эксепшенами нет.

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

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

Не получится ничего не добавляя в язык, как минимум в языке изначально должны быть обобщенные типы, discriminated unions, сопоставление с образцом, лямбды и, желательно, do-нотация.

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

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

UFO just landed and posted this here

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

UFO just landed and posted this here

Это не входит в ее рабочие обязанности =)
Как не входит в зону ответственности сервиса хоть каклибо оценивать данные, которые его попросили передать. А вот просить продавщицу решать проблемы логистики и поставок — это искаженное восприятие действительности. Если возникают вопросы какой ексепшн кинуть или как, скорее всего вы делаете что не то и не там)

Вам когда-нибудь приходилось анализировать 7 уровней try/catch методов с возможным отловом и обёртыванием/заменой типа исключения на каждом из уровней?

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

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

public class Message
{
    public LevelEnum Level { get; set; }
    public string Text { get; set; }
    // Если смогли отловить исключение, то оно пригодится.
    public Exception Error { get; set; }
}

Процессоры же отвечающие за обработку бизнес-логики начинают накапливать итоги ошибок:

public TResponse ProcessSomething(...)
{
    // Ответ имеет коллекцию List<Message> и метод Add добавляющий в неё.
    var response = new FooResponse();
    Message msg;

    var step1 = DoStepOne(out msg);
    if (!response.Add(msg).IsSuccess)
        return response;

    // Или с приходом ValueTuple и моды на Go:
    var (step2, msg2) = DoStepTwo(...);
    if (!response.Add(msg2).IsSuccess)
        return response;

    // Или с приходом моды на Монады:
    var step3WithMessageMaybe = DoStepThree(...);
    if (!step3WithMessageMaybe.TryGetRight(out var step3))
        return response.Add(step3Maybe.Left);

    // А иногда нужен более подробный объектный трейс:
    var step4WithTraceMaybe = DoStepFour(...);
    if (!step4WithTraceMaybe.TryGetRight(out var step4))
        return response.AddFooTrace(step4WithTraceMaybe.Left);

    ...
}

Лично для меня, все 3 варианта выглядят плюс-минус одинаково и все они имеют риск опечатки, когда случайно используется ответ/результат из предыдущего шага.
Railway Oriented Programming (или ссылка #2) подход отчасти может уменьшить риск опечатки, но с накоплением сложного трейса в нём туже.

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

Классический подход к таким вещам в C# — исключения

Перестал читать с этого места. ЭТО НЕ КЛАССИЧЕСКИЙ ПОДХОД!
Это подход криворуких джуниоров.
Исключения — это очень дорогая операция и опытные программисты стараются её избегать.
Никакая это не классика.
И вот именно с этого места вся статья и посыпалась как карточный домик

Ох как эмоционально. А теперь мы что, мы идём в любую кодовую базу среднего проекта на C#, и видим, что именно подход с исключениями там — основной.

Вам сейчас ответят что
кодовую базу среднего проекта на C#

пишут
криворукие джуниоры

и доказывать ничего не надо)
Исключения — это очень дорогая операция и опытные программисты стараются её избегать.

Ну как дорогая… в середине 90-х ещё была дорогая. Сейчас совершенно пустяковая, в сравнении с вызовами окружающих фреймворков/библиотек.
Ну как дорогая… в середине 90-х ещё была дорогая. Сейчас совершенно пустяковая, в сравнении с вызовами окружающих фреймворков/библиотек.
в середине 10х ускорил старт мобильного приложения почти на секунду за счёт того, что заглушил исключение там, где оно возникло, а не наверху цепочки await
Очевидно, проблема была отнюдь не в исключении, а в коде, который его обрабатывал.
который её не обрабатывал, скорее. Но это никак не отменяет того, что подъём исключения по стеку вызовов операция не дешёвая и сильно дороже, чем вернуть значение.
Не дешёвая, но не настолько уж и дорогая сейчас. А самое главное, не взаимозаменяемые они. «Вернуть значение» подразумевает, что результаты вызываемой функции известны и все они должны быть обработаны непосредственно после вызова. Исключение предполагает, что может быть что-то нештатное, неизвестное вызывающему методу, и его обработает какой-то вышестоящий метод.
Не дешёвая, но не настолько уж и дорогая сейчас.

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

Ну так там же дело не в исключениях было, а в каком-то кривом коде в его обработчике. Этот же код мог вызываться и без всяких исключений, if (!Connect()) TryAgainSomeMoreTime();
Дело не в исключении, но это не делает пробросу исключений быстрее.

if (!Connect()) TryAgainSomeMoreTime();

if (servernotfound)
Как бы вы не меняли код, «тормозящее» место у вас будет не в проброске исключений, а вот тут:
var servernotfound = !Connect();
if (servernotfound)
мне очень интересно, какой смысл вы вкладываете в функцию Connect()?
Пардон, я внимательнее прочитал, проблема там была после коннекта. Ну тогда тормозящее место
if (servernotfound)
{
//где-то вот тут
}
И не особо важно, как тот код обернуть, в if() {} или в catch() {}
Следующий вопрос, как решение проблемы, уже решённой 5 лет назад помогает оспорить утверждение, что «исключения медленно пробрасываться по стеку»? Это до сих пор так, иначе бы не возникало рекомендаций использовать исключения только в исключительных ситуациях. (не взирая на их название)
помогает оспорить утверждение, что «исключения медленно пробрасываться по стеку»? Это до сих пор так

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

Исключение возникало при каждом запуске? Какое-то оно тогда… не исключительное.

При половине запусков ) В сети сотового оператора запрос выполнялся, а по Wi-Fi — нет, случался WebException

Чем сейчас, кстати, занимаешься, после того, как винфон всё?

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


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

В TryPattern можно выдавать в out string InfoMsg, куда писать как минимум Exception.Message + необходимые пояснения, если они нужны. И выводить их клиенту в зависимости от (нужное подставить).

Maybe.CreateSuccess может отдавать Maybe, и если пихнули нулл возвращать Failure

Имеется ввиду, что если бы берёте чужую функцию с подобным Maybe, то она может быть и такой:
Maybe<User> GetUser()
{
    return null;
}

Ну, как минимум оно свалится в default свитча, который, после вашего комента, я теперь буду ставить везде)

Да, действительно, спасибо

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


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

Ничего, в C# система типов довольно слабая.

Добавлю свои две копейки.
Еще есть смысл понимать, что есть два типа ошибок.
  • Ошибка(Error) по бизнес логике
  • Отказ(Failure) по инфраструктуре.

Почему это имеет значение? Обычно ошибки можно и нужно обработать сразу по месту, и тогда с эксепшенами работать не удобно, но касательно глобальных отказов код обычно не уполномочен париться, и тогда пробрасывать Result вверх по стеку не удобно.
Поэтому неплохо заходит, в случае ошибок работать с Result, а в случае отказа инфраструктуры кидать Exception.
Например, пользователь выбрал лимит чего-то там, или прав доступа нет, операция возвращает Error. Отвалилась сеть или бд, кидаем Exception.
Давным давно, во времена .NET 1.1, Microsoft ввела базовые классы ApplicationException и RuntimeException и настоятельно рекомендовала наследовать свои исключения от первого или второго в зависимости от типа ошибки. Со временем от этой идеи отказались.
Хорошая была идея так-то, насколько помню, сломалась на том, что команда разработки платформы, сама их перепутала в нескольких местах.
Хотя она таки и не решает проблему «статической слепоты» при повсеместном внедрении интерфейсов через DI.
Я принес вам решение проблемы с исключениями в C#

Ну не вы, а тема существует с версии 4.5, как минимум. Зачем клепать велосипед, когда готовые решения существуют и довольно давно. Сама конструкция Maybe не имеет никакого смысла без Rop например.

Ненайденный юзер — это null, а не исключительная ситуация. Зачем бросаться экзепшенами направо и налево во вполне корректных кейсах?

Null — это ошибка на миллиард долларов, как завещал нам их автор.

Ничего автор не принес. Все это давно есть в Scala, Rust и т.д.
От всего сообщества C#-разработчиков, выражаю вам глубокую радость по этому поводу!
Я проголосовал за подход SomeOrDefault, он мне нравится, но проверки на null утомляют.

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

interface UserRepository {
	userExist(userId: string): boolean;
	getUserOrThrow(userId: string): User;
}

// Пример использования:
if (userRepository.userExist(userId)) {
    const user = userRepository.getUserOrThrow(userId);
    // работаем с юзером, будучи уверенными, что всё ок
}
// делаем что-то другое, раз юзера не существует...


Смысл такой, мы не обрабатываем исключения через try/catch, которые бросает метод getUserOrThrow. Если исключение брошено, значит что-то пошло не по плану (500).

Но если нужно (ответить 404, например), мы проверяем причины, по которым может быть вызвано исключение, и обрабатываем их с помощью обычных if/else.

Как вам такой подход?

А можно немножко расшифровать предлагаемый подход.


Вот допустим у нас есть класс AuthorizationService который где то там использует метод GetUser.
Этот метод GetUser должен быть на интерфейсе, допустим, IUserRepository у которого две реализвации EntityFrameWorkUserRepository и XmlFileUserRepository.


С точки зрения существующего C# какие монады


  1. Какие монады или Exception должен декларировать IUserReporsitory?
  2. Как примерно должен выглядеть код обработки ошибок? В AuthorizationService и репозиориях? — т.е. как транслировать ошибки более высокого уровня в более низкого уровня.
  3. При возникновении ошибок как будет выглядеть лог? (В случае Exception мы получаем stack trace).

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

А мне нравится, как реализован контроль ошибок в LabView. Он тоже конвенционален, но позволяет логично уменьшить объем кода, ответственный за обработку ошибок. Например, вы вызываете последовательно несколько методов. И результат важен, когда все они выполнились без ошибок. Ошибка на каждом этапе делает бесполезным результат.
Смысл контроля ошибок в LabView в том, что состояние это структура из трех полей: Булевая ошибка (false — Ok, true — Ошибка), код ошибки (long), и описание ошибки (строка). Каждая функция содержит такую выходную структуру. Анализ результата прост — проверка на ошибку. Если ошибка, то можно проанализировать код ошибки, а также использовать описание ошибки, например, для вывода пользователю. По умолчанию (библиотечные функции) в описание ошибки записывается также название функции, которая эту ошибку вызвала. Если ошибок нет, то код ошибки равен нулю, а описание ошибки — пустая строка.
Но этого мало. Конвенциональная практика в Labview состоит в том, что функции передается еще и структура входной ошибки. И проверка производится ВНУТРИ функции. т.е если на входе уже ошибка, то код функции не выполняется, а входная ошибка передается на выход. И так далее по цепочке. В конце цепочки вызовов производится проверка на ошибку. Т.е обработчик ошибок не размазан по всему коду. И вы точно понимаете, что никаких действий функция не будет выполнять, если в нее поступили данные с признаком ошибки.

Вообще-то, такой подход как раз и называется "обработчик ошибок размазан по всему коду".


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

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

Проблема вот тут:


И вы точно понимаете, что никаких действий функция не будет выполнять, если в нее поступили данные с признаком ошибки.
да нет тут проблем. Если мы обсуждаем именно Labview. Максимум, что вы получите — это результат выполнения функции с ошибкой (в виде заполненной структуры), а также окошка с ошибкой, причем позволяющего продолжить работу, для этого никакого кода писать не надо. Чтобы положить среду и выполняемый код в результате неконсистентных данных, это надо постараться.

Ну так в C# то же самое: чтобы положить именно среду выполнения, надо постараться. "Окошко с ошибкой", правда, придётся самому писать — но это всего лишь десяток строчек, которые можно нагуглить и скопировать.

Есть нюанс. У меня в проектах по несколько тысяч функций, не говоря уже о штатных библиотеках. И после каждого вызова не накопируешься окошек с ошибками. То, что среда автоматически позволяет показать ошибку (а скрыть ее еще проще), это огромное удобство, которое и уровень вхождения снижает и повышает качество программного кода. Не удивлюсь, если National Instruments защитила эту идею патентом. Так как реально это упрощает работу ( я программирую на разных языках уже 30 лет и мне есть с чем сравнивать)

Зачем после каждого? Достаточно одного на программу. Я не понимаю как вообще можно даже рассматривать возможность написания отдельного окошка для каждой функции.


А для снижения уровня вхождения есть отладчик...

Любому текстовому отладчику до Labview как до Луны. Я совершенно серьезно утверждаю.
Могу сказать только, что в LabView обрабатывать ошибки существенно проще, особенно в сравнении с вышеприведенным текстом. Я точно знаю, без всяких конвенций библиотеки — результат ошибка или нет. Код ошибки — независимые данные, как и описание. Также я могу подправить описание и продолжить выполнение.
Также можно предотвратить выполнение последующего кода просто выставив ошибку. Конвенционально предусмотрено, что функция не выполняется, если на входе ошибка. Вы, конечно, можете переопределить это поведение. Но по умолчанию это именно так.

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


Все те сложности, которые вы читали в "вышеприведенным тексте", находятся уровнем выше.

Прерывание выполнения такое же зло, как и оператор goto.
Вы Labview пользовали? или так, теоретические рассуждения?
UFO just landed and posted this here
ну. а причем тут Labview, если проблема в C#/C++ библиотеках? Я сам занимаюсь совмещением кода Labview и C#/C++. Тут еще и Visual Basic был. Чтобы не вылетало, нужно очень аккуратно с определением параметров функций обращаться. Есть нюансы, да.
UFO just landed and posted this here
Какими, например? Просто интересно.
UFO just landed and posted this here
А почему она должна? Обработка исключений в самой библиотеке как раз и есть та парадигма, которую тут считают совершенной. Кто, кроме разработчика библиотеки, знает лучше, как обрабатывать ошибки?
UFO just landed and posted this here

Во-первых, с чего бы прерывание выполнения было злом? Во-вторых, ваши мысли про C# такие же теоретические.

Ну выполняете вы процесс, а функция вычисления у вас выдает деление на ноль. Ну вот такое стечение обстоятельств. Что у вас происходит? Правильно :) программа выдает «прощай» и «свободен», программист не предусмотрел на это реакцию. А у вас управление технологическим процессом. Вот это и есть зло.
UFO just landed and posted this here
Создается впечатление, что вы под Labview ни разу не делали ни одного TCP соединения.
UFO just landed and posted this here

Вы так пишете, как будто LabView способна сделать что-нибудь умное в ситуации, когда программист деления на ноль не предусмотрел!

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

Ага, и в итоге принимается решение — "передвинуть конвейер на NaN метров", безо всяких "истерических" реакций...

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

Мы рассматриваем случай, когда программист забыл о всех проверках.


Это на порядки лучше, чем неожиданное завершение работы.

Во-первых, не факт что лучше.


Во-вторых, с чего это вы приравняли ранний выход из функции и завершение работы программы?

Даже если я забыл о проверках.
Что значит не факт???? у вас крутится цикл с обратной связью по управлению, а в модуле регистрации событий (вообще третичная функция) выпало необработанное исключение и программа упала. Это нормально?????
Здесь я вылизываю критичные секции и могу быть за них спокоен, а также могу быть спокоен на счет того, что ошибки в некритичных не уронят основной процесс только потому, что возникла ошибка, которую не смогли спровоцировать в процессе тестирования.
UFO just landed and posted this here
Что значит не факт????

То и значит. Представьте, что ошибка произошла не в третичной функции, а в самом цикле с обратной связью. Что лучше — аварийно остановиться или зависнуть в переходном процессе?


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

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

Ну вы хоть для начала поинтересуйтесь, что из себя представляет система разработки LabView. А потом теории выдвигайте. Вы бы еще разработчикам под Step7 эти претензии предъявляли.
Проблемы, которые вы описываете, отсутствуют как класс в данных средах. Потому что разработчики хорошо поработали над этим велосипедом.

За счёт какой магии этот класс проблем отсутствует?

За счет того, что обработка исключений реализована на уровне среды разработки и выполнения. А формат информации об ошибке не отдан на откуп разработчику ПО, а интегрирован в экосистему начиная с самого нижнего уровня. В любом месте, НЕ ПРЕРЫВАЯ ВЫПОЛНЕНИЯ, ошибку можно 1. зафиксировать ее факт. 2. выполнить действия 3. описать ее 4. сбросить, установить, скопировать. И что самое главное, прозрачно отдать на верхний уровень. И быть уверенным в том, что ее верхний уровень однозначно сможет обработать. И все это используя одну структуру. Эта структура является базовой конструкцией, такой-же, как и вещественные и строчные типы. Программист только ее заполняет нужными данными. Единое соглашение, в отличие от зоопарка, характерного для более гибких языков.
Поэтому в случае, если ID клиента не найден, я просто заполняю данную структуру с кодом ошибки и возвращаю как результат выполнения функции. Защищенные секции не требуются. Следующая функция может даже не знать (и не обязана знать), что там перед ней должно было быть вызвано, она вначале проверяет — есть ли входная ошибка или нет. Если нет ошибок, то выполняется основной код. И так далее по цепочке. В конце функционального блока можно посмотреть, была ли ошибка и какая. И решить, что с ней делать.
Это просто другой подход к программированию.
UFO just landed and posted this here
В любом месте, НЕ ПРЕРЫВАЯ ВЫПОЛНЕНИЯ, ошибку можно 1. зафиксировать ее факт. 2. выполнить действия 3. описать ее 4. сбросить, установить, скопировать. И что самое главное, прозрачно отдать на верхний уровень.

Пока не вижу отличий от других ЯП.


Эта структура является базовой конструкцией, такой-же, как и вещественные и строчные типы.

Ну-ну, учитывая что совсем недавно вы описали из чего эта структура состоит… Но отличий от других ЯП не вижу.


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

Не вижу отличий от других ЯП.


Защищенные секции не требуются.

Не уверен что это достоинство.


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

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


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

В C# так же, только "функциональный блок" заменяется на "try/catch".


Это просто другой подход к программированию.

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

Я вижу, вам очень интересно писать конструкции вида
error = func_A(a);
if (!error)
{ error = func_B(v);
if (!error)
{error = func_C©
if (!error)
{error = func_D()}
}}}

Тогда как предлагается
func_A(a, error);
func_B(v, error);
func_C(c, error);
func_D(error);
if (error->status) { printf («ошибка код %d, расшифровка %s», error->code, error->source) }

void func_B(int a, t_error *error)
{ if (!(error->status))
{

if (a == 0) {error->status = true; error->code = 232; error->source = «операнд равен нулю»; return}
}
}

что лучше? писать кучу «бесполезных» ifов в коде или вынести проверку в функцию?

Нет, я пишу вот так:


try {
    func_A(a);
    func_B(v);
    func_C(c);
    func_D();
} catch (SomeException ex) {
    Console.WriteLine($"ошибка код {ex.Code} расшифровка {ex.Message}");
}

Иногда ещё — вот так:


func_A(a)
    .Bind(() => func_B(v))
    .Bind(() => func_C(c))
    .Bind(() => func_D())
    .Catch(error => {
        Console.WriteLine($"ошибка код {error.Code} расшифровка {error.Message}");
    })
Хорошо. А если вам надо проигнорировать определенную ошибку (с конкретным кодом) на выходе func_B? т.е продолжить выполнение следующих функций. А остальные обработать.

В первом случае — вот так:


try {
    func_A(a);
    try { func_B(v); }
    catch (SomeException ex) when (ex.Code = 12345) { }
    func_C(c);
    func_D();
} catch (SomeException ex) {
    Console.WriteLine($"ошибка код {ex.Code} расшифровка {ex.Message}");
}

По сравнению с вашим вариантом — появился лишний отступ, но в целом многословности не прибавилось.


Во втором случае — вот так:


func_A(a)
    .Bind(() => func_B(v).Catch(error => {
        if (error.Code == 12345)
            return Result.Ok();
        return Result.Error(error);
    }))
    .Bind(() => func_C(c))
    .Bind(() => func_D())
    .Catch(error => {
        Console.WriteLine($"ошибка код {error.Code} расшифровка {error.Message}");
    })

Тут чуть многословнее вышло (всё-таки C# не Хаскель), но для часто встречающихся ситуаций можно свои функции написать, тогда вообще func_B(v).IgnoreError(12345) выйдет.

зачем каждый раз оборачивать в try catch, если надо просто проверить ошибку выполнения функции? Какой в сакральный смысл в событийности для последовательного выполнения?

Чтобы получить доступ к ошибке, конечно же.


А где вы тут событийность увидели?

событийность я вижу в том, что в момент формирования ошибки выполнение функции прерывается и управление переходит в секцию catch, минуя весь последующий код. В результате чего очень геморройно встраивать в код вещи, которые должны быть выполнены независимо от пользовательских ошибок вызываемых функций. Я уж не говорю про то, что при обертывании try catch большого объема кода, больше одного вызова функции, теряется информация о источнике исключения.
UFO just landed and posted this here
Ну вы же сами понимаете, какой монстр в результате получается. И по объему кода и по уровню восприятия таких конструкций.
Ну вы же сами понимаете, какой монстр в результате получается. И по объему кода и по уровню восприятия таких конструкций.
А для этого в С# есть using и в последних версиях ещё и однострочный его вариант
UFO just landed and posted this here

В нормальных языках есть scoped переменные у которых финализатор вызывается при выходе из скоупа и можно задавать хуки на выход из скоупа.

UFO just landed and posted this here

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

UFO just landed and posted this here

О, в LabView тоже изобрели Either. Передача ошибки/результата по цепочке обработчиков это задача метода map определенного для монады Either или Maybe. Удобство в том, что обработчики не должны переживать что значения может не быть — за них проверку сделает map
Вот из Elm, например, сигнатура
Maybe.map : (a -> b) -> Maybe a -> Maybe b
Обработчик просто переделывает a в b. И таких обработчиков можно повесить подряд сколько угодно.

На самом деле там изобрели гошный if err != nil return err, только чуть сложнее потому что err тут — зачем-то входной параметр; до Either их решение не дотягивает.

Изобрели его еще в 90-х. Как минимум версия Labview 4.0 в 1995 году работала с таким обработчиком ошибок. Причем с тех пор в неизменном виде.
Можете минусить сколько угодно. Странно только, что вы недовольны тем, что что-то просто существует. И главное спорить с тем, что десятилетиями используется.
Можете минусить сколько угодно. Странно только, что вы недовольны тем, что что-то просто существует. И главное спорить с тем, что десятилетиями используется.

И главное спорить с тем, что десятилетиями используется.

COBOL существует очень давно, но сегодня писать на нём что-то новое я бы не стал.

Версии Labview выходят каждые пол-года. Сначала релиз, потом с сервис-паками. Поэтому с COBOL не сравнивайте. Сообщество разработчиков насчитывает не менее сотни тысяч человек. Это лучшая система разработки для автоматизации производства, является промышленным стандартом в США. Ближайший к нему Сименс, но по удобству проектирования он катастрофически проигрывает.

Но это всё не делает обработку ошибок в стиле 90х годов чем-то хорошим. Я готов признать, что для 95го года такая обработка ошибок и правда была чем-то передовым. Но с того времени прошло 25 лет!

Обоснуй, в чем try catch пользовательских ошибок лучше анализа обычной структуры с кодом и описанием ошибки. Давай, чтобы было по-передовому.
UFO just landed and posted this here
В пределах однопоточного последовательно выполняемого кода ЗАЧЕМ try catch городить для обработки пользовательских (имеется в виду ошибок, определяемых разработчиком функции) ошибок? Вот ответ на какой вопрос я пытаюсь получить.
UFO just landed and posted this here
Вы удивитесь, но под Labview я тоже пишу многопоточные приложения, причем любой цикл, включающий хоть один вызов функции, АВТОМАТИЧЕСКИ оформляется потоком. Но это не отменяет последовательную обработку ошибок внутри потока, существенно упрощая ее восприятие.
Вам ведь знакома система контроля ошибок в Labview. Про какой стек вы говорите? Внутри функции проверяется входная ошибка. Классический входной контроль. Обработчик ошибок, если вообще есть или нужен, выполняется в конце потока. И можно быть уверенным в том, что последовательность операций внутри потока будет выполнена всегда, независимо от ошибок. Это тоже бывает очень важно. И сильно влияет на качество программного кода.
UFO just landed and posted this here

Хотя бы тем, что try/catch раскрывается компилятором, а структуру вы вручную всюду прокидываете. Но и try/catch — не вершина обработки ошибок, в 2020м году можно ещё лучше сделать.

Но с того времени прошло 25 лет!

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

Монады — они четвертьвековой давности только как концепция. Реализация у них всё это время улучшалась.

Так громко я уже несколько лет не ржал.


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


Само понятие «монады» ввел Роджер Гудмент в конце пятидесятых, спустя примерно декаду после первых публикаций Маклейна и Эйленберга по теории категорий.


И реализация с тех пор не изменилась вообще никак. Монада — слишком тривиальная сущность, там нечего улучшать.

Так громко я уже несколько лет не ржал.

Извините, что вмешиваюсь в монолог умного человека, но просто любопытно стало: что смешного в том, что кто-то где-то (возможно) заблуждается? Или для вас случаи, когда вы что-то знаете лучше других — некий повод самоутвердиться? Вам не хватает уверенности в себе? У вас детские психологические травмы, подростковые конфликты, непонимание со стороны родителей? Хотите поговорить об этом? ;)
Извините, что вмешиваюсь в монолог умного человека [...]

Ничего страшного, мне не привыкать.


что смешного в том, что кто-то где-то (возможно) заблуждается?

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


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


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

с чрезвычайным апломбом

Я не знаю, где вы узрели апломб, тем более чрезвычайный, в обычной фразе «Монады — они четвертьвековой давности только как концепция. Реализация у них всё это время улучшалась». Особенно если учесть, что она действительно вполне себе верна. Мы же не про теорию категорий говорим, а про прикладную реализацию монад в программировании, которой действительно, ну пусть не 25 лет, но 30.
А самое главное, вещи вроде
Так громко я уже несколько лет не ржал.

… не чморят тех, кому вы их адресуете. А вот вас — чморят. В разговоре людей, первый из которых ведёт себя крайне токсично, публика слушает второго. Даже если первый и прав-то на самом деле.
В разговоре людей, первый из которых ведёт себя крайне токсично, публика слушает второго.

Это не совсем так.Большинство — да, действительно слушает пушистых заек (простите, но слово «токсично» в русском — да и английском — языке в таком контексте использовать грамотный человек не станет).


Зато узкий круг лиц небезнадежных мне лично интересных — слушает именно что меня, а не наоборот. А на остальных леммингов мне насрать.


Если бы вы были правы — ни Маяковского, ни Есенина мы бы никогда не услышали, а про Серебряный век знали бы исключительно по вялой рифмовке Орешина и Радимова.


Мы же не про теорию категорий говорим, а про прикладную реализацию монад в программировании,

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


не чморят тех, кому вы их адресуете. А вот вас — чморят.

В ваших глазах — возможно, я не претендую, да и пофиг. Но не нужно говорить за всю сеть.

Зато узкий круг лиц небезнадежных мне лично интересных — слушает именно что меня, а не наоборот.

Это должен быть очень, очень узкий круг.
Но не нужно говорить за всю сеть.

Я за всю сеть и не говорю. Я говорю сугубо за воспитанную часть её пользователей.
Это должен быть очень, очень узкий круг.

Не слишком широкий, да.


Я говорю сугубо за воспитанную часть её пользователей.

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


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

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


Например, в том же Хаскеле до 2014 года монады не были аппликативными функторами, как бы смешно это не звучало с точки зрения теорката.

Треугольником со сторонами 3,4,5 прямой угол строят больше двух тысяч лет.

Утверждать, что исключение это плохо – не правильно. Все зависит от контекста.

Например: при реализации Web API если по userId не найден пользователь и нужно просто отдать в ответ http-код 404. В таком варианте выброс UserNotFoundException и перехват его в фильтре с преобразованием в HttpNotFoundResult — будет самым правильным вариантом. Вам не придется пробрасывать результат через весь колстек.

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

Просто выбрано неудачное название Mayby… Обычно такую реализацию я делаю в виде дженерика OperationResult.

Раньше я писал Exception'ы, выстраивал цепочки и радовался жизни, перехватывая их через самописный Middleware и оборачивая их в ProblemDetails. А затем я посмотрел сколько RPS теряю на throw new Exception("Hello world!") и мне стало максимально грустно. Терять в 10 раз RPS, только потому что обработка исключения действительно дорогое удовольствие, это очень грустно.


Сначала, я пробовал разные методики, например, которые мне встречались в PInvoke. Если null — то нету, если есть — значит есть. Отсюда и *Default() пошли. Но этого было мало. Логика росла, валидация убивала смысл жизни, а затем я пришел к такому простенькому интерфейсу, а там уже реализация, которая принимала все в себя через конструктор new InteractionResult(result/errors/exception/validationResult)


public interface IInteractionResult
    {
        bool Succeeded { get; }

        IDictionary<string, string[]> Errors { get; }
    }

    public interface IInteractionResult<TResult> : IInteractionResult
        where TResult : class
    {
        TResult Result { get; }
    }

Честно подсмотренная практика у ASP.NET Core Identity. И неважно что здесь она Maybe, в комментариях выше отсыллки Either. Важно, что она решает проблемы и достаточно легко реализуемая конструкция.


Exception'ы это как уровень Error или Critical (нет) в вашем инструменте логирования. Если вы ожидаете, что пользователь может вам недодать информации — это Warning. Скажите вежливо, что вы не можете это сделать, объяснив вежливо почему. Но если произошло действительно что-то необратимое/необрабатываемое, тут уже стоит выкинуть соотвествующий Exception.

> Я принес вам решение проблемы с исключениями в C#

И где же решение проблемы? Вы просто перечислили, что уже и так известно: try/catch, монада, кортеж <result, error>, Null(Default)Object.

В других языках тоже нет единого соглашения, каждый делает как хочет.

В других языках тоже нет единого соглашения, каждый делает как хочет.

В других мейнстримных ООП-языках, вы хотели сказать. В Haskell, Scala, Rust как раз используют Either или вариации на эту тему.

I am the god of hellfire and i bring you fire (извините)
Я ждал, когда кто-то заговорит об этом. На мой взгляд, rust сейчас самый стабильный язык из тех, с которыми я работал. На нем почти реализованы монады, но при этом, нет исключений, которые непонятно зачем сделали в хаскеле. В итоге, я всегда знаю, какая функция может не вернуть значение – Option (Maybe), либо же она может вернуть ошибку – Result<T, Error> (Either). Мне не нужно делать пометок в стиле «throws ...» как в джаве. Мне не нужно заглядывать внутрь метода и разбираться, какие исключения отлавливать в блоке catch, я знаю заранее, какую ошибку (одну!) может вернуть функция. Если функция в расте возвращает u32, значет она возвратит его всегда.
Если функция в расте возвращает u32, значет она возвратит его всегда.

fn i_return_u32_honestly() -> u32 {
    panic!("lol")
}
UFO just landed and posted this here

Если смотреть с этой стороны, то не понятно в чём же отличие от Хаскеля, который был признан комментатором выше "языком с непонятно зачем сделанными исключениями".

UFO just landed and posted this here
В том и отличие, что в хаскеле есть исключения, а в расте нет. То-есть, в расте я могу по сигнатуре функции понять, возможна ли ошибка или нет.
Например, функция из стандартной библиотеки хаскеля:
read :: Read a => String -> a

Ничего в сигнатуре не говорит о том, что может вылететь иключение (хоть функция и чистая):
read "five"::Int
*** Exception: Prelude.read: no parse

Вам выше показали функцию i_return_u32_honestly. Что именно в её сигнатуре говорит вам о том, что там может вылететь паника?


И где отличие Хаскеля и Раста?

UFO just landed and posted this here

Ну, вообще-то есть, если паника реализована через раскрутку стека и если использовать std::panic::catch_unwind.

я трачу кучу времени на то, чтобы решить, как быть, если что-то идёт не по плану

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

Для себя решил так:
Исключения — только для исключительных ситуаций. Как только появляется поведение, не предусмотренное ходом выполнения программы — кидаем исключение.
Для всего остального — монады из C# Functional Programming Language Extensions
Там есть и Option<>, и Either<,>, и куча всего другого, что позволяет не изобретать велосипед.
При этом не вижу ничего плохого возвращать хоть null, хоть самописный класс, если это позволяет определять ожидаемое поведение и обрабатывать соответствующие ошибки (null/не null в некоторых случаях может быть аналогичен Option<>, в самописных классах обычно делают поля и для результата, и для ошибки, что аналогично Either<,>).

В примере с условным чтением файла:
Если мы ожидаем, что файл всегда должен существовать и без этого ничего дальше работать не может, то кидаем исключение. Как правило, это файлы сборок, конфигурационные файлы и другие файлы, без которых работа приложения невозможна.
Если мы допускаем, что файла может не быть, то либо Option<> (если причина нас не интересует), либо Either<,>. Например, загружаемые пользователем файлы, временные файлы, сгенерированные программой

В основе «классического» подхода с собственными исключениям лежит отсутствие в первых версиях языка LINQ, лямбда-выражений и другой функциональщины. Поэтому и рекомендовался подход с созданием собственных исключений (т.е. при ожидании отсутствия файла нужно на самом деле кидать своё исключение, а не FileNotFoundException). С развитием языка появилось больше функциональных возможностей, в т.ч. и для использования монад. Поменялась и «классика» — теперь нужно просто брать лучшее у каждого из подходов.
Исключения кидать нужно только в исключительных ситуациях, когда логика не предполагает это «исключительное» состояние совсем. И API нужно делать соответствующее, например, если предполагается что User не будет найден можно написать так:
IEnumerable<User> FindUsers(string id);

TryGet – хороший подход когда критична производительность и потребление памяти.
User? – не очень удобно так Nullable типы все равно нужно проверять на null, а они потенциальный источник больших проблем.
На мой взгляд код должен использовать обычные типы что бы поддержать любые «разумно» возможные сценарии. Maybe – наверно подойдет в каких то сценариях, но опять же, в негативном, как и в позитивном сценарии, он может содержать еще какие то данные.
У Exception в .NET есть дополнительная функциональность — они сохраняют stack trace
Это ни как не относится к обработке ошибок на прямую.
Но это оооочень полезно когда нужно понять где именно произошла ошибка.

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

А может кто-нибудь бывалый мне объяснить, что плохого в использовании исключений как средства выхода из success flow?
Потому что мне доводилось, к примеру, писать код на чистом WinAPI, без их использования, и программа в итоге превращалась в размазню из вложенных if'ов. При использовании подхода в виде Maybe/Either, как мне кажется, будет в точности то же самое. Так что преимуществ у него мало, ну, кроме возможности заставить клиентский код проверить успешность. И то это максимум защита от дурака, но не от инициативного идиота.
Что касается исключений, то тут всплывает ещё один вопрос: ИМХО, не так важно разделение ошибок на ожидаемые/неожиданные, как их разделение на фатальные/исправимые. Т.е. важно, можем/должны ли мы что-то поделать с конкретной ошибкой, в то время как всё остальное должно максимум регистрироваться и отдаваться «наверх» (catch-log-rethrow). Если же регистрация на данном уровне не предусмотрена, то и ловить не требуется.
А раз так, то может иметь смысл явно обрабатывать только исправимые ошибки, что в исключениях достигается селективными catch.
Да, остаётся проблема того, что фатальность/исправимость зависит от контекста. Если ошибка является исправимой для конкретного случая, могут потребоваться вложенные try-блоки, да и исключения будут летать напрасно…
UFO just landed and posted this here
Гм, ну с Хаскелем мне работать не доводилось, я не в курсе как именно там происходит обработка ошибок. Почитаю при случае, спасибо.

А подскажите, как это под капотом работает? Ну то есть допустим parseDate вернул ParseError, а дальше что происходит?

Ничего не происходит, этот ParseError сразу же возвращается из parseDateTime.


Потому что компилятор преобразует код выше во что-то вроде вот этого:


parseDateTime str = parseDate str >>= (\(date, afterDate) -> ...)

а операция >>= для типа Either определена вот так:


(Left x) >>= fn = Left x
(Right y) >>= fn = fn y

То есть автоматический возврат результата, если его тип несовпадает с типом переменной куда мы хотим его записать? А если возвращаемый тип parseDateTime объявлен как Either Int DateTime?

Никакого несовпадения типов быть не может, это ж Хаскель. В монаде Either "левый" тип считается типом ошибки, а "правый" — типом значения. Функция может вернуть либо значение, либо ошибку. Значение записывается в переменную. ошибка возвращается сразу же.


А если возвращаемый тип parseDateTime объявлен как Either Int DateTime?

То её нельзя будет использовать как показано выше. Но можно будет использовать вот так:


parseDateTime :: String -> Either Int DateTime
parseDateTime str = do
  date <- parseDate str
  pure date
Исключение позволяет разрушить нужный контекст, в случае монады разрушение этого контекста откладывается, более того, его придется контролировать результат на каждом уровне. (привет WinApi)
Условно есть CarService.GetOwnedCars(User user), RetailService.GetHouses(User user)
которые в каких-то разных местах вызываются с user'ом и придется всегда знать о какой-то начальной точке где надо проверить юзера и не создавать/ходить в эти сервисы, либо проверять каждый раз user'а внутри этих сервисов, что крайне усложняет поддержку кода
Исключение позволяет всего этого избежать и разрушить контекст немедленно, приведенная монада c case это точно такое же отложенное разрушение контекста как c кодом ошибки.

Научитесь писать контракт модуля обдуманно и не изобретайте велосипедов.

В своем проекте, когда метод может вернуть null, всегда помечали его как CanBeNull, если не помечен, значит вернется либо exception либо валидный результат. И если возвращается exception разрушаем контекст вызова до определенного уровня. А вместе разрушения неважно, то ли юзера нет в базе, то ли база упала — неважно, важно что у контекста нет валидных данных для продолжения работы, по незявисящим от него причинам.

Как писали выше, любой метод в C# теоретически может кинуть любое исключение, поэтому дефолтные обработчики исключений обычно присутствуют, просто чтобы приложение не завалилось от какого-нибудь NullReferenceException. А дальше уже изыски в зависимости от требований — а чего, собственно, вам нужно от ошибок/исключений?


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


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


  1. Ошибки, допустимые бизнес-логикой. То есть систему так сконфигурировали, что при обработке запроса возникает ошибка (например, пользователя выключили в настройках, а он пробует залогиниться).
  2. Ошибки из-за неправильного внешнего запроса. То есть снаружи (от клиента) прилетел запрос, который не должен прилетать с точки зрения бизнес-логики. Например, вызов GetUser(userId) с передачей несуществующего userId. Или запрос к HTTP API в непредусмотренном формате. Или обращение к чему-то, что запрещено правами доступа.
  3. Остальные ошибки — например, ошибка выполнения запроса к БД (отсутствие соединения, таймаут). NullReferenceException из-за косяка программиста. StackOverflowException из-за избыточного чтения stackoverflow.com.

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


  1. Ошибки, допустимые бизнес-логикой, возникают из-за пользователей — это они так настроили систему.
  2. Ошибки из-за неправильных запросов возникают по вине клиентских приложений.
  3. Остальные ошибки возникают по вине сервера.

Соответственно, в логах сервера:


  1. Ошибки, допустимые бизнес-логикой, пишутся с уровнем INFO и без трассировки стека.
  2. Ошибки неправильных запросов пишутся с уровнем WARN, трассировка стека по желанию самого исключения (флажок withStackTrace в конструкторе), но обычно трассировки нет.
  3. Остальные ошибки пишутся с уровнем ERROR с трассировкой стека.

Точка зрения сервера: на ошибки 1-го и 2-го типа он повлиять не может, а ошибки 3-го типа должен минимизировать, это его зона ответственности.
Точка зрения клиентского приложения: на ошибки 1-го и 3-го типа (которые возвращает сервер), оно повлиять не может, а ошибки 2-го типа должно минимизировать, это его зона ответственности.


У нас далеко не хайлоад, и профайлер не показывает затраты на выбрасывание исключений как критичные. Пока что :)


P.S. Чем больше копится опыт, тем больше понимаю, что главные проблемы в программировании — не технические, а проблемы бизнеса, которые ваш код должен решать.

Попробовал реализовать подход с монадами, на основе перегрузки операторов. Мне кажется результат лаконичнее чем на основе наследования.
Код на Паскале, но думаю перевести его C# не составит труда.

Определение монады на основе Record (Value type в терминологии .Net):
type
  Monad = class sealed
  public type
    Result<TSuccess, TFailure> = record
    private
      FIsSuccess: Boolean;
      FSuccess: TSuccess;
      FFailure: TFailure;
    public
      class function Success(const Value: TSuccess): Result<TSuccess, TFailure>; static; inline;
      class function Failure(const Value: TFailure): Result<TSuccess, TFailure>; static; inline;
    public
      class operator Implicit(const R: Result<TSuccess, TFailure>): Boolean; static; inline;
      class operator Explicit(const R: Result<TSuccess, TFailure>): TSuccess; static; inline;
      class operator Explicit(const R: Result<TSuccess, TFailure>): TFailure; static; inline;
    end;
  end;


Идея в том, что тип Monad.Result реализует неявное преобразование к Boolean в зависимости от успеха операции. И явное преобразование в типы TSuccess и TFailure.
Обертка в класс Monad — костыль для красоты именования, т.к. нормальных Namespace в Delphi так и не сделали.

Реализация:
class function Monad.Result<TSuccess, TFailure>.Success(const Value: TSuccess): Result<TSuccess, TFailure>;
begin
  Result.FIsSuccess := True;
  Result.FSuccess := Value;
end;

class function Monad.Result<TSuccess, TFailure>.Failure(const Value: TFailure): Result<TSuccess, TFailure>;
begin
  Result.FIsSuccess := False;
  Result.FFailure := Value;
end;

class operator Monad.Result<TSuccess, TFailure>.Explicit(const R: Result<TSuccess, TFailure>): TFailure;
begin
  if R.FIsSuccess then
    raise Exception.Create('Invalid cast for success');

  Result := R.FFailure;
end;

class operator Monad.Result<TSuccess, TFailure>.Explicit(const R: Result<TSuccess, TFailure>): TSuccess;
begin
  if not R.FIsSuccess then
    raise Exception.Create('Invalid cast for failure');

  Result := R.FSuccess;
end;

class operator Monad.Result<TSuccess, TFailure>.Implicit(const R: Result<TSuccess, TFailure>): Boolean;
begin
  Result := R.FIsSucces;
end;


Реализация метода:
function GetUser(const Id: string): Monad.Result<TUser, string>;
begin
  if UserStorage.DefaultUser.Id = Id then
    Result := Monad.Result<TUser, string>.Success(UserStorage.DefaultUser)
  else
    Result := Monad.Result<TUser, string>.Failure('User no found');
end;

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

Использование:
procedure Test;
var
  Result: Monad.Result<TUser, string>;
begin
  Result := GetUser(1);

  if Result then
    WriteLn(TUser(Result).Name)
  else
    WriteLn(string(Result));
end;


Из недостатков такого подхода – возвращаемые типы для успеха и не успеха не должны совпадать, т.к. результат приведения типа будет не определен. Тоже и для типа Boolean — будет конфликт с неявным приведением.
Обертка в класс Monad — костыль для красоты именования, т.к. нормальных Namespace в Delphi так и не сделали.

Там же модули есть.

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

unit — это и есть модуль


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

А почему это проблема?

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

А теперь предметно. Представьте, что у вас нет
Int32.TryParse(...)
а есть только
Int32.Parse(...)
Int32.TryParse — это правильный API, который заставляет программиста задуматься о негативном сценарии и его последствиях и решить что делать дальше здесь и сейчас – придумать альтернативный сценарий с учетом всех возможных деталей, добавить логирование этих деталей и т.п. Использовать Int32.Parse намного проще, но в конечном итоге решение что делать просто откладывается на потом. По сути — это долг. Никто обычно не делает исключения со всеми деталями, а текст ошибки и стэк не сильно поможет. То есть, там, где вы все-таки поймаете исключение у вас не будет контекста — деталей при каких возникла эта исключительная ситуация и не будет возможности точно компенсировать «урон». Я уже не говорю о катастрофическом падении производительности если исключения кидаются горячим кодом. Понятно, что всего не предугадаешь, и если происходит что-то неординарное, редкое, что ваш код не предполагал, то тут помогут исключения, пока вы не напишете логику по обработке этой ситуации (и даже возможно сделаете его частью вашего API) или посчитаете это сценарий незначительным, редким, не опасным или/и не предполагающим негативных последствий. К тому же если хочется гарантировать «железобетонность» логики, то модульные тесты помогут это сделать, а исключения гораздо сложнее тестировать, на мой взгляд. И тут уже нужны интеграционные тесты, вариативность которых может зашкаливать. Количество негативных ситуаций, которые нужно обработать в каком-то модуле будет зависеть от других модулей и будет непредсказуемым и меняется с изменением кода в других модулях, так как исключения — не часть API (в C#). Но в целом — кидать исключения и ловить их потом (или не ловить) проще и часто этого достаточно.

Я пытаюсь представить себе, что было бы если бы Windows API или WinRT API был написан полагаясь на исключения.
Не понятно, за что тут минусят, я высказал свое мнение в предыдущем комментарии, если оно не совпадает с чьим, то другим, то нужно ставить минус? Буду иметь ввиду.

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


Там вообще 1 минус, почему вы не рассматриваете вариант, что кто-то просто случайно ткнул? Или что он один такой на весь Хабр, а все остальные с вами согласны?

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


Аргументы это основа конструктивной дискуссии, разве не так? Почему не рассматриваю и откуда такой вывод?

Потому что написали "если оно не совпадает с чьим, то другим, то нужно ставить минус?". Где тут рассмотрение варианта, что ваши аргументы неправильные? Рассмотрения нет, значит не рассматриваете. А если бы расссматривали, то написали бы "если я не прав, то нужно ставить минус?", и сами бы поняли, что да, минусы в этом случае обоснованы.


Если оно не совпадает с чьим-то другим, то минус ставить не нужно. А если человек не прав, то нужно. Я вам минус не ставил, если что.


Аргументы это основа конструктивной дискуссии, разве не так?

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

Да, признаюсь, я не учел вероятность того, что человек мог быть не согласен с моими аргументами, а с мнением он мог быть даже и согласен, поэтому в фразе «я высказал свое мнение в предыдущем комментарии» я опустил слово «аргументы». Но вы учли это нюанс. И мне было бы интересно ваше мнение (и аргументы) по предмету дискуссии – «решение проблемы с исключениями в C#».

Я считаю, что исключения надо использовать в исключительных ситуациях, но с вашим решением "IEnumerable<User> FindUsers(string id)" я не согласен.
Вы пишете "Nullable типы все равно нужно проверять на null", но в вашем решении вместо этого надо проверять на длину списка. Разницы никакой, зато код менее понятен, списки и множественное число при получении по id, который по определению идентифицирует одну запись.


Правильнее всего, на мой взгляд, если функция получения пользователя по id будет возвращать User|null, при этом переменная типа User не может быть null. В таком варианте поведение функции понятно из контракта, и нет проблемы "а чего она там выбрасывает", как и проблемы передать null туда, где требуется User.

Конечно же, я не претендую на то, что предложенное мной вариант хорош во всех отношениях, просто, в каких-то случаях я бы сделал так, ну а в каких-то нет. Его достоинство в том, что сигнатура метода и его название подчеркивают, что будет происходить поиск User-а по Id и, конечно, не гарантируется что User будет найден. Эта функция вернет перечисление. Оно не имеет длины. IEnumerable<> отлично поддерживается синтаксисом языка foreach, linq и кучей библиотек и расширений, то есть будет просто строить цепочки типа поискать пользователя, поискать всех его друзей и т.п. без «If». Из минусов: 1. Эта функция может вернуть более одного User-a. 2. Создаст минимум два объекта в куче — пострадает производительность и GC. Соответственно, я не стал бы использовать этот подход, когда последние два фактора особенно важны, а использовал подход, вероятно, предложенный вами. Я не понял эту фразу «User|null, при этом переменная типа User не может быть null» Что значит “User|null”? И что означает «передать null туда, где требуется User»? В итоге, как выглядит метод, который предлагаете вы?

User|null это union type, в некоторых языках для union с null есть специальная форма User?.


что означает «передать null туда, где требуется User»?
как выглядит метод, который предлагаете вы?

function getUser(string id): ?User { ... }
function f(User user) { ... }

$user = $this->getUser($id);  // null
f($user);  // error!

В PHP будет ошибка при при первой же попытке передать null туда, где требуется объект, а не NullReferenceException где-нибудь далеко от неправильного кода при первой попытке вызвать метод класса User.


Из минусов

Самый главный минус — непонятный код. Нам не нужен список, нам нужен объект пользователя.


функция вернет перечисление. Оно не имеет длины

Ну и как тогда проверить, найден ли пользователь. чтобы показать ошибку 404 not found если нет?

Данный пост про C# и это понятно, что другие языки имеют свои хорошие решения этой ситуации. Да и в .NET принято проверять входные параметры, так что передача null куда либо, вероятнее всего, вызовет исключение не очень далеко. Я пытаюсь показать, что ошибку тут можно трактовать не как ошибку, а как еще один альтернативный сценарий того, что пользователя просто не удалось найти. Поэтому вместо метода GetUser предлагаю использовать FindUsers. Пользователь метода точно подумает об альтернативных сценариях и сможет осознано решить, что делать дальше: 404 или попытаться поискать в другом месте или поискать позже и т.п. Чтобы выдать 404:
var user = FindUsers(id).SingleOrDefault();
if (user == null) {
 // 404
}

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

Ну так вы же все равно проверяете на null, значит разницы с Nullable типами нет, я об этом и сказал. Совокупность FindUsers+SingleOrDefault это фактически и есть GetUser.

Важная деталь: «пользователь метода точно подумает об альтернативных сценариях и сможет осознано решить, что делать дальше». В данном случае нужно было просто выдать 404:
Ну и как тогда проверить, найден ли пользователь. чтобы показать ошибку 404 not found если нет?
Проблема в таком высокоуровневом коде в том, что какая то более низкоуровневая приблуда, которая использовалась в GetUser, все равно вызовет исключение или нет. То есть уже будет источником такой же проблемы, как бы не реализовывался GetUser вопрос все равно упирается в документацию и принципы принятые при написании кода.

В новых сервисах включаю nullable reference, повышаю важность до компайл еррор и живу почти счастливо счастливо в стиле GetOrDefault. Эксепшены кидаю только в совсем уж вопиющих случаях типа _config = config ?? throw new ArgumentNull…
Ну или выкидываю ApplicationException, если хочу показать подробности наружу. (Пишу бэк).

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

Дык foreach — это de facto функтор же, отец ааппликатива и дед монады. Только неуклюжий и подкостыленный. Чем он «проще и привычнее монады»?

Чем он «проще и привычнее монады»?
Тем, что foreach есть в каждом языке, а что такое монада, знают полтора человека на десять тысяч разработчиков. Как-то так.
foreach есть в каждом языке

Citation needed. В эрланге, например, принципиально циклов нет, чтобы не тащить процедурное говно в язык.


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

Ну в 2020 году это просто означает, что 9998½ их них надо гнать из профессии ссаными тряпками.

Citation needed. В эрланге, например, принципиально циклов нет, чтобы не тащить процедурное говно в язык.
Эрланг знает еще меньше программистов, чем полтора на 10k. Статистика в пределах погрешности.
Ну в 2020 году это просто означает, что 9998½ их них надо гнать из профессии ссаными тряпками.

Боюсь, вам будет очень неудобно жить без ребят, создающих и поддерживающих 99% софта, которым вы пользуетесь.

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

Так не пользуйтесь. Посмотрел ваши комменты — полные тщеславия. Хотя ничего умного вы не пишите, на мой взгляд просто повторяете то что где то прочитали без понимания, зачем это нужно. Видимо это дает вам ощущение интеллектуального превосходства.)
Применения монад не делает вас более профессиональным программистом, только порождает переусложненный код в котором трудно разобраться. Намного сложнее написать простую систему которая отбрасывает все не существенные детали и делает только что нужно, нежели сложную систему которая пытается учитывать все возможные случаи — тут все по книжечке хаскель/монады -> http клинет для сбора и парсинга json, через неделю а может и месяц… Зато учитывает все ситуации, вопрос кому это нужно?)
Хотите строить идеальную модель занимайтесь НИР, только не нужно говорить что все программисты решающие реальные проблемы — быдло...

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


Спасибо.

UFO just landed and posted this here
Ну в 2020 году это просто означает, что 9998½ их них надо гнать из профессии ссаными тряпками.

Есть проблема, есть решение проблемы. Возможно, есть несколько вариантов решения проблемы. Какого… я должен знать все возможные варианты решения проблем, если у меня и так соблюдается критерий разумной достаточности? Или человеку с молотком везде мерещатся гвозди?
UFO just landed and posted this here
Чтобы хотя бы знать, что он на самом деле соблюдается.

Деньги? При общей стоимости 1 млн денег, если решение X стоит 10 денег, а решение Y будет стоить 9.95 или даже 1.01 денег — без разницы, потому что обсуждение будет стоит дороже.

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


И это, как вы тут риски посчитали?

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

Логика простая — улучшать можно до бесконечности. Это «улучшение» стоят каких-то ресурсов (временных или материальных), которые конечны.
Мы можем считать число Пи до 3..5 знаков после запятой, а можем считать до сотен. Это критично для водопровода или нет?
Берем любое рандомное или спроектированное решение — считаем его «цену». Смотрим — «цена» нас устраивает или нет? Если устраивает и есть еще ресурсы, которые мы можем позволить на «улучшение улучшения» и «найти оптимательнее»- можем сделать еще пару итераций. Нашли лучшее? отлично. Нет? ну может быть потом.
В некоторых (критичных) областях разница может достигать тысяч процентов.

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

Деньгами :) Достаточно прикинуть сколько «стоит» написание коментария тут. Умножить на среднюю цену разработчика мид\сеньер — примерный уровень людей обладающих реальным опытом. Понять что к однозначному решению не пришли :) Равно как и решения, которые были реализованы 10..20 лет назад и с тех пор не сильно что-то поменялось :)
UFO just landed and posted this here
Ну с такой логикой можно и ещё «проще и привычнее» способ взять: прсто дать программисту возможность напрямую с БД работать при помощи банального select и тогда ему ещё и о куче других вещей придётся волей-неволей подумать
Есть разные уровни абстракции. Тот, кто пишет реализацию метода GetUser(), как раз и работает с select, надо полагать. А в статье-то речь идет о DX пользователей этого API.
UFO just landed and posted this here

В случае с асинхронными методами мне приглянулся класс (вернее — struct) "CondirionalValue" от Майкрософт:
https://docs.microsoft.com/en-us/dotnet/api/microsoft.servicefabric.data.conditionalvalue-1?view=azure-dotnet


Я его перенял и немного переименовал:


public struct ConditionalObject<TValue>
    {
        /// <summary>
        /// Initializes a new instance of the <cref name="ConditionalObject{TValue}" /> class with the given value.
        /// </summary>
        /// <param name="hasValue">Indicates whether the value is valid.</param>
        /// <param name="value">The value.</param>
        public ConditionalObject(bool hasValue, TValue value)
        {
            HasValue = hasValue;
            Value = value;
        }

        /// <summary>
        /// Gets a value indicating whether the current <cref name="ConditionalObject{TValue}" /> object has a valid value of its underlying type.
        /// </summary>
        /// <returns><languageKeyword>true</languageKeyword>: Value is valid, <languageKeyword>false</languageKeyword> otherwise.</returns>
        public bool HasValue { get; }

        /// <summary>
        /// Gets the value of the current <cref name="ConditionalObject{TValue}" /> object if it has been assigned a valid underlying value.
        /// </summary>
        /// <returns>The value of the object. If HasValue is <languageKeyword>false</languageKeyword>, returns the default value for type of the TValue parameter.</returns>
        public TValue Value { get; }
    }

О паттерне "MayBe" я впервые узнал от Марка Зимана: Ссылка: The Maybe functor из его курса Encapsulation and SOLID

Фактически вы Nullable реализовали )

exactly… Во-первых, тут достаточно struct, во вторых, решение с вводом дополнительных классов "Success" и "Failure" мне кажется неоправданным

Ну, ваше решение не отвечает на вопрос, а в чем же именно ошибка, и тоже не мешает обратиться к несуществующему Value.

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

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

Интересно, а топикстартер с вилками бороться не пробовал?
А то они острые — чуть промахнёшься мимо рта, или занесешь в рот с чуть повышенной скоростью — et voila — 4-ре дырки!