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



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

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

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

    Возьму простой пример. Допустим у нас есть сервис, который отдаёт нам модель юзера по Id.

    Вот такой:



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

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

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


    Тут все просто. Метод ищет пользователя, если, не находит — выплевывает исключение.

    Примерно так:



    Пользоваться таким методом можно вот так:



    Плюсы подхода очевидны.

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

    Но минусов — тьма. И просто закрыть глаза на них — не получится.

    • Никаких гарантий. Вообще никаких. Человек который использует твой сервис, может даже и не подумать о том, что тут что-то надо обрабатывать. Или обработает не все возможные. Если их не обработают на месте, а обработают выше, в том коде, который на такие исключения не рассчитан — могут возникнуть достаточно большие проблемы.
    • Тебе придется скрупулёзно писать и обновлять документацию такого метода, и компилятор тебе не будет гарантировать, что ты описал всё, что выплевываешь.
    • Очень плохо подходит для случаев, когда вызывающей стороне не нужно знать, почему произошла проблема.
    • В доменном коде тебе постоянно нужно будет добавлять свои типы исключений, они раздувают кодовую базу, часто дублируются и требуют поддержки.
    • Когда метод потенциально выплевывает пять шесть типов исключений, код превращается в нечитаемое говно — и код метода, и код использования.
    • В сишарпе принято использовать интерфейсы. Если есть какой-то сервис, и есть код, который его использует, то мы в этом коде работаем с интерфейсом сервиса. Так вот выкидывается исключение или нет — интерфейс это не определяет НИКАК. Т.е. если кто-то написал класс, который имплементит такой-то интерфейс, и внутри этого класса он выкидывает исключение, и он даже внес это в доку класса — я при использовании этого кода об этом НЕ УЗНАЮ.

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

    Ещё один распространненый способ разруливать это — try pattern.


    Идея завязана на out параметры в сишарпе. Выглядит вот так:



    Если все норм, мы возвращаем true, и присваиваем out переменной user найденное значение. Если не норм, отдаём false, а out переменную заполняем дефолтным значением (в случае с классом это будет null).

    Использовать такой метод следует так:



    У подхода много плюсов:

    • Он идиоматичен. Такая конвенция знакома всем шарпистам, она используется в родных коллекциях, все знаю, как с этим работать.
    • Способ надежен. Просто проигнорировать возможную проблему не получится. Ведь метод возвращает bool, и пользователь кода вынужден будет обратить внимание на твою задумку.
    • Код использования выглядит достаточно лаконично и понятно.
    • Отлично подходит для ситуаций, когда причина возможной неудачи очевидна — Мы не тащим очевидную информацию в стиле «not found».
    • Не нужно создавать дополнительные файлы исключений, сама реализация очень проста синтаксически — метод не перегружается лишним кодом и докой.
    • Мы явным образом снимаем с себя ответственность за обработку неудачи, и передаем её вызывающему коду.

    Минусы тоже есть:

    • Это все ещё конвенция. Никто не мешает пользователю кода просто вызвать метод, не проверять возвращаемое значение, и начать использовать out-параметр.
    • Не подходит для случаев, когда хотим передать вызывающему коду причину ошибки. На самом деле, мы можем добавить второй out-параметр, error: Exception, но тогда мы потеряем бОльшую часть плюсов. Особенно если типов ошибок может быть несколько.
    • Вся лаконичность пойдет к черту, код испортится. Кроме того, это уже не будет общепринятой конвенцией, и есть шанс, что коллеги начнут крутить пальцем у виска. Так же есть вариант определять Enum со статусом операции, и возвращать его вместо bool.
    • Не работает с async-ами. C# это не поддерживает, и, похоже, не будет.
    • Плохо сочетается с новой фичей из C#8 — nullable. Потому что тип out параметра у нас по факту nullable, но если мы скажем об этом компилятору, он заставит пользователя кода проверять его дважды. А если не укажем, то у нас будет nullable параметр, у которого тип — NotNullable. Может вводить в заблуждение, и в целом очень неприятный расклад.

    Очень похожий способ — SomeOrDefault.


    Тоже распространенный для дотнета подход, когда мы отдаем найденное значение, а иначе null.

    Делается так:



    А использовать вот так:



    Плюсы:

    • Он так же идиоматичен. Все знают, что значит постфикс OrDefault, подход используется в родных коллекциях.
    • Начиная с C#8 этот способ так же может быть надежным. Если в вашем проекте включена фича компилятора nullable, и вы не кладете болт на варнинги, SomeOrDefault-а достаточно, чтобы гарантировать обработку провала.
    • Код использования — самое идиоматичное, что вообще можно увидеть в сишарпе. Все всё поймут. И его очень мало.
    • Так же подходит для ситуаций, когда причина возможной неудачи очевидна.
    • Никаких дополнительных файлов и нагрузки на реализатора — это очень легко написать и поддерживать.

    Минусы:

    • Это конвенция. Люди могут ей не следовать, или даже не знать о ней.
    • Не подходит для случаев, когда хотим передать вызывающему коду причину ошибки.
    • Если у вызывающего кода не C#8+ с включенным нулабл, единственная гарантия — это нейминг. Очень склизская гарантия.

    Наивысшая надежность — Maybe монада.


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

    Тогда метод будет выглядеть вот так:



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

    Использовать код можно например так:



    Здесь не так много плюсов, но они очень увесистые.

    • Гарантии, надежность. Есть только один способ обойти нашу защиту — взять, и руками скастить результат к Success. Но это я не знаю, кем надо быть.
    • Мы легко можем протаскивать необходимую информацию об ошибке через класс Failure. Здесь много чего можно наинженерить, но главное — возможности для этого есть. Правда это будет уже не Maybe, а Result монада, но какая разница.
    • Вербозность. По коду, который отдаёт Maybe сразу понятно, что все может пойти не по плану.
    • Сам тип Maybe может быть один, универсальный на весь проект.
    • Подход отлично масштабируется на асинхронный код.
    • Легко разделять ответственность — мы можем сделать методы, которые готовы работать с Maybe, и метода, которые не готовы. Так мы построим приложение, в котором четко, на уровне типов разделена ответственность по обработке ошибок.

    А минусы — паршивые.

    • Это совсем не идоматично. Твои коллеги не будут ожидать такие штуки, для C# такая практика — довольно спорная.
    • Костыльная реализация. Мы эмулируем Discriminated Union из фп с помощью наследования, но это именно что эмуляция. Поэтому компилятор не будет нам верить, что есть всего два возможных наследника, а будет плевать в нас варнингами, что наш switch-expression не обработал все возможные случаи.
    • Все ещё не полная надежность.
    • Переусложнение кода, не всегда оправданное.

    Короче, монады хороши с логической точки зрения, но их реализация в текущем сишарпе выглядит ну очень некрасиво и непривычно. Это само по себе — большая проблема. Но она скоро решится — разработчики языка обещают поставить в одной из будущих версий Discriminated Unions — именно с помощью таких конструкций монады реализованы во всяких F#-ах или Haskell-ях. Код с монадами перестанет быть уродливым и запутанным, но остается проблема с тем, что подход для шарпистов непривычен. Кроме того весь уже написанный код набит исключениями, и переезжать никто не будет, а куча разных способов решать одинаковые проблемы в одном проект — это совсем не хорошая идея.

    Да, у меня в статье Maybe представлена исключительно как концепт. У неё есть отличные реализации в виде библиотек. В случае, если нужно передать информацию об ошибке, используется монада Either/Result. Для которой так же существуют сторонние решения.

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


    Если мне не нужно знать, что там за ошибка, сам случай не сверх критичный, у меня C#8+ со включенным nullable, у всех пользователей кода тоже — я бы использовал SomeOrDefault. Если Nullable нету, тогда tryPattern. Если момент критичный, тогда Maybe.

    Если у меня нет nullable, и кейс асинхронный — значит try pattern и someOrDefault мне не пойдет, и тогда я бы тоже взял Maybe.

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

    Exception хорошо подходит для случаев:

    • Когда у тебя есть модуль, в нем произошла ошибка, и это значит что с этим модулем больше работать нельзя (например сторонний сервис упал). Выплевываем исключение, ловим его где то сверху, уведомляем все заинтересованные части системы, что сервис сдох.
    • Когда приложение не может продолжать свою работу. Например у нас десктопный софт, который является тонким клиентом, а сеть пропала. Бахаем ексепшн, ловим, говорим «извините», закрываемся.
    • Когда понятия не имеешь, что делать в случае ошибки, да и на проект тебе насрать — тогда бы я тоже взял Exception.

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

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

    Это поднимает куда более серьезную проблему. Ладно, подход с исключениями технически несовершенен, но даже если и был бы, есть штука, куда более несовершенная. Программисты. Человеческий фактор. Я вот пришел сюда, такой умный, начал учить как обрабатывать ошибки, а потом заглянул в код своих проектов, и везде вижу одно и то же- мой класс, как разработчика, недостаточно высок, я постоянно не понимаю, как разруливать исключительные ситуации. Я их игнорирую, логгирую, и прячу. Кроме тех мест, где они кому-то уже навредили, и меня заставили именно там все продумать. И никакие технические возможности языка не заставят меня продумывать все.

    Но. Они заставят продумывать чуть больше, может быть, на 5%, может на 1, может на 10. И это вообще единственный способ хоть как то уменьшать влияние человеческого фактора. Поэтому, я не вижу причин, чтобы отказываться от тех же монад или гарантированно обрабатываемых исключений.

    Я привел четыре концептуальных подхода к работе с ошибками, но на деле их намного больше. Например приходит в голову подход в Go — отдавать из функций кортеж (результат*ошибка). Как по мне- очень спорный способ, но я открыт к дискуссии. Делитесь мыслями в комментариях, какие ещё у нас есть варианты, и в чем их преимущество.

    Код примеров лежит здесь.



    На правах рекламы


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

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

    Какой способ ваш любимый

    • 33,3%Exceptions188
    • 12,4%TryPattern70
    • 22,5%SomeOrDefault127
    • 26,4%Maybe/Result149
    • 5,3%Другое (в комментариях)30
    VDSina.ru хостинг серверов
    Серверы в Москве и Амстердаме

    Комментарии 474

      +3

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

        +9

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

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

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

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

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

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

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

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

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

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

                0
                А почему если не секрет?
                  0
                  Потому что
                  Для каждого Сотрудник из Сотрудники Цикл
                    // какой-то код
                  КонецЦикла;
                  
                    +1

                    Че, падежи не завезли? ээх)

                      0

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


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

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


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

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

                      0
                      Аааа, в этом плане. Ну как бы на мой взгляд если уж кто-то решил стать программистом, да ещё и выбрал себе язык вроде C#, и при этом он не знает даже значение слова «maybe», то на мой взгляд тоже что-то где-то пошло очень не так… :)
                        +2
                        Когда я решил стать программистом, я тоже не знал слова «maybe». Но к счастью, в английском они там почти все слова из Паскаля взяли, поэтому английский потом я подтянул.
                          +2
                          Ну ок, решать стать программистом и не знать значение «maybe» допускается, но когда уже стал, то надо соответствовать :)
              +16

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


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


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

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


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

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


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

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

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

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

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


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


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

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


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


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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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


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


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


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


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


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

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

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


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

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


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

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

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


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

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


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


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

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

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


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

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

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

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

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


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

                                                  0

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


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

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


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

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

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

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

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


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

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

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

                                                        +1

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

                                                          –1
                                                          Да от куда мне знать, человек я темный. Я имел ввиду вот этот комментарий:
                                                          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_.
                                                            +1

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

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

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

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

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

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

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

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

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

                                                    +1

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

                                                      0

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


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

                                                        +1

                                                        «Пусть» значит, что стоило бы приводить обе эти функции в пример в исходной статье, чтобы избежать всяких непоняток. Тем не менее, я тут подумал — давайте обсудим GetUser(int id) и как её писать. И вот тут есть такие вводные:


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

                                                        Как на языках без исключений писать GetUser(int id)? Хотелось бы добавить исключения для этого случая?

                                                          +2
                                                          Есть такое хорошее мнение, с которым я согласен, что исключения — они для тех ситуаций, которые могли быть предотвращены при кодировании прямыми проверками или более мощной системой типов.

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


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

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


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

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

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

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

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

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

                                                  0
                                                  Либо у вас база данных юзеров по идее должна быть неизменной, запрос используется для авторизации, ошибка вводa практически исключена, а попытка неавторизованного доступа это критичная нештатная ситуация :)
                                                    +3
                                                    Она будет нештатная для метода GetUser(int id) ибо название метода подразумевает, что он возвращает юзера, если метод должен возвращать что-то еще, какие-то состояния, то и называться должен по-другому. Например, как сделано в том-же linq: First() — плюет эксепшеном если элемент не найден, FirstOrDefault() — не плюет эксепшенами.
                                                    Для метода, которому понадобился юзер, чтоб совершить над ними какие-то действия, в большинстве случаев отсутствие юзера также будет нештатной ситуацией: нет юзера — нет действий, возвращаем ошибку.
                                                    И эксепшен в данном случае будет удобнее, ибо обработать его можно непосредственно перед формированием ответа АПИ, а не везде каждый раз перепроверять по цепочке вызовов чтож там такое случилось.
                                                      –2

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


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

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

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

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

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

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

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

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

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

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


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

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


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

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


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


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

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

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

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

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


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

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

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

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

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

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

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

                                        +3

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

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

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

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

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

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

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

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

                                                        Обновил немного статью

                                                    0

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

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

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


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

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

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

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

                                                        +1

                                                        Чем же?

                                                          +2

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

                                                            –1

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

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

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

                                                          0

                                                          Тем не менее, есть forgetful functor из Either в Maybe, который ИМХО и связывает семантику.

                                                            0

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

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

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

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

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

                                                          Вот тоже не понимаю почему в шарпе не могли как в джаве сделать checked exceptions. Хотя бы опционально с возможностью включать/выключать для отдельных проектов.


                                                          Приходится использовать одноименное расширение для VS. Но оно к сожалению тоже не на 100% хорошо работает…

                                                            +9

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


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

                                                              0

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


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

                                                                +1

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


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


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

                                                                  0

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


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

                                                                    +3

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

                                                                      0
                                                                      Проблема в том что это тоже далеко не всегда хорошо работает. То есть если вы например работатете с async/await или с COM'om/unmanaged code.

                                                                      При этом вся ситуация выглядит гораздо приятнее если исключения хотя бы задокументированы и тем более если они задокументированы так что с определением их наличия справляется IDE. Ну и как бы если мы вернёмся к джаве, то никто не мешает иметь как checked, так и unchecked исключения.
                                                                        +2
                                                                        То есть если вы например работатете с async/await или с COM'om/unmanaged code.

                                                                        Для асинков есть TaskScheduler.UnobservedTaskException, в случае с COM/unmanaged там что угодно может быть, это правда. но там ни один из подходов не будет достаточно хорош
                                                                          0
                                                                          TaskScheduler.UnobservedTaskException тоже не панацея. И да, я не спорю что оно всё как-то более-менее работает и почти всегда можно найти какое-то решение. Но на мой взгляд многое было бы проще если в С# добавят пару фич для работы с исключениями. Даже если их добавят как опциональные.
                                                                            0
                                                                            Но на мой взгляд многое было бы проще если в С# добавят пару фич для работы с исключениями. Даже если их добавят как опциональные.

                                                                            Всё в ваших руках github.com/dotnet/csharplang
                                                                              0
                                                                              Ну это очень условно именно в моих руках. Но да, к счастью язык всё ещё развивается:)
                                                                    0
                                                                    В случае с функциями обратного вызова для функциональных примитивов вроде map, filter, reduce и т. д. добавить сигнатуру в метод нельзя.

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

                                                                    0

                                                                    Если бы 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>.

                                                                      0

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


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

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

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


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

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


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

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

                                                                      0

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

                                                                        +3

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

                                                                          –2

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

                                                                            –10

                                                                            Как и человека, который поставил мне минус (простите)

                                                                          +1

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

                                                                          +1

                                                                          Извините, но это всё не так.


                                                                          Checked exceptions прекрасно себя чувствуют в ФП, и монада Either — это такая каноническая реализация checked exceptions. Да, map/filter/reducefold имеют несовместимую сигнатуру, но совместимую сигнатуру имеют монадические версии этих функций.


                                                                          Основная проблема с checked exceptions в джаве (на мой взгляд, конечно) — отсутствие полиморфизма по ним.

                                                                    +5
                                                                    Идея простая. Есть отдельная сборка, в ней лежит абстрактный класс Maybe, у него два наследника, Success и Failure. Отдельная сборка и интёрнал конструктор нужны, чтобы гарантировать — наследников всегда будет только два.

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

                                                                      +5

                                                                      Неважен конкретный вариант. Главное в едином стиле на уровне команды/репы.

                                                                        –10
                                                                        А можно вспомнить опыт Golang и возвращать 2 значения, как именно — зависит от языка, но можно…
                                                                        1 значение — success flow, 2 значение — Error

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

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

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

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

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

                                                                              0

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

                                                                              +6

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

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

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

                                                                                    +7

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

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

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

                                                                                          0

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

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

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

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

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


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

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

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

                                                                                                  0

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

                                                                                                  0

                                                                                                  Проверка файла на наличие перед работой с ним не гарантирует успеха в общем случае
                                                                                                  Вот тут можно почитать подробнее — https://blog.paranoidcoding.com/2009/12/10/the-file-system-is-unpredictable.html

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

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

                                                                                                  +2

                                                                                                  C#:


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

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


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

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

                                                                                                    0

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

                                                                                                      –1
                                                                                                      Ну это же будет работать только на одном уровне. То есть он через 5 вызовов это не прокинет, если по стеку выше нет аналогичного заворота в Error<> и такого же типа.
                                                                                                        0

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

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

                                                                                                      Это же Rust'овский метод работы с возвращаемыми значениями!
                                                                                                        +1

                                                                                                        Если бы...

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

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

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

                                                                                                                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; }
                                                                                                                }
                                                                                                                
                                                                                                                  +18
                                                                                                                  public T Result { get; set; }

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

                                                                                                                    +4
                                                                                                                    и засеттить
                                                                                                                      0

                                                                                                                      А как надо?

                                                                                                                        +1

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

                                                                                                                          0

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


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

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

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

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

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


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

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


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

                                                                                                                                0

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


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

                                                                                                                      +7

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


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

                                                                                                                        +6
                                                                                                                        Готовить их на самом деле просто. Есть нехитрое правило: исключения, про которые вы не в курсе, доверьте дефолтному обработчику. Если вы знаете, что метод N в каких-то случаях бросает исключение ESomething, и его обработка влияет на логику вызывающего метода, то вы его обрабатываете. Если вы не знаете про его существование, то не обрабатываете.
                                                                                                                          +3

                                                                                                                          Ага. А потом у вас ложится всё приложение потому что где-то какой-то некритичный процесс кинул какое-то глупое исключение.

                                                                                                                            +4

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

                                                                                                                              0

                                                                                                                              Ну так об этом и речь. Ну то есть о том что я не могу просто игнорировать исключения просто потому что 'я не в курсе".

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

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

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

                                                                                                                                +3
                                                                                                                                Но если вы не знаете, что там может быть за исключение, и как восстанавливать работу в случае его возникновения, то откуда вы знаете, что процесс некритичный, а исключение глупое? Если этой информации нет в контракте метода, то полагаться на «авось» — тоже стрёмная стратегия.
                                                                                                                                  +1

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


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

                                                                                                                                    0
                                                                                                                                    Минуточку, но если вы знаете, что вот этот процесс некритичный и исключения от него нужно игнорировать, то вы знаете контракт.
                                                                                                                                      +1

                                                                                                                                      Ну вот смотрите. Скажем у меня есть софт, который иам что-то делает. Что-то важное.


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


                                                                                                                                      Могу я никак не обрабатывать исключения которые в теории может кинуть эта библиотека? Или мне как минимум надо завернуть её в общий try catch?

                                                                                                                                        +1