Иногда мне приходится писать метод или свойство для библиотеки классов, для которой нет ничего исключительного в том, что у меня нет реального ответа, но есть ошибка. Что-то не может быть определено, недоступно, не найдено, в настоящее время невозможно или больше нет доступных данных.
Я думаю, что есть три возможных решения для такой относительно неисключительной ситуации, чтобы указать сбой в C # 4:
- вернуть магическое значение, которое иначе не имеет значения (например,
null
и-1
); - бросить исключение (например
KeyNotFoundException
); - вернуть
false
и предоставить фактическое возвращаемое значение вout
параметре (например,Dictionary<,>.TryGetValue
).
Итак, вопросы: в какой неисключительной ситуации я должен выбросить исключение? И если я не должен бросать: когда возвращается волшебное значение, предложенное выше, реализующее Try*
метод с out
параметром ? (Мне out
параметр кажется грязным, и это больше работы, чтобы использовать его правильно.)
Я ищу фактические ответы, такие как ответы, включающие рекомендации по проектированию (я не знаю ничего о Try*
методах), удобство использования (как я спрашиваю об этом для библиотеки классов), согласованность с BCL и удобочитаемость.
В библиотеке базовых классов .NET Framework используются все три метода:
- вернуть магическое значение, которое иначе не имеет значения:
Collection<T>.IndexOf
возвращает -1,StreamReader.Read
возвращает -1,Math.Sqrt
возвращает NaN,Hashtable.Item
возвращает ноль;
- бросить исключение:
Dictionary<,>.Item
выдает KeyNotFoundException,Double.Parse
бросает FormatException; или же
- вернуть
false
и предоставить фактическое возвращаемое значение вout
параметре:
Обратите внимание, что, как Hashtable
было создано в то время, когда в C # не было обобщений, он использует object
и поэтому может возвращаться null
как магическое значение. Но с дженериками, исключения используются в Dictionary<,>
, и первоначально это не имело TryGetValue
. Видимо, понимание меняется.
Очевидно, что Item
- TryGetValue
и Parse
- TryParse
двойственность есть причина, поэтому я полагаю , что бросать исключения для неисключительных отказов в C # 4 не сделали . Однако Try*
методы не всегда существуют, даже когда они Dictionary<,>.Item
существуют.
источник
Ответы:
Я не думаю, что ваши примеры действительно эквивалентны. Есть три отдельные группы, каждая из которых имеет свое собственное обоснование своего поведения.
StreamReader.Read
или когда есть простое в использовании значение, которое никогда не будет действительным ответом (-1 дляIndexOf
).Приведенные вами примеры совершенно понятны для случаев 2 и 3. Что касается магических значений, можно утверждать, является ли это хорошим решением для проектирования или не во всех случаях.
NaN
ВозвращаемыйMath.Sqrt
является частным случаем - он соответствует стандарту с плавающей запятой.источник
Either<TLeft, TRight>
монады?Вы пытаетесь сообщить пользователю API, что они должны делать. Если вы выбрасываете исключение, ничто не заставляет их ловить его, и только чтение документации даст им понять, каковы все возможности. Лично я нахожу медленным и утомительным копаться в документации, чтобы найти все исключения, которые может выдать определенный метод (даже если это в intellisense, мне все равно придется копировать их вручную).
Волшебное значение по-прежнему требует, чтобы вы прочитали документацию и, возможно, использовали некоторую
const
таблицу для декодирования значения. По крайней мере, у него нет накладных расходов на исключение для того, что вы называете неисключительным случаем.Вот почему, хотя
out
параметры иногда не одобряются, я предпочитаю этот метод сTry...
синтаксисом. Это канонический синтаксис .NET и C #. Вы сообщаете пользователю API, что он должен проверить возвращаемое значение, прежде чем использовать результат. Вы также можете включить второйout
параметр с полезным сообщением об ошибке, которое снова помогает при отладке. Вот почему я голосую за решениеTry...
сout
параметром.Другой вариант - вернуть специальный «результат» объекта, хотя я нахожу это гораздо более утомительным:
Тогда ваша функция выглядит правильно, потому что она имеет только входные параметры и возвращает только одну вещь. Просто то, что он возвращает, это своего рода кортеж.
На самом деле, если вам
Tuple<>
нравятся такие вещи, вы можете использовать новые классы, представленные в .NET 4. Лично мне не нравится тот факт, что значение каждого поля менее явное, потому что я не могу датьItem1
иItem2
полезные имена.источник
IMyResult
причина, можно передать более сложный результат, чем простоtrue
илиfalse
или значение результата.Try*()
полезно только для простых вещей, таких как преобразование строки в int.Как уже показывают ваши примеры, каждый такой случай должен оцениваться отдельно, и между «исключительными обстоятельствами» и «контролем потока» существует значительный спектр серого, особенно если ваш метод предназначен для многократного использования и может использоваться в совершенно разных схемах. чем это было изначально разработано. Не ожидайте, что мы все здесь согласимся с тем, что означает «не исключение», особенно если вы немедленно обсудите возможность использования «исключений» для реализации этого.
Мы также можем не согласиться с тем, какой дизайн делает код более легким для чтения и поддержки, но я предполагаю, что у разработчика библиотеки есть четкое личное представление об этом, и ему нужно только сбалансировать его с другими вовлеченными соображениями.
Краткий ответ
Следуйте своим внутренним чувствам, кроме случаев, когда вы разрабатываете довольно быстрые методы и ожидаете возможности непредвиденного повторного использования.
Длинный ответ
Каждый будущий абонент может свободно переводить между кодами ошибок и исключениями по своему усмотрению в обоих направлениях; это делает два подхода к проектированию практически эквивалентными, за исключением производительности, удобства отладки и некоторых контекстов ограниченной функциональной совместимости. Обычно это сводится к производительности, поэтому давайте сосредоточимся на этом.
Как правило, ожидайте, что выбрасывание исключения в 200 раз медленнее, чем обычное возвращение (на самом деле, в этом есть существенная разница).
Как еще одно практическое правило, выбрасывание исключения часто позволяет получить более чистый код по сравнению с самыми грубыми магическими значениями, поскольку вы не полагаетесь на то, что программист переводит код ошибки в другой код ошибки, поскольку он проходит через несколько уровней клиентского кода в направлении точка, в которой достаточно контекста для последовательной и адекватной обработки. (Особый случай: здесь,
null
как правило, лучше, чем у других магических ценностей, из-за его склонности к автоматическому переводу себяNullReferenceException
в случае некоторых, но не всех типов дефектов; обычно, но не всегда, довольно близко к источнику дефекта. )Так какой урок?
Для функции, которая вызывается всего несколько раз в течение жизненного цикла приложения (например, инициализация приложения), используйте все, что дает вам более понятный и понятный код. Производительность не может быть проблемой.
Для одноразовой функции используйте все, что дает более чистый код. Затем выполните некоторое профилирование (при необходимости вообще) и измените исключения на коды возврата, если они входят в число предполагаемых верхних узких мест на основе измерений или общей структуры программы.
Для дорогой многоразовой функции используйте все, что дает вам более чистый код. Если вам, по сути, всегда приходится проходить в обе стороны по сети или анализировать XML-файл на диске, затраты на создание исключения, вероятно, незначительны. Гораздо важнее не потерять детали любого сбоя, даже не случайно, чем быстро вернуться из «неустранимого отказа».
Бережливая многоразовая функция требует больше размышлений. Используя исключения, вы вызываете что-то вроде 100-кратного замедления для вызывающих, которые увидят исключение при половине их (многих) вызовов, если тело функции выполняется очень быстро. Исключения по-прежнему остаются вариантом дизайна, но вам придется предоставить альтернативу с низкими накладными расходами для абонентов, которые не могут себе этого позволить. Давайте посмотрим на пример.
Вы перечисляете отличный пример того
Dictionary<,>.Item
, что, собственно говоря, изменилось от возвратаnull
значений к выбрасываниюKeyNotFoundException
между .NET 1.1 и .NET 2.0 (только если вы хотите считатьHashtable.Item
его практическим неуниверсальным предвестником). Причина этого «изменения» здесь не без интереса. Оптимизация производительности типов значений (не больше бокса) сделало оригинальное магическое значение (null
) недоступным;out
параметры просто вернут небольшую часть затрат на производительность. Это последнее соображение производительности совершенно незначительно по сравнению с накладными расходами на бросание aKeyNotFoundException
, но дизайн исключений здесь все же лучше. Почему?Contains
перед любым обращением к индексатору, и этот шаблон читается совершенно естественно. Если разработчик хочет, но забывает звонитьContains
, никакие проблемы с производительностью не могут закрасться;KeyNotFoundException
достаточно громко, чтобы быть замеченным и исправленным.источник
Contains
перед вызовом индексатора может вызвать состояние гонки, которое не обязательно должно присутствоватьTryGetValue
.ConcurrentDictionary
для многопоточного доступа иDictionary
однопоточного доступа для оптимальной производительности. То есть неиспользование `` Contains` НЕ делает поток кода безопасным с этим конкретнымDictionary
классом.Вы не должны допустить провала.
Я знаю, это волнующе и идеалистично, но выслушай меня. При разработке дизайна есть ряд случаев, когда у вас есть возможность выбрать версию, в которой нет режимов отказа. Вместо того, чтобы иметь «FindAll», который терпит неудачу, LINQ использует предложение where, которое просто возвращает пустое перечисляемое значение. Вместо того, чтобы иметь объект, который необходимо инициализировать перед использованием, позвольте конструктору инициализировать объект (или инициализировать при обнаружении неинициализированного). Ключ удаляет ветку сбоев в коде потребителя. Это проблема, поэтому сосредоточьтесь на ней.
Другой стратегией для этого является
KeyNotFound
сценарий. Почти в каждой кодовой базе, над которой я работал с 3.0, существует что-то вроде этого метода расширения:Для этого нет режима реального сбоя.
ConcurrentDictionary
имеет аналогичныйGetOrAdd
встроенный.Все это говорит, что всегда будут времена, когда это просто неизбежно. Все три имеют свое место, но я бы предпочел первый вариант. Несмотря на все, что связано с нулевой опасностью, он хорошо известен и вписывается во многие сценарии «предмет не найден» или «результат не применим», которые составляют набор «не исключительный сбой». Особенно, когда вы создаете типы значений, допускающие значение NULL, значение «это может не сработать» очень явно в коде, и его трудно забыть / испортить.
Второй вариант достаточно хорош, когда ваш пользователь делает что-то глупое. Дает вам строку с неверным форматом, пытается установить дату 42 декабря ... что-то, что вы хотите, чтобы вещи быстро и эффектно взрывались во время тестирования, чтобы выявлять и исправлять плохой код.
Последний вариант мне больше всего не нравится. Выходные параметры неудобны и имеют тенденцию нарушать некоторые из лучших практик при создании методов, такие как фокусировка на чем-то одном и отсутствие побочных эффектов. Кроме того, выходной параметр обычно имеет смысл только во время успеха. Тем не менее, они важны для определенных операций, которые обычно ограничены проблемами параллелизма или соображениями производительности (например, когда вы не хотите совершать вторую поездку в БД).
Если возвращаемое значение и выходной параметр нетривиальны, то предположение Скотта Уитлока о результирующем объекте является предпочтительным (как
Match
класс Regex ).источник
out
параметры полностью ортогональны к вопросу о побочных эффектах. Изменениеref
параметра является побочным эффектом, а изменение состояния объекта, которое вы передаете через входной параметр, является побочным эффектом, ноout
параметр - это просто неуклюжий способ заставить функцию возвращать более одного значения. Там нет никакого побочного эффекта, просто несколько возвращаемых значений.Всегда предпочитайте бросать исключения. У него есть единый интерфейс среди всех функций, которые могут давать сбой, и он указывает на сбой настолько шумно, насколько это возможно - очень желательное свойство.
Обратите внимание , что
Parse
иTryParse
на самом деле не то же самое , за исключением режимов отказа. Факт, что этоTryParse
может также возвратить значение, действительно несколько ортогонально. Рассмотрим ситуацию, когда, например, вы проверяете некоторые входные данные. На самом деле вам все равно, что это за значение, если оно действительно. И нет ничего плохого в том, чтобы предлагать какую-тоIsValidFor(arguments)
функцию. Но это никогда не может быть основным режимом работы.источник
Как уже отмечали другие, магическое значение (включая логическое возвращаемое значение) не так уж и велико, кроме как маркер конца диапазона. Причина: семантика не является явной, даже если вы исследуете методы объекта. Вы должны прочитать всю документацию по всему объекту вплоть до «о, да, если он возвращает -42, это означает, что бла бла бла».
Это решение может использоваться по историческим причинам или по причинам производительности, но в противном случае его следует избегать.
Это оставляет два общих случая: исследование или исключения.
Здесь эмпирическое правило заключается в том, что программа не должна реагировать на исключения, кроме как для обработки, когда программа / непреднамеренно / нарушает какое-либо условие. Для того, чтобы этого не произошло, следует использовать зондирование. Следовательно, исключение либо означает, что соответствующее исследование не было выполнено заранее, либо произошло нечто совершенно неожиданное.
Пример:
Вы хотите создать файл по заданному пути.
Вам следует использовать объект File, чтобы заранее оценить, является ли этот путь допустимым для создания или записи файла.
Если ваша программа каким-то образом все-таки пытается записать путь, который запрещен или иным образом недоступен для записи, вы должны получить исключение. Это может произойти из-за состояния гонки (какой-то другой пользователь удалил каталог или сделал его доступным только для чтения после того, как вы попытались)
Задача обработки неожиданного сбоя (сигнализируемая исключением) и проверки, являются ли условия правильными для операции заранее (зондирование), как правило, будут структурированы по-разному, и поэтому должны использовать разные механизмы.
источник
Я думаю, что
Try
шаблон - лучший выбор, когда код просто указывает на то, что произошло. Я ненавижу param и люблю обнуляемый объект. Я создал следующий класстак что я могу написать следующий код
Выглядит как nullable, но не только для типа значения
Другой пример
источник
try
catch
нанесет ущерб вашей производительности, если вы звонитеGetXElement()
и терпите неудачу много раз.public struct Nullable<T> where T : struct
главное отличие в ограничении. Кстати, последняя версия находится здесь github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…Если вас интересует маршрут «магическая ценность», еще один способ решить эту проблему - перегрузить цель класса Lazy. Несмотря на то, что Lazy предназначен для отсрочки создания экземпляров, ничто не мешает вам использовать как вариант Maybe или Option. Например:
источник