it-swarm.com.ru

Соглашения об исключениях или кодах ошибок

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

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

96
Jorge Ferreira

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

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

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

51
Jorge Ferreira

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

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

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

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

73
Tom Dunham

Я предпочитаю исключения, потому что

  • они прерывают поток логики
  • они получают выгоду от иерархии классов, которая дает больше возможностей/функциональности
  • при правильном использовании может представлять широкий спектр ошибок (например, InvalidMethodCallException также является LogicException, поскольку оба они возникают, когда в вашем коде есть ошибка, которую следует обнаружить до выполнения), и
  • они могут быть использованы для исправления ошибки (то есть определение класса FileReadException может затем содержать код для проверки, существует ли файл или заблокирован и т. д.)
22
JamShady

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

17
Maxam

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

Вот несколько статей, обсуждающих, сравнивающих и противопоставляющих две техники:

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

15
Pistos

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

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

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

Employee EmpOfMonth = GetEmployeeOfTheMonth();

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

Employee EmpOfMonth; 
if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR)
    // code to Handle the error here

Если вы кодируете так, что каждый метод выполняет одну-единственную простую вещь, вы должны генерировать исключение всякий раз, когда метод не может достичь желаемой цели метода. Исключения намного богаче и проще в использовании, чем коды ошибок. Ваш код намного чище - стандартный поток «нормального» пути кода может быть строго посвящен случаю, когда метод IS способен выполнить то, что вы хотели, чтобы он делал ... А затем код для очистки или обрабатывать «исключительные» обстоятельства, когда происходит что-то плохое, что мешает успешному завершению метода, может быть отделено от обычного кода. Кроме того, если вы не можете обработать исключение в том месте, где оно произошло, и должны передать его по стеку в пользовательский интерфейс (или, что еще хуже, через провод от компонента среднего уровня к пользовательскому интерфейсу), то с моделью исключения вы не нужно кодировать каждый промежуточный метод в вашем стеке, чтобы распознать и передать исключение в стек ... Модель исключений делает это для вас автоматически ... С кодами ошибок эта часть головоломки может очень быстро обременительно ,.

14
Charles Bretana

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

Когда возникает исключение, приложение больше не следует своему «нормальному» пути выполнения. Первая причина, по которой это так важно, заключается в том, что если автор кода не сделает все возможное и действительно плохое, программа остановится и не будет продолжать делать непредсказуемые вещи. Если код ошибки не проверяется и соответствующие действия не предпринимаются в ответ на неверный код ошибки, программа будет продолжать делать то, что она делает, и кто знает, каков будет результат этого действия. Есть много ситуаций, когда выполнение программы «что угодно» может оказаться очень дорогим. Рассмотрим программу, которая получает информацию об эффективности для различных финансовых инструментов, которые продает компания, и передает эту информацию брокерам/оптовикам. Если что-то пойдет не так и программа продолжит работу, она может отправить ошибочные данные о производительности брокерам и оптовикам. Я не знаю ни о ком другом, но я не хочу, чтобы кто-то сидел в офисе вице-президента и объяснял, почему мой кодекс заставил компанию получить 7-значные штрафы. Доставка сообщений об ошибках клиентам, как правило, предпочтительнее, чем доставка неправильных данных, которые могут показаться «реальными», и с последней ситуацией гораздо проще столкнуться с гораздо менее агрессивным подходом, таким как коды ошибок.

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

try {
    // Normal things are happening logic
catch (// A problem) {
    // Something went wrong logic
}

... предпочтительнее для этого:

// Some normal stuff logic
if (errorCode means error) {
    // Some stuff went wrong logic
}
// Some normal stuff logic
if (errorCode means error) {
    // Some stuff went wrong logic
}
// Some normal stuff logic
if (errorCode means error) {
    // Some stuff went wrong logic
}

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

10
Dogs

В прошлом я присоединился к лагерю кодов ошибок (слишком много занимался программированием на Си). Но теперь я увидел свет. 

Да, исключения являются небольшим бременем для системы. Но они упрощают код, уменьшая количество ошибок (и WTF).

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

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

9
Toon Krijthe

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

Существует несколько сценариев, где исключения являются очевидным выбором:

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

  2. Однако в ситуации 1 (когда происходит что-то неожиданное и необработанное, вы просто не хотите регистрировать это), исключения могут быть полезны, потому что вы можете добавить контекстную информацию. Например, если я получу исключение SqlException в моих помощниках данных нижнего уровня, я захочу отловить эту ошибку на низкоуровневом уровне (где я знаю команду SQL, которая вызвала ошибку), чтобы я мог получить эту информацию и перебросить с дополнительной информацией. Обратите внимание на волшебное Слово здесь: перебрасывать, а не глотать. Первое правило обработки исключений: не глотать исключения . Также обратите внимание, что моя внутренняя ловушка не должна ничего регистрировать, потому что внешняя ловушка будет иметь трассировку всего стека и может регистрировать ее.

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

    Одним из примеров может быть последовательность операторов SQL внутри блока транзакции. Опять же, это также «неуправляемая» ситуация, даже если вы решили поймать это рано (обрабатывайте это локально, вместо того, чтобы подниматься вверх), это все еще фатальная ситуация, откуда лучший результат - отменить все или, по крайней мере, прервать большую часть процесса.
    (*) Это похоже на on error goto, который мы использовали в старой Visual Basic

  4. В конструкторах вы можете генерировать только исключения.

Тем не менее, во всех других ситуациях, когда вы возвращаете некоторую информацию, по которой вызывающая сторона МОЖЕТ/ДОЛЖНА предпринять какое-либо действие, использование кодов возврата, вероятно, является лучшей альтернативой. Это включает в себя все ожидаемые «ошибки», потому что, вероятно, они должны обрабатываться непосредственным вызывающим абонентом, и вряд ли нужно будет поднимать слишком много уровней в стеке.

Конечно, всегда можно обрабатывать ожидаемые ошибки как исключения и затем сразу перехватывать их на один уровень выше, а также можно охватить каждую строку кода в попытке перехвата и выполнить действия для каждой возможной ошибки. IMO, это плохой дизайн, не только потому, что он намного более многословен, но особенно потому, что возможные исключения, которые могут быть выброшены, не очевидны без чтения исходного кода - и исключения могут быть выброшены из любого глубокого метода, создавая невидимые gotos . Они нарушают структуру кода, создавая несколько невидимых точек выхода, которые затрудняют чтение и проверку кода. Другими словами, вы никогда не должны использовать исключения как flow-control, потому что это будет трудно понять и поддерживать другим. Может быть даже трудно понять все возможные потоки кода для тестирования.
Опять же: для правильной очистки/утилизации вы можете использовать try-finally, не перехватывая ничего.

Самая популярная критика в отношении кодов возврата заключается в том, что «кто-то может игнорировать коды ошибок, но в том же смысле кто-то может также проглотить исключения. Плохая обработка исключений проста в обоих методах. Но написание Хорошая программа, основанная на кодах ошибок, по-прежнему намного проще, чем написать программу, основанную на исключениях . И если одна по какой-либо причине решит игнорировать все ошибки (старый on error resume next), вы можете легко сделать это с помощью кодов возврата и Вы не можете сделать это без большого количества пробных примеров.Вторая по популярности критика в отношении кодов возврата заключается в том, что «трудно всплыть», но это потому, что люди не понимают, что исключения относятся к невосстановимым ситуациям, а коды ошибок - нет.

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

Подвести итог:.

  • .

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

Однако, что касается GO, он также позволяет множественные возвращаемые значения , что очень помогает при использовании кодов возврата, поскольку вы можете одновременно возвращать ошибку и что-то еще. На C #/Java мы можем добиться этого без параметров, Tuples или (моего любимого) Generics, которые в сочетании с перечислениями могут предоставить вызывающей стороне четкие коды ошибок:.

public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options) { .... return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area"); ... return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order); } var result = CreateOrder(options); if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK) // do something else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS) order = result.Entity; // etc...

If I add a new possible return in my method, I can even check all callers if they are covering that new value in a switch statement for example. You really can't do that with exceptions. When you use return codes, you'll usually know in advance all possible errors, and test for them. With exceptions you usually don't know what might happen. Wrapping enums inside exceptions (instead of Generics) is an alternative (as long as it's clear the type of exceptions that each method will throw), but IMO it's still bad design.

5
drizin

Я могу сидеть здесь на заборе, но ...

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

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

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

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

4
jrb

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

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

4
Eclipse

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

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

Простой пример на Java:

public class DataValidationException extends Exception {


    private DataValidation error;

    /**
     * 
     */
    DataValidationException(DataValidation dataValidation) {
        super();
        this.error = dataValidation;
    }


}

enum DataValidation{

    TOO_SMALL(1,"The input is too small"),

    TOO_LARGE(2,"The input is too large");


    private DataValidation(int code, String input) {
        this.input = input;
        this.code = code;
    }

    private String input;

    private int code;

}

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

3
sakana

Подписи метода должны сообщать вам, что делает метод. Что-то вроде long errorCode = getErrorCode (); может быть хорошо, но long errorCode = fetchRecord (); сбивает с толку.

3
Paul Croarkin

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

Во всех других случаях, исключения, вероятно, путь.

3
Claudiu

Исключения - для исключительных обстоятельств, т. Е. Когда они не являются частью нормального потока кода. 

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

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

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

Но если вы пойдете в другом направлении, использование исключений позволит вам создавать абстракции еще более высокого уровня для обработки ошибок, которые могут сделать ваш код еще более выразительным и естественным. Я очень рекомендую прочитать эту превосходную, но недооцененную статью эксперта по С ++ Андрея Александреску на тему того, что он называет «Принуждения»: http://www.ddj.com/cpp/184403864 . Хотя это статья на C++, принципы в целом применимы, и я довольно успешно перевел концепцию принудительного применения на C #.

2
philsquared

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

В SOA, где методы могут вызываться на разных машинах, исключения не могут передаваться по проводам, вместо этого мы используем ответы об успехе/неудаче со структурой, подобной приведенной ниже (C #):

public class ServiceResponse
{
    public bool IsSuccess => string.IsNullOrEmpty(this.ErrorMessage);

    public string ErrorMessage { get; set; }
}

public class ServiceResponse<TResult> : ServiceResponse
{
    public TResult Result { get; set; }
}

И используйте так:

public async Task<ServiceResponse<string>> GetUserName(Guid userId)
{
    var response = await this.GetUser(userId);
    if (!response.IsSuccess) return new ServiceResponse<string>
    {
        ErrorMessage = $"Failed to get user."
    };
    return new ServiceResponse<string>
    {
        Result = user.Name
    };
}

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

2
orad

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

Возврат недействительных указателей, которые могут быть разыменованы (например, вызовет исключение NullPointerException в Java), недопустим.

Использование нескольких различных числовых кодов ошибок (-1, -2) в качестве возвращаемых значений для одной и той же функции обычно является плохим стилем, поскольку клиенты могут выполнить проверку "== -1" вместо "<0".

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

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

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

1
tkruse

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

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

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

1
gomons

Мое общее правило:

  • В функции может появиться только одна ошибка: используйте код ошибки (в качестве параметра функции)
  • Может появиться более одной конкретной ошибки: сгенерировать исключение
0
DEADBEEF

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

0
Jim C

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

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

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

0
ashah